1. 项目概述:为什么在VS2015 C++环境下折腾WebService?
如果你是一个用惯了C#或者Java的开发者,第一次在Visual Studio 2015的C++环境里想创建一个WebService,或者去调用一个现成的,大概率会一头撞在墙上。那种感觉就像你开惯了自动挡轿车,突然给你一辆手动挡的老式卡车,虽然都是车,但操作逻辑和需要的工具完全不是一个路数。网上搜到的教程,十有八九是C#的,剩下的一两个C++的,可能还是针对古老的SOAP Toolkit或者VS2005的,对着一堆失效的菜单和找不到的选项,挫败感极强。
这个项目标题“VS2015 C++ 创建和调用webservice教程”,直指的就是这个痛点。它不是一个简单的功能演示,而是一份针对特定历史环境(VS2015)和特定语言(原生C++)的“生存指南”。VS2015是一个承上启下的版本,它移除了早期一些对WebService的“傻瓜式”支持(比如那个传说中的“添加Web引用”),但新的、更现代的方案(如RESTful API的轻量级客户端库)在当时又未完全成熟或普及。因此,在这个环境下,我们往往需要回归本质,手动处理SOAP协议、XML解析和HTTP通信这些底层细节。
所以,这篇内容的价值在于:为你厘清在VS2015 C++环境中处理WebService的核心思路,提供一套切实可行、步骤清晰的实操方案,并附上我踩过无数坑后总结的避坑技巧。无论你是需要集成一个遗留的SOAP系统,还是在特定约束下必须使用C++进行Web服务交互,这篇文章都能帮你把路走通。
2. 核心思路与方案选型:为什么是gSOAP?
面对“用C++调用WebService”这个问题,尤其是在VS2015这个环境下,我们有几个岔路口要走。首先得明白,WebService(特指基于SOAP/WSDL的)本质上是一种基于XML的远程过程调用(RPC)协议,跑在HTTP/HTTPS之上。因此,核心工作就三块:生成客户端代理代码、处理XML序列化/反序列化、执行HTTP网络通信。
在VS2015的C++世界里,微软并没有提供一个像C#里“添加服务引用”那样开箱即用的完美工具。历史上,VS2008及更早版本有“添加Web引用”,但之后被面向.NET的WCF服务引用所取代,对原生C++的支持变得非常间接。因此,我们通常转向第三方成熟库。主流选择有:
- gSOAP:这是C/C++领域处理SOAP WebService的“老炮”和事实标准。它是一个编译器(
soapcpp2)和运行时库的组合。你给它一个WSDL(Web服务描述语言)文件,它能生成一堆纯C/C++的客户端存根(stub)代码,以及数据结构的序列化/反序列化代码。然后你链接它的库,就能像调用本地函数一样调用远程服务。 - Microsoft SOAP Toolkit:非常古老的技术,ActiveX控件形式,在现代C++项目中集成麻烦,且VS2015默认不支持,不推荐。
- 手动构建SOAP消息:使用像
libcurl处理HTTP,用tinyxml2或pugixml解析和构建XML。这是最灵活但也是最繁琐、最容易出错的方式,只适合极其简单的服务或学习协议原理。
为什么我们首选gSOAP?因为它把最脏最累的活——根据WSDL生成协议代码和编组(marshalling)——给自动化了。你只需要关心业务逻辑。它的生成代码质量很高,兼容性广,并且是开源跨平台的。虽然初始配置看起来步骤多一点,但一旦跑通,后续开发和维护成本极低。对于标题中“创建和调用”的双重需求,gSOAP同样支持生成服务端骨架(skeleton)代码,方便你搭建WebService服务端。
因此,本教程的核心方案确定为:使用gSOAP工具链,在VS2015中创建C++项目,通过WSDL生成客户端/服务端代码,并完成编译、链接和调试。对于纯调用场景,我们重点讲解客户端;若涉及创建,则会补充服务端部分。
3. 环境准备与工具链部署
工欲善其事,必先利其器。在VS2015里用C++搞WebService,第一步不是打开IDE,而是把“武器库”准备好。
3.1 获取并安装gSOAP开发包
gSOAP的官方发布通常是一个源代码包。最稳妥的方式是去其SourceForge或官方GitHub仓库下载稳定版(例如2.8.x版本)。下载后,你得到一个压缩包,比如gsoap_2.8.134.zip。
关键步骤与决策点:
- 解压路径:不要放在包含中文或空格的路径里。我习惯在
C:\Dev或D:\Libraries下创建一个gsoap文件夹,比如D:\Libraries\gsoap_2.8.134。这个路径我们记为$GSOAP_ROOT。 - 核心工具:在
$GSOAP_ROOT\bin\win32(或对应64位目录)下,找到两个关键的.exe文件:wsdl2h.exe: 这是“WSDL to Header”转换器。它读取一个或多个WSDL/XSD文件,生成一个统一的、gSOAP格式的C++头文件(.h文件)。这个头文件定义了所有的数据结构和服务接口。soapcpp2.exe: 这是SOAP编译器。它读取上一步生成的.h文件,生成大量的C/C++源代码文件(客户端存根、服务端骨架、序列化例程等)。
- 编译运行时库:gSOAP的核心功能封装在几个运行时库中(
libgsoap++.lib,libgsoap.lib,libgsoapssl.lib等)。$GSOAP_ROOT目录下通常有Visual Studio的解决方案文件(.sln),例如gsoap.sln或位于gsoap\VisualStudio2015目录下。- 用VS2015打开这个解决方案。
- 根据你的项目需要,选择编译“Release”或“Debug”版本,以及“Win32”或“x64”平台。这里必须和你后续要创建的C++客户端项目平台一致!如果客户端是x64,这里也必须编译x64的库。
- 编译整个解决方案。成功后,在
$GSOAP_ROOT\lib或$GSOAP_ROOT\VisualStudio2015\lib下应该能找到生成的.lib文件。
实操心得1:平台一致性是第一个大坑。我见过太多人在这里栽跟头:用Win32的gSOAP库去链接x64的项目,导致一堆“无法解析的外部符号”链接错误。务必确保:你的gSOAP库、你的项目、你项目依赖的其他第三方库(如OpenSSL,如果需要HTTPS),这三者的平台(Win32/x64)和运行时库(MT/MD)设置完全一致。一个简单的检查方法是,在VS2015的项目属性 -> C/C++ -> 代码生成 -> 运行库,查看设置。
3.2 准备一个测试用的WSDL
为了演示,我们需要一个WSDL文件。你可以使用公司内部的WebService地址(后面加上?wsdl参数),或者找一个公网上简单的测试服务。例如,以前有一个著名的WebService用于查询天气(http://www.webxml.com.cn/WebServices/WeatherWebService.asmx?wsdl),虽然现在可能已失效,但原理不变。你也可以自己写一个简单的WSDL,或者使用gSOAP自带的示例。
这里,假设我们有一个简单的计算器服务WSDL,它提供了一个Add方法,接收两个整数,返回它们的和。我们将这个WSDL文件保存为Calculator.wsdl,放在项目目录下。
3.3 创建Visual Studio 2015 C++项目
打开VS2015,创建一个新的“Win32控制台应用程序”项目,命名为WebServiceClient。在应用程序向导中,选择“控制台应用程序”,并取消勾选“预编译头”。对于gSOAP项目,预编译头有时会带来不必要的复杂性,新手可以先避开。
创建好后,我们需要配置项目属性,让它能找到gSOAP。
- 包含目录(Include Directories):在项目属性 -> C/C++ -> 常规 -> 附加包含目录中,添加gSOAP的头文件路径。通常是
$GSOAP_ROOT和$GSOAP_ROOT\import。例如:D:\Libraries\gsoap_2.8.134;D:\Libraries\gsoap_2.8.134\import。 - 库目录(Library Directories):在项目属性 -> 链接器 -> 常规 -> 附加库目录中,添加你编译好的gSOAP库文件(
.lib)所在路径。例如:D:\Libraries\gsoap_2.8.134\VisualStudio2015\lib\Release\x64。 - 附加依赖项(Additional Dependencies):在项目属性 -> 链接器 -> 输入 -> 附加依赖项中,添加你需要链接的库文件名。对于C++客户端,通常需要
libgsoap++.lib和libgsoap.lib。如果服务使用HTTPS,还需要libgsoapssl.lib以及OpenSSL的库(如libssl.lib,libcrypto.lib)。 - 预处理器定义(Preprocessor Definitions):为了确保兼容性和功能,通常需要添加
WITH_NONAMESPACES和WITH_NOGLOBAL。这告诉gSOAP编译器不要假设使用默认的命名空间,代码更具可移植性。可以在项目属性 -> C/C++ -> 预处理器 -> 预处理器定义中添加。
4. 从WSDL到可执行代码:生成与集成
环境配好了,项目建好了,现在进入核心环节:把WSDL这个“蓝图”变成我们C++项目里能用的代码。
4.1 使用wsdl2h生成头文件
我们不在VS2015里直接操作,而是打开命令提示符(CMD),切换到你的项目目录(即Calculator.wsdl所在的目录)。
执行以下命令:
"D:\Libraries\gsoap_2.8.134\bin\win64\wsdl2h.exe" -o Calculator.h Calculator.wsdl解释一下参数:
-o Calculator.h: 指定输出的头文件名为Calculator.h。Calculator.wsdl: 输入的WSDL文件。
如果WSDL依赖其他Schema(XSD),或者服务地址比较复杂,你可能需要更多参数,例如-s(不生成STL代码,为了兼容老编译器)或-n name(使用指定的命名空间前缀)。对于大多数现代C++项目,直接使用STL是没问题的。
运行成功后,会在当前目录生成Calculator.h文件。用文本编辑器打开它,你会看到gSOAP根据WSDL生成的C++类定义,例如一个名为ns1__Add的结构体(代表输入参数),和一个名为CalculatorSoap的服务代理类,其中包含Add等虚拟方法。
4.2 使用soapcpp2生成C++源代码
接下来,用soapcpp2处理这个头文件。继续在CMD中执行:
"D:\Libraries\gsoap_2.8.134\bin\win64\soapcpp2.exe" -i -C -I"D:\Libraries\gsoap_2.8.134\import" Calculator.h参数解析:
-i: 生成C++代理类(client proxy)和对象类(方便使用)。这会让生成的代码更符合C++的面向对象习惯。-C: 仅生成客户端代码。如果你也在创建WebService服务端,则不要加这个参数,它会同时生成客户端和服务端代码。-I: 指定gSOAP的import目录路径,这个目录下有一些必要的内置文件(如stlvector.h)。Calculator.h: 上一步生成的头文件。
执行后,会生成一大堆文件,其中对我们客户端最重要的有:
soapCalculatorSoapProxy.h和soapCalculatorSoapProxy.cpp: 这是主要的客户端代理类实现。CalculatorSoap.nsmap: 一个包含XML命名空间映射的C/C++代码片段,必须在某个源文件中包含(通常是主文件)。- 一堆以
.nsmap、.xsd、.xml结尾的文件,以及soapStub.h、soapH.h、soapC.cpp等。这些是序列化和协议相关的支撑代码。
实操心得2:生成文件的管理。这一堆生成文件看着吓人。一个清晰的做法是:在VS2015解决方案资源管理器中,为你的项目添加一个“Generated”筛选器(文件夹),然后把所有
soapcpp2生成的.cpp和.h文件(除了CalculatorSoap.nsmap)都添加进去。CalculatorSoap.nsmap这个文件比较特殊,它通常被#include到你的主源文件(如main.cpp)里。不要把生成的文件和你的手写业务代码混在一起,这样项目结构清晰,也方便清理和重新生成。
4.3 将生成的文件添加到VS2015项目
- 在VS2015的“解决方案资源管理器”中,右键点击你的
WebServiceClient项目,选择“添加” -> “现有项”。 - 浏览并选中上一步生成的所有
.cpp文件(主要是soapCalculatorSoapProxy.cpp和soapC.cpp),以及soapStub.h、soapH.h、soapCalculatorSoapProxy.h等头文件,将它们添加到项目中。 - 在你的主程序文件(例如
main.cpp)的开头,包含必要的头文件和命名空间映射:// main.cpp #include <iostream> #include "soapCalculatorSoapProxy.h" // 客户端代理头文件 #include "CalculatorSoap.nsmap" // 必须包含的命名空间映射 int main() { // 你的代码将写在这里 return 0; } - 确保项目能正常编译。此时可能会遇到一些编译错误,最常见的是关于
std::string、std::vector等STL类型的问题。这是因为gSOAP默认可能使用它自己包装的类型。如果遇到,可以回到wsdl2h那一步,尝试加上-s参数禁止STL,但更推荐的方式是确保你的项目正确包含了标准库,并且wsdl2h生成的头文件是兼容你编译器的。对于VS2015,通常直接使用STL是没问题的。
5. 编写客户端调用代码
生成和集成的“重活”干完了,现在来点“轻巧”的:编写实际调用服务的代码。你会发现,有了gSOAP生成的代理类,调用远程WebService和调用一个本地C++类方法几乎一样简单。
5.1 初始化与调用
在main.cpp中,我们编写具体的调用逻辑:
#include <iostream> #include “soapCalculatorSoapProxy.h” #include “CalculatorSoap.nsmap” int main() { // 1. 创建服务代理对象 CalculatorSoapProxy calculator; // 2. 准备请求参数(根据生成的`Calculator.h`中的定义) // 假设生成的输入参数结构体是 `_ns1__Add` _ns1__Add request; request.a = 10; request.b = 20; // 3. 准备接收响应的变量(根据生成的`Calculator.h`中的定义) // 假设生成的响应结构体是 `_ns1__AddResponse` _ns1__AddResponse response; // 4. 设置服务端点地址(如果WSDL里的地址不对或需要覆盖) // calculator.soap_endpoint = "http://your-actual-service-url/Calculator.asmx"; // 5. 发起远程调用! int soap_result = calculator.Add(&request, response); // 6. 检查调用结果 if (soap_result == SOAP_OK) { // 调用成功,打印结果 std::cout << “调用成功!结果:” << response.AddResult << std::endl; } else { // 调用失败,打印错误信息 std::cerr << “WebService调用失败!” << std::endl; // 可以打印更详细的错误信息 calculator.soap_stream_fault(std::cerr); } // 7. 清理资源(代理类的析构函数通常会做,但显式调用destroy可以确保) calculator.destroy(); return 0; }这段代码的逻辑非常直观:
- 实例化代理:
CalculatorSoapProxy是soapcpp2根据WSDL生成的类,它封装了SOAP通信细节。 - 填充请求:
_ns1__Add是生成的请求结构体,其成员a和b对应WSDL中Add方法的两个参数。你需要根据生成的Calculator.h文件来确定确切的类型和成员名。 - 声明响应:
_ns1__AddResponse是生成的响应结构体,通常包含一个以方法名+Result命名的成员(如AddResult)来存放返回值。 - (可选)覆盖端点:如果服务的实际地址与WSDL中描述的不同,可以通过设置
proxy.soap_endpoint来覆盖。 - 发起调用:调用代理类的方法(这里是
Add),传入请求和响应对象的指针(或引用,具体看生成代码的签名)。返回值soap_result是一个整数,SOAP_OK(通常是0)表示成功。 - 处理结果:成功则从响应对象中取出结果;失败则通过
soap_stream_fault输出错误详情到标准错误流。 - 清理:调用
destroy()释放SOAP引擎内部资源。
5.2 处理复杂类型与数组
实际服务中,参数和返回值可能不是简单的int、double或string,而是复杂的结构体甚至数组。gSOAP同样能很好地处理。
例如,如果WSDL定义了一个Person类型,包含name(字符串)和age(整数),那么wsdl2h会生成类似下面的结构体:
class ns1__Person { public: std::string name; int age; };如果GetPersons方法返回一个Person数组,那么响应结构体中可能会有一个std::vector<ns1__Person*>或ns1__ArrayOfPerson类型的成员。gSOAP生成的代码会自动处理这些复杂类型的序列化和反序列化,你在C++代码中直接使用STL容器(如std::vector)即可。
注意事项:内存管理。当生成代码中使用指针(特别是
std::vector<ns1__Person*>)时,需要留意内存的分配和释放。gSOAP运行时库通常会在序列化/反序列化过程中管理这些内存。但如果你自己构造一个复杂的请求对象树,最好使用gSOAP提供的soap_malloc函数来分配内存,这样整个内存池可以由一个soap上下文统一管理,避免内存泄漏。简单来说,对于传入代理方法的请求/响应对象,除非文档特别说明,否则一般不需要手动delete。
6. 编译、链接与调试
代码写好了,最后一步是让它在VS2015里成功跑起来。
6.1 解决编译与链接错误
即使前面步骤都正确,第一次编译仍可能遇到问题。以下是几个常见错误及解决方法:
错误 LNK2001: 无法解析的外部符号
namespaces: 这是最经典的错误,意味着CalculatorSoap.nsmap文件没有被正确包含。确保在你的一个且仅一个.cpp文件(通常是main.cpp)中,#include了CalculatorSoap.nsmap。这个文件定义了SOAP信封所需的XML命名空间,链接器需要它。错误 C2039: “string”: 不是“std”的成员或类似STL错误: 这通常是因为gSOAP生成的头文件与你的编译器设置不兼容。检查:
- 项目属性 -> C/C++ -> 语言 -> 符合模式,如果设置为“是”,尝试改为“否”。gSOAP生成的代码有时不完全符合严格的C++标准。
- 确保在
wsdl2h生成头文件时,没有使用-s参数(如果你希望使用STL)。如果使用了-s,生成的头文件会用std::string,但需要确保你的项目设置了支持C++标准库。 - 在
stdafx.h(如果你用了预编译头)或项目属性 -> C/C++ -> 预处理器 -> 预处理器定义中,添加_STLPORT_VERSION或WITH_STL等宏,具体取决于gSOAP版本和配置。一个更简单粗暴但有效的方法是:直接编辑生成的soapStub.h或Calculator.h,在开头显式地#include <string>和<vector>。
错误 LNK2019: 无法解析的外部符号
soap_serve等: 如果你只做客户端,但在链接时包含了服务端的代码(比如没有用-C参数生成纯客户端代码),或者错误地链接了服务端库。确保soapcpp2使用了-C参数,并且项目中没有包含soapServer.cpp、soapService.cpp等文件。运行时崩溃在
soap_connect或soap_call:- 网络问题:首先检查服务地址
soap_endpoint是否正确,网络是否通畅。可以用浏览器或Postman先测试一下服务是否可用。 - SSL/TLS问题:如果服务是HTTPS的,你需要确保:
- 链接了
libgsoapssl.lib。 - 在代码中初始化SSL上下文:
#include “openssl/ssl.h”并在调用前执行soap_ssl_init();。更简单的做法是,使用gSOAP提供的soap_ssl_client_context函数来设置代理对象的SSL选项。 - 将OpenSSL的DLL(
libssl-1_1-x64.dll,libcrypto-1_1-x64.dll)放到可执行文件同级目录或系统路径。
- 链接了
- 内存损坏:检查是否有数组越界、使用未初始化的指针等问题。使用VS2015的调试器,在崩溃时查看调用堆栈和变量值。
- 网络问题:首先检查服务地址
6.2 调试技巧
启用gSOAP日志:gSOAP提供了强大的日志功能,可以打印出收发的原始SOAP XML消息,这对调试协议问题至关重要。在调用服务前,设置以下代码:
calculator.soap_set_recv_logfile(&calculator, stdout); // 记录接收到的消息 calculator.soap_set_sent_logfile(&calculator, stdout); // 记录发送的消息 calculator.soap_set_test_logfile(&calculator, stdout); // 记录测试日志运行程序,你会在控制台看到完整的SOAP请求和响应信封,可以直观地检查XML结构、命名空间、参数值是否正确。
使用SoapUI进行独立测试:在编写C++客户端之前或遇到问题时,强烈建议使用SoapUI(一个专业的WebService测试工具)加载WSDL,创建测试请求并发送。这可以帮你快速验证:1) WSDL本身是否有效;2) 服务端点是否可达;3) 预期的请求/响应格式是怎样的。用SoapUI成功调用后,再对照其生成的SOAP消息来调整你的C++代码,事半功倍。
7. 进阶:创建WebService服务端
标题中也有“创建”的需求。使用gSOAP创建服务端,流程是镜像的。
- 生成服务端代码:运行
soapcpp2时,不要使用-C参数。例如:
注意soapcpp2 -i -S -I"D:\Libraries\gsoap_2.8.134\import" Calculator.h-S参数表示生成服务端代码(与-C相对)。这会额外生成soapServer.cpp、soapService.cpp等文件。 - 实现服务逻辑:生成的文件中会包含一个服务类(例如
CalculatorSoapService)的骨架,其中的方法(如Add)是虚函数或空实现。你需要创建一个新的类继承它,并重写这些方法,在里面实现具体的计算逻辑。 - 编写服务主程序:在主程序中,创建你实现的服务类实例,然后调用
soap_bind,soap_accept,soap_serve等函数来启动一个SOAP over HTTP的监听服务。gSOAP的示例代码($GSOAP_ROOT\samples目录下)提供了完整的服务端模板。 - 编译与运行:将服务端相关的生成文件(
soapServer.cpp,soapService.cpp, 你的实现类.cpp)添加到新项目中,并链接libgsoap++.lib等库。编译运行后,一个简单的WebService服务端就在指定端口(如8080)上运行起来了。
服务端的配置和错误处理比客户端更复杂,涉及多线程、IO模型、错误恢复等,但核心原理与客户端一致:gSOAP帮你处理了SOAP协议解析和封装,你只需关注业务逻辑。
8. 常见问题与排查实录
即使按照教程一步步来,实际项目中还是会遇到各种稀奇古怪的问题。这里记录几个我踩过的坑和解决方案:
问题:生成的代码编译报错,提示“
soap未定义的标识符”或“soap_context相关错误”。- 排查:检查是否在所有包含gSOAP生成的头文件(如
soapH.h)的源文件中,最早包含了soapStub.h。正确的顺序是:#include “soapStub.h”必须在其他gSOAP头文件之前。因为soapStub.h定义了关键的SOAP_STD_INIT宏和soap结构体。 - 解决:在你的
main.cpp或实现文件中,确保头文件包含顺序如下:#include “soapStub.h” // 必须第一! #include “soapCalculatorSoapProxy.h” #include “CalculatorSoap.nsmap”
- 排查:检查是否在所有包含gSOAP生成的头文件(如
问题:调用成功,但返回的数据是乱码或总是默认值(如0)。
- 排查:启用gSOAP日志,查看服务器返回的SOAP响应体。很可能XML中的字段名或命名空间与gSOAP生成的代码期望的不匹配。
- 解决:仔细对比WSDL和生成的
Calculator.h文件。使用wsdl2h时,可以尝试不同的选项来影响命名空间和名称的生成,例如-qname(限定名)或-c(生成纯C代码)。有时服务端实现的SOAP消息并不完全符合WSDL标准,可能需要手动调整生成的头文件,或者使用gSOAP的插件机制进行定制化映射。
问题:在Windows 10/11上,控制台程序一闪而过,看不到输出。
- 解决:这不是gSOAP的问题,是控制台程序的通用问题。在
main函数末尾,return 0;之前,加上system(“pause”);(需要#include <cstdlib>)。或者在VS2015中,按Ctrl+F5(开始执行不调试)运行程序,而不是F5。
- 解决:这不是gSOAP的问题,是控制台程序的通用问题。在
问题:如何设置HTTP超时、代理或自定义HTTP头?
- 解决:gSOAP的代理对象内部有一个
soap结构体(可以通过calculator.soap访问),它控制着所有底层设置。- 超时:
calculator.soap.send_timeout = 10; // 发送超时10秒calculator.soap.recv_timeout = 10; // 接收超时10秒 - 代理:
calculator.soap.proxy_host = “proxy.mycompany.com”;calculator.soap.proxy_port = 8080; - 自定义HTTP头:可以使用
soap_header函数,或直接操作calculator.soap.header(一个SOAP_ENV__Header结构体指针)来添加SOAP头信息。对于非SOAP的标准HTTP头,可以通过soap_set_http_header函数设置。
- 超时:
- 解决:gSOAP的代理对象内部有一个
最后,我想说的是,在VS2015的C++环境里集成WebService,尤其是用gSOAP,初看步骤繁多,像在组装一台精密仪器。但一旦你把工具链配置好、生成流程跑通,后面就是纯粹的C++业务编码了。这份“笨重”换来的,是对SOAP协议最彻底的控制和跨平台的潜力。当你看到那个简单的calculator.Add(&request, response)调用成功,并返回正确结果时,你会觉得之前所有的折腾都是值得的。这个过程,本身就是对“底层”、“协议”、“互操作性”这些概念的一次深刻实践。