☰
VC++通过COM组件调用WebService实现短信发送的完整指南
2026/10/19 4:45:40 网站建设 项目流程

简介:这份资源面向需要在 VC++ 中集成远程服务的开发者,聚焦通过 COM 接口与 SOAP 协议调用 Web Service 的完整实现思路。内容围绕 ISoapConnector、ISoapSerializer、ISoapReader 等核心组件展开,涵盖创建 HttpConnector30 连接器、设置 EndPointURL 与 SoapAction、构建 SOAP 信封与消息体、传递 cmpcode、phone、content 等参数,以及读取 RpcResult 响应的关键环节,适合具备一定 C++ 与 COM 基础、希望打通本地程序与远程接口交互的中级开发者参考。资源包共 1 个 doc 文档,约 28KB,以代码示例与流程说明为主,便于对照理解调用链路。目前已有 179 人学习,可作为 VC++ 调用 Web Service 的入门范例与排错参考。

1. VC调用WebService:从COM组件到SOAP消息的完整落地路径

接手一个老工控上位机项目时,我遇到一个需求:设备管理软件需要在报警触发时给值班人员发短信。软件是VC6时代的MFC框架,没有HTTP库,没有JSON解析器,连CString都是上古版本。但短信网关只暴露了一个WebService接口,WSDL地址挂在远端服务器上。当时第一反应是用WinHTTP手搓SOAP报文,但XML拼装和命名空间处理太容易翻车,后来发现系统里已经注册了SOAP Toolkit的COM组件,直接调用HttpConnector30和SoapSerializer30就能完成整个调用链路。这份资源就是围绕这个场景展开的——在VC++环境下,通过COM接口调用远程WebService,完成短信发送。适合正在维护MFC老项目、需要对接SOAP接口但不想引入重型依赖的开发者。核心思路是:用ISoapConnector建立连接,用ISoapSerializer构造SOAP信封,用ISoapReader解析响应,整条链路不依赖任何第三方库,纯COM调用。

2. 核心模块拆解:连接器、序列化器与读取器的协作链路

2.1 三个COM对象的职责划分

整个调用链路围绕三个核心COM对象展开,它们各司其职,串起来就是一次完整的SOAP请求-响应周期。

第一个是ISoapConnectorPtr,负责传输层。它封装了HTTP协议细节,你只需要告诉它目标地址和SOAPAction,它就能把序列化好的消息发出去,再把响应流拿回来。代码里用的是HttpConnector30,这是SOAP Toolkit 3.0提供的HTTP传输实现。

第二个是ISoapSerializerPtr,负责消息构造。SOAP消息本质是一段特定结构的XML,手写字符串拼接容易在命名空间、编码格式上出问题。序列化器提供StartEnvelope、StartBody、StartElement、WriteString、EndElement这一套方法,按顺序调用就能生成合法的SOAP信封。

第三个是ISoapReaderPtr,负责响应解析。服务端返回的也是SOAP格式的XML,读取器把它加载后,可以通过RpcResult属性直接拿到方法返回值,省去手动解析XML的麻烦。

这三个对象的创建都通过CreateInstance完成,传入对应的__uuidof即可。需要注意的是,它们都是智能指针类型,声明时用Ptr后缀,不需要手动Release。

// 三个核心COM对象的声明与创建 ISoapConnectorPtr SoapConnector; // 传输层:负责HTTP连接与消息收发 ISoapSerializerPtr Serializer; // 序列化层:负责构造SOAP请求消息 ISoapReaderPtr Reader; // 反序列化层:负责解析SOAP响应消息 // 创建HttpConnector30实例,这是SOAP Toolkit提供的HTTP传输组件 SoapConnector.CreateInstance(__uuidof(HttpConnector30)); // 创建SoapSerializer30实例,用于构造SOAP消息体 Serializer.CreateInstance(__uuidof(SoapSerializer30)); // 创建SoapReader30实例,用于读取服务端返回结果 Reader.CreateInstance(__uuidof(SoapReader30));

这里有个细节值得注意:CreateInstance内部调用的是COM的CoCreateInstance,所以调用线程必须已经初始化COM库。在MFC对话框应用中,通常在InitInstance里调用AfxOleInit()就能完成初始化。如果忘了这一步,CreateInstance会直接抛异常,而且异常信息不太直观,容易让人以为是组件没注册。

2.2 连接建立与EndPointURL的配置

连接建立分两步:先设置EndPointURL属性,再调用Connect()方法。EndPointURL就是WebService的访问地址,通常以?wsdl结尾。但这里有个容易混淆的点:?wsdl是给开发工具看服务描述的,实际调用时服务端并不需要这个后缀。不过SOAP Toolkit的HttpConnector30会自动处理,你写完整的WSDL地址它也能正确POST到服务端点。

// 设置WebService的终结点地址 SoapConnector->Property["EndPointURL"] = "http://服务端地址/cif/services/SMS?wsdl"; // 发起连接,返回HRESULT用于判断成功与否 HRESULT hr = SoapConnector->Connect(); if (FAILED(hr)) { return _T(""); // 连接失败直接返回空字符串 }

Connect()返回的HRESULT需要判断。常见失败原因有三种:网络不通导致连接超时、服务端地址写错返回404、服务端要求认证但未提供凭据。如果是超时,可以调整Property["Timeout"]的值,单位是毫秒,默认值偏短,跨网段调用时建议设到10000以上。

2.3 SoapAction的拼接规则

SoapAction是HTTP头中的一个字段,用来告诉服务端要调用哪个方法。它的值通常是命名空间URI加上方法名,中间用斜杠或直接拼接。代码里的写法是szNameSpace + _bstr_t("send"),其中szNameSpace是"http://服务端地址/cif/services/SMS/",拼出来就是"http://服务端地址/cif/services/SMS/send"。

这里有个血泪经验:SoapAction的值必须和服务端WSDL里定义的一模一样,多一个斜杠少一个斜杠都会导致服务端返回"方法未找到"。有些服务端框架对大小写也敏感,send和Send可能被当成两个不同的方法。最稳妥的做法是打开WSDL文件,搜索soap:operation标签,把soapAction属性的值原样复制过来。

// 命名空间URI,通常与WSDL中targetNamespace一致 char *szNameSpace = "http://服务端地址/cif/services/SMS/"; // 拼接SoapAction:命名空间 + 方法名 SoapConnector->Property["SoapAction"] = szNameSpace + _bstr_t("send");

_bstr_t是COM的字符串包装类,它能自动处理char*到BSTR的转换。直接给Property赋值char*可能会因为编码问题导致乱码,用_bstr_t包一层更安全。

2.4 SOAP消息体的构造流程

消息构造是整个调用中最繁琐的部分,因为SOAP信封有严格的层级结构:Envelope > Body > 方法元素 > 参数元素。序列化器的方法调用顺序必须和这个层级一致,否则生成的XML不合法,服务端会直接返回400错误。

构造流程按以下顺序推进:

第一步,BeginMessage()通知连接器准备发送消息。这一步会在内部创建一个输入流,供序列化器写入。

第二步,Serializer->Init()把序列化器绑定到连接器的输入流上。参数是SoapConnector->InputStream,这是一个IStream接口指针,用_variant_t包装后传入。

第三步,StartEnvelope开始信封,三个参数分别是命名空间前缀、命名空间URI、编码方式。通常写L"SOAP-ENV"、空字符串、L"UTF-8"。

第四步,StartBody开始消息体,参数传空字符串即可。

第五步,StartElement开始方法元素,第一个参数是方法名"send",第二个参数是命名空间URI。这里要注意:命名空间URI必须和WSDL中定义的一致,否则服务端无法识别。

第六步,逐个构造参数元素。每个参数用StartElement开始,WriteString写入值,EndElement结束。参数顺序必须和方法签名一致。

第七步,逐层EndElement关闭方法元素,EndBody关闭消息体,EndEnvelope关闭信封。

// 通知连接器准备发送消息 SoapConnector->BeginMessage(); // 将序列化器绑定到连接器的输入流 Serializer->Init(_variant_t((IUnknown*)SoapConnector->InputStream)); // 开始SOAP信封 Serializer->StartEnvelope(L"SOAP-ENV", L"", L"UTF-8"); // 开始SOAP Body Serializer->StartBody(L""); // 开始方法元素send,指定命名空间 Serializer->StartElement(L"send", L"http://服务端地址/cif/services/SMS?wsdl", L"", L""); // 构造第一个参数cmpcode Serializer->StartElement(L"cmpcode", L"http://服务端地址/cif/services/SMS?wsdl", L"NONE", L""); Serializer->WriteString(L"0166"); // 企业代码 Serializer->EndElement(); // 构造第二个参数phone Serializer->StartElement(L"phone", L"http://服务端地址/cif/services/SMS?wsdl", L"NONE", L""); Serializer->WriteString(L"13258310354"); // 手机号码 Serializer->EndElement(); // 构造第三个参数content Serializer->StartElement(L"content", L"http://服务端地址/cif/services/SMS?wsdl", L"NONE", L""); Serializer->WriteString(L"你好"); // 短信内容 Serializer->EndElement(); // 构造第四个参数receivedate,传空值 Serializer->StartElement(L"receivedate", L"http://服务端地址/cif/services/SMS?wsdl", L"NONE", L""); Serializer->WriteString(L""); Serializer->EndElement(); // 逐层关闭元素 Serializer->EndElement(); // 关闭send Serializer->EndBody(); // 关闭Body Serializer->EndEnvelope(); // 关闭Envelope

StartElement的第三个参数是属性名,第四个参数是属性值。代码里传的是L"NONE"和空字符串,这会在XML中生成一个NONE=""的属性。有些服务端对多余属性不敏感,但严格模式下可能会报错。如果服务端没有特殊要求,这两个参数直接传空字符串更干净。

2.5 消息发送与响应读取

消息构造完成后,调用EndMessage()真正发送。这个方法会阻塞直到服务端返回或超时。返回的HRESULT同样需要判断,失败时可能是网络中断或服务端返回了非200状态码。

响应读取用SoapReader30,通过Load方法加载连接器的输出流。加载完成后,RpcResult属性指向返回值的XML节点,text字段就是文本内容。如果服务端返回的是复杂结构,RpcResult下还会有子节点,需要遍历获取。

// 发送消息并获取返回结果 hr = SoapConnector->EndMessage(); if (FAILED(hr)) { return _T(""); // 发送失败 } // 将读取器绑定到连接器的输出流 Reader->Load(_variant_t((IUnknown*)SoapConnector->OutputStream), _T("")); // 提取返回值文本 CString strResult = CString((const char *)Reader->RpcResult->text); return strResult;

RpcResult返回的是一个ISoapRpcResult接口,text属性是BSTR类型。直接强转成const char*在Unicode环境下会出问题,建议用_bstr_t中转一下,或者根据项目字符集做转换。

3. 避坑与排查:调用失败时先看这五个地方

3.1 连接返回失败但网络正常

现象是Connect()返回E_FAIL,但用浏览器能打开WSDL地址。原因通常是COM库未初始化,或者HttpConnector30组件未注册。解决方法是确认InitInstance中调用了AfxOleInit(),然后在命令行执行regsvr32 msssoap30.dll重新注册组件。如果系统是64位但项目编译为32位,还需要注意组件注册到SysWOW64目录下。

3.2 服务端返回"方法未找到"

现象是EndMessage()成功但响应内容提示方法不存在。原因基本是SoapAction或命名空间URI写错了。解决方法是打开WSDL文件,找到soap:operation标签,把soapAction属性的值完整复制到代码里。注意有些服务端的命名空间URI末尾带斜杠,有些不带,必须严格一致。

3.3 参数传递后服务端收到空值

现象是服务端日志显示参数为空。原因是StartElement的命名空间参数传错了。很多开发者以为参数元素不需要命名空间,传了空字符串,但部分服务端框架要求参数元素必须属于方法所在的命名空间。解决方法是在StartElement的第二个参数中传入和方法元素相同的命名空间URI。

3.4 响应读取时RpcResult为空指针

现象是Reader->Load成功但RpcResult为NULL。原因是服务端返回的不是RPC风格的SOAP响应,而是Document风格。SoapReader30的RpcResult只对RPC风格有效。解决方法是改用Reader->Body获取整个Body节点,然后手动遍历子节点提取返回值。

3.5 中文内容变成乱码

现象是短信内容中的中文在服务端显示为问号或乱码。原因是StartEnvelope中指定的编码是UTF-8,但WriteString传入的是本地编码的CString。解决方法是先把中文转成UTF-8再写入,或者把信封编码改为GB2312并确保服务端支持。

注意:SOAP Toolkit 3.0是较早期的组件,在Windows 10及以上系统中可能需要手动注册。如果项目允许引入第三方库,用gSOAP或直接WinHTTP手写SOAP也是可选方案,但COM方式的优势是零依赖、部署简单。

4. 二次开发改造:从硬编码到可配置的短信发送模块

原始代码把服务端地址、企业代码、手机号、短信内容全部硬编码在函数里,每次改参数都要重新编译。在实际项目中,我一般会把它改造成一个独立的短信发送类,把可变部分抽成配置项。

第一步,定义一个配置结构体,把地址、命名空间、企业代码、超时时间都放进去。这些值可以从INI文件或注册表读取,避免硬编码。

// 短信服务配置结构体 struct SmsConfig { CString strEndPoint; // WebService地址 CString strNameSpace; // 命名空间URI CString strSoapAction; // SoapAction方法名 CString strCmpCode; // 企业代码 int nTimeout; // 超时毫秒数 }; // 从INI文件加载配置 SmsConfig LoadConfig(LPCTSTR szIniPath) { SmsConfig cfg; cfg.strEndPoint = GetPrivateProfileString(_T("SMS"), _T("EndPoint"), _T(""), szIniPath); cfg.strNameSpace = GetPrivateProfileString(_T("SMS"), _T("NameSpace"), _T(""), szIniPath); cfg.strSoapAction = GetPrivateProfileString(_T("SMS"), _T("SoapAction"), _T(""), szIniPath); cfg.strCmpCode = GetPrivateProfileString(_T("SMS"), _T("CmpCode"), _T(""), szIniPath); cfg.nTimeout = GetPrivateProfileInt(_T("SMS"), _T("Timeout"), 10000, szIniPath); return cfg; }

第二步,把发送函数封装成类方法,参数只保留手机号和内容。连接器、序列化器、读取器作为成员变量,在构造函数中创建,析构函数中自动释放。

class CSmsSender { public: CSmsSender(const SmsConfig& cfg) : m_cfg(cfg) { m_pConnector.CreateInstance(__uuidof(HttpConnector30)); m_pSerializer.CreateInstance(__uuidof(SoapSerializer30)); m_pReader.CreateInstance(__uuidof(SoapReader30)); } // 发送短信,返回服务端响应文本 CString Send(LPCTSTR szPhone, LPCTSTR szContent) { try { m_pConnector->Property["EndPointURL"] = (_bstr_t)m_cfg.strEndPoint; m_pConnector->Property["Timeout"] = m_cfg.nTimeout; HRESULT hr = m_pConnector->Connect(); if (FAILED(hr)) return _T("CONNECT_FAIL"); m_pConnector->Property["SoapAction"] = (_bstr_t)(m_cfg.strNameSpace + m_cfg.strSoapAction); m_pConnector->BeginMessage(); m_pSerializer->Init(_variant_t((IUnknown*)m_pConnector->InputStream)); m_pSerializer->StartEnvelope(L"SOAP-ENV", L"", L"UTF-8"); m_pSerializer->StartBody(L""); m_pSerializer->StartElement(L"send", (_bstr_t)m_cfg.strNameSpace, L"", L""); // 企业代码 m_pSerializer->StartElement(L"cmpcode", (_bstr_t)m_cfg.strNameSpace, L"", L""); m_pSerializer->WriteString((_bstr_t)m_cfg.strCmpCode); m_pSerializer->EndElement(); // 手机号 m_pSerializer->StartElement(L"phone", (_bstr_t)m_cfg.strNameSpace, L"", L""); m_pSerializer->WriteString((_bstr_t)szPhone); m_pSerializer->EndElement(); // 短信内容 m_pSerializer->StartElement(L"content", (_bstr_t)m_cfg.strNameSpace, L"", L""); m_pSerializer->WriteString((_bstr_t)szContent); m_pSerializer->EndElement(); m_pSerializer->EndElement(); m_pSerializer->EndBody(); m_pSerializer->EndEnvelope(); hr = m_pConnector->EndMessage(); if (FAILED(hr)) return _T("SEND_FAIL"); m_pReader->Load(_variant_t((IUnknown*)m_pConnector->OutputStream), _T("")); return CString((const char*)m_pReader->RpcResult->text); } catch (_com_error& e) { return CString((char*)e.Description()); } } private: SmsConfig m_cfg; ISoapConnectorPtr m_pConnector; ISoapSerializerPtr m_pSerializer; ISoapReaderPtr m_pReader; };

第三步,调用方只需要三行代码:加载配置、创建发送器、调用Send。改造后的模块可以复用到任何需要发短信的地方,配置变更也不需要重新编译。

SmsConfig cfg = LoadConfig(_T("config.ini")); CSmsSender sender(cfg); CString strResult = sender.Send(_T("13258310354"), _T("设备报警:温度超限"));

改造过程中有个细节需要留意:_bstr_t的构造和析构有一定开销,在频繁发送的场景下,可以把配置中的字符串预先转成_bstr_t缓存起来,避免每次调用都转换。另外,如果服务端返回的响应结构比较复杂,RpcResult->text可能拿不到完整内容,这时候需要遍历RpcResult的子节点,用ChildNodes属性逐个读取。

从那以后我每次对接新的WebService接口,都强制先用SOAP UI或Postman把请求跑通,确认SoapAction、命名空间、参数顺序都正确后,再把报文结构翻译成序列化器代码。这样能避免在VC里反复编译调试,省下大量时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询