ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Windows-classic-samples 源码实战:Windows Web Services 自定义编码通道的 HTTP 计算器服务代理客户端

Windows-classic-samples 源码实战:Windows Web Services 自定义编码通道的 HTTP 计算器服务代理客户端
  • 示例工程

【免费下载链接】Windows-classic-samples

This repo contains samples that demonstrate the API used in Windows classic desktop applications.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载

导读

本文围绕 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 调用:

  1. CreateEncoder:分配并初始化编码器上下文。示例中Encoder结构体内含 1024 字节的内部缓冲区、原始 Content-Type 缓存(256 个 WCHAR)以及底层WS_WRITE_CALLBACK(用于把最终字节写回 HTTP 传输层)。分配失败返回E_OUTOFMEMORY。
  2. EncoderGetContentType:WWS 告知将要发送的真实 Content-Type(如text/xml),示例将其原样拷贝保存,然后向上层返回一个替换后的 Content-Type——L"myContentType",并把contentEncoding置空。这一步的作用是:让通道的后续处理(如 HTTP 头)看到自定义类型,而真实类型信息被完整保留在帧内。
  3. EncoderStart:先写入 4 字节(ULONG)的 Content-Type 长度,再写入 Content-Type 的 WCHAR 序列(EncodedChannel.cpp 第 79-101 行),形成自定义帧头。
  4. EncoderEncode:对每个待发送字节执行+1变换。实现按 1024 字节分块处理:当bufferLength > encoder->bufferLength时,先填满整块缓冲、逐字节加一并写回,再处理剩余部分;不足一块的尾部同样逐字节加一后写回(第 103-136 行)。外层回调还会遍历传入的WS_BYTES数组逐段编码(第 138-158 行)。
  5. EncoderEnd:空操作返回S_OK,表示编码结束。
  6. FreeEncoder:释放编码器上下文。从源码看,该函数仅delete encoder释放Encoder结构本身,内部缓冲区未单独释放(示例代码以教学演示为主)。

解码端(接收)生命周期

WS_CHANNEL_DECODER的回调与之对称:

  1. CreateDecoder:分配Decoder结构并保存底层WS_READ_CALLBACK。
  2. DecoderGetContentType:对收到的 Content-Type 做严格校验——若存在contentEncoding或 Content-Type 与L"myContentType"不一致,一律返回E_FAIL;校验通过后返回帧头中保存的原始 Content-Type(第 258-283 行)。
  3. DecoderStart:通过ReadBlock工具函数先读取 4 字节长度,再按该长度读取完整 Content-Type(第 285-310 行)。ReadBlock(第 197-235 行)循环调用底层读回调直至读满所需字节数或到达流末尾,可选的outLength参数用于容忍不足一帧的尾部数据。
  4. DecoderDecode:读取一块数据后对每个字节执行-1变换,还原发送端+1前的原始载荷(第 312-336 行)。
  5. DecoderEnd:空操作返回S_OK。
  6. 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.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载
上一篇:wp-calypso 的 @automattic/launchpad 包:WordPress.com 启动清单组件的演进与实战指南
下一篇:AutoplayVideos 开源项目教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进