- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
导读
本文围绕 Windows-classic-samples 仓库中的Samples/WebServices/HttpCalculatorWithEncodedChannelClient示例,深入讲解如何基于 Windows Web Services API(WWS)编写一个使用自定义编码通道的 HTTP 服务代理客户端:客户端通过一个字节级自定义加密/转换协议与计算器服务通信,而底层 SOAP/XML 消息的编码与解码完全由开发者自己实现。读完本文,你将掌握WS_CHANNEL_ENCODER/WS_CHANNEL_DECODER回调契约的完整生命周期、通道属性在WsCreateServiceProxy中的挂载方式,以及如何配合Wsutil.exe从 WSDL 生成并调用服务契约方法,并能在本地用配套服务端示例完整跑通1 + 2 = 3的调用链路。
示例概览:服务代理客户端 + 自定义编码通道
该示例的核心目标(见 README.md)是:使用 Windows Web Services API 的service proxy(服务代理)与一个基于 HTTP 的计算器服务通信,且通信通道使用自定义编码器(custom encoder)。也就是说,示例不满足于 WWS 内置的文本/二进制编码,而是演示如何接管消息的编码与解码过程,实现自定义的线上数据格式。
它与仓库中成对的 HttpCalculatorWithEncodedChannelService 示例共同构成完整的客户端-服务端演示:服务端使用WS_CHANNEL_PROPERTY_DECODER属性挂载同一个自定义解码器,客户端使用WS_CHANNEL_PROPERTY_ENCODER属性挂载自定义编码器,两端通过同一套自定义协议互通。
关联 API 一览
README 中列出的核心 API 与使用位置如下:
| API | 作用 | 实际使用位置 |
|---|---|---|
WsCreateError | 创建保存丰富错误信息的错误对象 | 客户端主程序 |
WsCreateHeap | 创建堆,用于存放反序列化后的数据 | 客户端主程序 |
WsCreateServiceProxy | 创建服务代理 | 客户端主程序 |
WsOpenServiceProxy | 打开服务代理并建立与端点的连接 | 客户端主程序 |
WsCloseServiceProxy | 关闭服务代理 | 客户端主程序 |
WsFreeServiceProxy | 释放服务代理 | 客户端主程序 |
WsFreeHeap | 释放堆 | 客户端主程序 |
WsFreeError | 释放错误对象 | 客户端主程序 |
WsGetErrorProperty | 查询错误对象属性(如字符串数量) | 客户端错误打印 |
WsGetErrorString | 按索引获取错误字符串 | 客户端错误打印 |
WsCreateServiceHost/WsOpenServiceHost | 创建/启动服务宿主 | 配套服务端示例 |
系统与工具要求
README 明确给出的运行环境要求为:客户端 Windows 7,服务端 Windows Server 2008 R2。示例源码按 Windows 8.1 / Windows Server 2012 R2 + Visual Studio 2013 编写,通常无需改动即可在更高版本 Visual Studio 与 Windows 上编译运行。
工程结构与生成物
示例目录结构如下:
Samples/WebServices/HttpCalculatorWithEncodedChannelClient/ ├── README.md # 官方说明 ├── readme.txt # 更详细的构建/运行/URL 预留说明 └── cpp/ ├── HttpCalculatorWithEncodedChannelClient.cpp # 客户端主程序 ├── EncodedChannel.cpp # 自定义编码器与解码器实现 ├── EncodedChannel.h # 导出 channelEncoder / channelDecoder 两个全局对象 ├── CalculatorService.wsdl # 服务契约描述 ├── HttpCalculatorWithEncodedChannelClient.sln ├── HttpCalculatorWithEncodedChannelClient.vcxproj └── HttpCalculatorWithEncodedChannelClient.vcxproj.filters值得注意的构建细节位于 HttpCalculatorWithEncodedChannelClient.vcxproj 的CustomBuild节点:WSDL 文件通过Wsutil.exe在编译期自动生成 C 代理代码与头文件:
Wsutil.exe /wsdl:CalculatorService.wsdl /out:.生成产物为CalculatorService.wsdl.c与CalculatorService.wsdl.h(vcxproj 中同时把它们列入编译/包含项)。其中CalculatorService.wsdl.h提供了DefaultBinding_ICalculator_Add等代理函数声明,CalculatorService.wsdl.c提供其实现。链接器依赖包括webservices.lib;crypt32.lib;rpcrt4.lib;Iphlpapi.lib,子系统为 Console,工程同时配置了 Win32 / ARM / x64 三种平台的 Debug 与 Release 构建。
自定义通道的编码协议:Encoder 与 Decoder 成对实现
Windows Web Services 允许为通道注入自定义编码器/解码器。示例将两者的实现放在 EncodedChannel.cpp 中,并分别在文件底部导出两个全局对象:
WS_CHANNEL_ENCODER channelEncoder = { NULL, // createContext CreateEncoder, // 创建编码器实例 EncoderGetContentType, EncoderStart, EncoderEncode, EncoderEnd, FreeEncoder }; WS_CHANNEL_DECODER channelDecoder = { NULL, CreateDecoder, DecoderGetContentType, DecoderStart, DecoderDecode, DecoderEnd, FreeDecoder };EncodedChannel.h 仅以extern形式声明这两个对象,供客户端与服务端共同引用。
通道属性挂载点
客户端在创建服务代理前,先构造一个通道属性数组,将编码器对象以WS_CHANNEL_PROPERTY_ENCODER属性挂到通道上(HttpCalculatorWithEncodedChannelClient.cpp 第 95-98 行):
WS_CHANNEL_PROPERTY channelPropertyArray[1]; channelPropertyArray[0].id = WS_CHANNEL_PROPERTY_ENCODER; channelPropertyArray[0].value = &channelEncoder; channelPropertyArray[0].valueSize = sizeof(channelEncoder);服务端则在 HttpCalculatorWithEncodedChannelService.cpp 中以对称方式挂载WS_CHANNEL_PROPERTY_DECODER与channelDecoder。由此可以确认:编码器与解码器必须成对使用,线上的数据格式完全由这两组回调决定。
编码端(发送)生命周期
WS_CHANNEL_ENCODER的回调按如下顺序被 WWS 调用:
- CreateEncoder:分配并初始化编码器上下文。示例中
Encoder结构体内含 1024 字节的内部缓冲区、原始 Content-Type 缓存(256 个 WCHAR)以及底层WS_WRITE_CALLBACK(用于把最终字节写回 HTTP 传输层)。分配失败返回E_OUTOFMEMORY。 - EncoderGetContentType:WWS 告知将要发送的真实 Content-Type(如
text/xml),示例将其原样拷贝保存,然后向上层返回一个替换后的 Content-Type——L"myContentType",并把contentEncoding置空。这一步的作用是:让通道的后续处理(如 HTTP 头)看到自定义类型,而真实类型信息被完整保留在帧内。 - EncoderStart:先写入 4 字节(
ULONG)的 Content-Type 长度,再写入 Content-Type 的 WCHAR 序列(EncodedChannel.cpp 第 79-101 行),形成自定义帧头。 - EncoderEncode:对每个待发送字节执行
+1变换。实现按 1024 字节分块处理:当bufferLength > encoder->bufferLength时,先填满整块缓冲、逐字节加一并写回,再处理剩余部分;不足一块的尾部同样逐字节加一后写回(第 103-136 行)。外层回调还会遍历传入的WS_BYTES数组逐段编码(第 138-158 行)。 - EncoderEnd:空操作返回
S_OK,表示编码结束。 - FreeEncoder:释放编码器上下文。从源码看,该函数仅
delete encoder释放Encoder结构本身,内部缓冲区未单独释放(示例代码以教学演示为主)。
解码端(接收)生命周期
WS_CHANNEL_DECODER的回调与之对称:
- CreateDecoder:分配
Decoder结构并保存底层WS_READ_CALLBACK。 - DecoderGetContentType:对收到的 Content-Type 做严格校验——若存在
contentEncoding或 Content-Type 与L"myContentType"不一致,一律返回E_FAIL;校验通过后返回帧头中保存的原始 Content-Type(第 258-283 行)。 - DecoderStart:通过
ReadBlock工具函数先读取 4 字节长度,再按该长度读取完整 Content-Type(第 285-310 行)。ReadBlock(第 197-235 行)循环调用底层读回调直至读满所需字节数或到达流末尾,可选的outLength参数用于容忍不足一帧的尾部数据。 - DecoderDecode:读取一块数据后对每个字节执行
-1变换,还原发送端+1前的原始载荷(第 312-336 行)。 - DecoderEnd:空操作返回
S_OK。 - FreeDecoder:释放解码器上下文。
自定义传输协议格式
综合编码/解码两端实现,可以归纳出该示例定义的线上帧格式:
[4 字节 ULONG:原始 Content-Type 长度] [原始 Content-Type 的 WCHAR 序列] [载荷:每个字节 +1 后的 SOAP/XML 消息]Content-Type 在 HTTP 层显示为自定义的myContentType,而真实的媒体类型被放进帧头,由对端解码器解析并恢复。发送方向逐字节加一、接收方向逐字节减一的变换,即为本示例演示的“自定义编码”语义——实际业务中可以替换为任意加密、压缩或专用序列化逻辑,通道框架本身对此完全透明。
客户端主流程逐步解析
HttpCalculatorWithEncodedChannelClient.cpp 的wmain完整展示了服务代理的标准用法,可分为六个阶段。
1. 创建错误对象
hr = WsCreateError(NULL, 0, &error);错误对象用于收集调用失败时的丰富诊断信息,后续交给PrintError打印。
2. 创建堆
hr = WsCreateHeap( /*maxSize*/ 2048, /*trimSize*/ 512, NULL, 0, &heap, error);WsCreateHeap指定堆的最大容量(2048 字节)与收缩阈值(512 字节),该堆用于存放反序列化得到的数据(例如Add的返回值)。调用结束后由WsFreeHeap释放。
3. 配置通道属性并创建服务代理
hr = WsCreateServiceProxy( WS_CHANNEL_TYPE_REQUEST, // 请求-应答式通道 WS_HTTP_CHANNEL_BINDING, // HTTP 传输绑定 NULL, // 代理属性 NULL, // 安全描述 0, channelPropertyArray, // 通道属性:挂载自定义编码器 WsCountOf(channelPropertyArray), &proxy, error);通道类型为WS_CHANNEL_TYPE_REQUEST,绑定为WS_HTTP_CHANNEL_BINDING;安全描述与代理属性均不指定,重点是把第 95-98 行构造的channelPropertyArray(内含WS_CHANNEL_PROPERTY_ENCODER)传入。
4. 打开服务代理
WS_ENDPOINT_ADDRESS address = {}; static const WS_STRING url = WS_STRING_VALUE(L"http://localhost/example"); address.url = url; hr = WsOpenServiceProxy(proxy, &address, NULL, error);端点地址指向http://localhost/example——与服务端示例监听的http://+:80/example相对应。
5. 调用 WSDL 生成的契约方法
hr = DefaultBinding_ICalculator_Add( proxy, 1, 2, &result, heap, NULL, 0, NULL, error); wprintf(L"%d + %d = %d\n", 1, 2, result);DefaultBinding_ICalculator_Add由Wsutil.exe从 CalculatorService.wsdl 生成,对应 WSDL 中ICalculator端口类型的Add操作(输入a、b两个xsd:int,输出result)。参数1、2会经过序列化→自定义编码→HTTP 传输,服务端计算后经解码→反序列化返回,最终打印1 + 2 = 3。
6. 清理与错误打印
失败时统一跳转到Exit标签:若proxy已创建,则依次调用WsCloseServiceProxy、WsFreeServiceProxy;随后释放堆与错误对象。PrintError会先输出十六进制错误码,并对E_INVALIDARG、WS_E_INVALID_OPERATION给出“API 使用不当,极可能是程序 bug”的提示,再通过WsGetErrorProperty取错误字符串数量、WsGetErrorString逐个打印富错误信息。
对称的服务端:HttpCalculatorWithEncodedChannelService
客户端能跑通的前提是配套服务端存在。仓库中的 HttpCalculatorWithEncodedChannelService 演示了服务宿主侧对称的用法,关键差异点(见 HttpCalculatorWithEncodedChannelService.cpp):
- 通道属性挂载
WS_CHANNEL_PROPERTY_DECODER与channelDecoder(第 141-146 行); - 服务端点监听
http://+:80/example,通道类型为WS_CHANNEL_TYPE_REPLY,绑定WS_HTTP_CHANNEL_BINDING(第 158-166 行); - 通过
WS_SERVICE_CONTRACT将生成的CalculatorService_wsdl.contracts.DefaultBinding_ICalculator与{Add, Subtract}函数表绑定(第 116-124 行),Add/Subtract两个回调分别实现加法和减法并在控制台打印; - 注册
WS_SERVICE_ENDPOINT_PROPERTY_CLOSE_CHANNEL_CALLBACK,在通道关闭时SetEvent(closeServer)唤醒主线程; - 主线程
WsCreateServiceHost→WsOpenServiceHost启动监听后,WaitForSingleObject(closeServer, INFINITE)等待退出事件,最后WsCloseServiceHost收尾。
注意:客户端与服务端目录下各有一份内容一致的EncodedChannel.cpp(两份文件逐字节相同),客户端只用到其中的channelEncoder,服务端只用到channelDecoder,两个通道对象在单侧都完整编译,便于两侧独立构建。
构建与运行
构建
按 README 的说明:用 Visual Studio 2013 for Windows 8.1(任意 SKU)或更高版本打开cpp目录下的解决方案文件 HttpCalculatorWithEncodedChannelClient.sln,按 F7(Visual Studio 2013 为 F6)或通过菜单 Build → Build Solution 编译,产物输出到默认的\Debug或Release目录。编译过程中Wsutil.exe会自动生成 WSDL 对应的.c/.h文件。
运行
构建成功后按 F5(带调试)或 Ctrl-F5(不带调试)运行。由于客户端向http://localhost/example发起请求,需先启动配套服务端示例;服务端监听http://+:80/example并打印每次调用的算式。客户端预期输出:
1 + 2 = 3预留 HTTP URL(netsh urlacl)
readme.txt 特别说明:若要在非提升权限的 Visual Studio 中运行 HTTP 服务端示例,必须先以管理员身份预留 URL。在提升权限的 CMD 中执行:
netsh http add urlacl url=http://+:80/example user=%USERDOMAIN%\%USERNAME% netsh http add urlacl url=https://+:443/example user=%USERDOMAIN%\%USERNAME%使用提升权限的 PowerShell 时则写为:
netsh http add urlacl url=http://+:80/example user=$env:USERDOMAIN\$env:USERNAME netsh http add urlacl url=https://+:443/example user=$env:USERDOMAIN\$env:USERNAME删除预留(同样需提升权限的 CMD 或 PowerShell):
netsh http delete urlacl url=http://+:80/example netsh http delete urlacl url=https://+:443/example旧系统前置条件
readme.txt同时指出:在 Windows XP、Windows Vista、Windows Server 2003 与 Windows Server 2008 上运行本示例,可能需要先安装包含 Windows Web Services API 运行时 DLL 的 Windows Update,具体以 MSDN 文档为准。另外,示例仅为教学演示用途,未经生产环境测试,不应不加修改地直接用于生产。
从源码延伸的验证与二次开发要点
- 验证自定义协议生效:可对照客户端 HttpCalculatorWithEncodedChannelClient.cpp 与编码器 EncodedChannel.cpp 中的
EncoderGetContentType/DecoderGetContentType逻辑:线上 Content-Type 恒为myContentType,若某端收到不匹配的类型或附带contentEncoding,解码会返回E_FAIL——这是协议一致性的第一道防线。 - 替换自定义编码算法:编码端
EncoderEncode的逐字节+1与解码端DecoderDecode的逐字节-1是成对变换,可在自己的项目中替换为压缩、AES 加密等任意可逆算法,通道生命周期与回调签名无需变化;但务必保证两端的帧头格式(长度字段 + Content-Type)一致,否则DecoderStart阶段即会失败。 - 错误排查切入点:
E_INVALIDARG与WS_E_INVALID_OPERATION在本示例中被PrintError明确标注为 API 误用类错误;而自定义编码相关的失败更可能表现为DecoderGetContentType/DecoderStart返回的E_FAIL,排查时应优先核对端点地址(http://localhost/example与http://+:80/example的对应关系)、URL 预留是否完成、以及服务端是否已启动监听。
小结
HttpCalculatorWithEncodedChannelClient是理解 Windows Web Services 自定义通道机制的最小完整范例:它把“通道自定义编码”这一框架级能力拆解为WS_CHANNEL_ENCODER/WS_CHANNEL_DECODER各 6 个回调的成对实现,并通过 WSDL 工具链把代理调用简化为一句话。掌握它之后,无论是接入私有协议、叠加传输层加密,还是复刻类似的 HTTP 计算器演示,都能以这套“帧头 + 变换算法 + 生命周期回调”的模式为蓝本快速落地。
- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
相关推荐
如何在CI中运行lint-staged:--diff与--all参数实现Pull Request变更精准检查
如何在CI中运行lint staged: diff与 all参数实现Pull Request变更精准检查 在持续集成(CI)流水线中运行 lint staged
示例工程Windows Web Services 分层通道(Layered Channel)实战:用自定义通道实现 HTTP 计算器服务客户端
Windows Web Services 分层通道(Layered Channel)实战:用自定义通道实现 HTTP 计算器服务客户端 本篇技术指南以 Wind
示例工程Windows-classic-samples 实战:用 Windows Web Services API 服务代理构建 HTTP 采购订单客户端
Windows classic samples 实战:用 Windows Web Services API 服务代理构建 HTTP 采购订单客户端 本文以 Sa
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考