Windows Web Services API 流式 TCP 服务器实战:基于 Windows-classic-samples 的 StreamingTcpServer 深入解析
Windows Web Services API 流式 TCP 服务器实战:基于 Windows-classic-samples 的 StreamingTcpServer 深入解析
导读
本文以 Windows-classic-samples 仓库中的 StreamingTcpServer 示例 为主线,讲解如何使用 Windows Web Services API(WWSAPI)实现一个基于 TCP 的流式(streaming)服务器:它创建并打开监听器(listener)、接受客户端通道,并逐条流式读取单向消息。读完本文,你将掌握 WWSAPI 中监听器、通道、消息、堆(heap)等对象的完整生命周期,理解"应用层流式传输 + 传输层缓冲"的真实含义,并能结合源码一步步复现这一示例。文章同时给出配套的构建、运行、HTTP URL 预留等实操步骤,以及底层关键 API 的作用对照。
示例概览:它演示了什么
官方 README 对示例的描述非常凝练:它演示如何用 Windows Web Services API 实现一个 TCP 服务器,该服务器接受一个通道,并以流式方式读取单向(one-way)消息。
这里有两个必须澄清的技术细节,它们决定了这个示例的价值:
- 流式发生在应用层,而非传输层:示例只在应用层做流式处理——消息体(body)按元素逐个读入内存,读完一个就处理一个;而在传输层,数据仍是被缓冲的,不会在客户端尚未发完时就被"回流"给客户端。
- 流式模式并非所有通道类型都支持:README 明确指出,流式模式只在 HTTP 通道上受支持("The streaming mode is supported only on HTTP channels")。因此本例实际上是一个展示流式读取消息体编程模式的 TCP 示例,与客户端示例 StreamingTcpClient 构成一对完整的收发演示。
与之配套的仓库中还提供了异步版本的 AsyncStreamingTcpServer 与 AsyncStreamingTcpClient,用于对比同步流式与异步流式两种编程模型,可作为延伸阅读。
仓库结构与工程组织
示例位于 Samples/WebServices/StreamingTcpServer 目录,其 cpp 子目录包含以下文件:
| 文件 | 作用 |
|---|---|
| StreamingTcpServer.cpp | 示例唯一源文件,控制台程序入口,完整实现流式服务器 |
| PurchaseOrder.wsdl | 描述采购订单消息契约的 WSDL 文件 |
| StreamingTcpServer.vcxproj | Visual Studio 工程文件,含 WSDL 生成步骤与链接配置 |
| StreamingTcpServer.sln | 解决方案文件,支持 Debug/Release × Win32/ARM/x64 六种配置 |
| StreamingTcpServer.vcxproj.filters | 工程过滤器(源码分组) |
值得注意的是,StreamingTcpServer.cpp 通过 #include "PurchaseOrder.wsdl.h" 引入了由 WSDL 生成的头文件。这一步不是手工完成的,而是由工程中的自定义构建步骤自动完成——StreamingTcpServer.vcxproj 中声明了:
<CustomBuild Include="PurchaseOrder.wsdl">
<Outputs>PurchaseOrder.wsdl.c; PurchaseOrder.wsdl.h</Outputs>
<Command>Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.</Command>
</CustomBuild>
即在编译前调用 Wsutil.exe 将 WSDL 契约转译为 C 结构体与常量(如消息 action、全局元素描述符 PurchaseOrder_wsdl.globalElements.PurchaseOrderType),供源码直接使用。这一"WSDL → 头文件 → 编译"的流水线正是 WWSAPI 开发的标准姿势。
链接依赖
工程在链接阶段引入了 WWSAPI 运行时及其依赖库(见 StreamingTcpServer.vcxproj 等处的 <AdditionalDependencies>):
webservices.lib;crypt32.lib;rpcrt4.lib;Iphlpapi.lib
其中 webservices.lib 是 Windows Web Services API 的导入库;crypt32.lib、rpcrt4.lib、Iphlpapi.lib 是 WWSAPI 内部安全、RPC 与网络辅助功能所需的系统库。工程同时将子系统(SubSystem)设为 Console、字符集设为 Unicode,符合示例使用 wmain 与宽字符字符串的编码风格。
消息契约:PurchaseOrder.wsdl
客户端与服务端共享的契约定义在 PurchaseOrder.wsdl 中。该 WSDL 的目标命名空间为 http://example.org,核心内容如下:
- 数据类型:定义了
PurchaseOrderType(含quantity为xsd:int、productName为xsd:string)、OrderConfirmationType、GetOrderStatusType等元素; - 消息:
PurchaseOrder消息以tns:PurchaseOrderType元素为 body 部分; - 端口类型:
IPurchaseOrder定义了Order与OrderStatus两个操作; - Action 标识:
Order操作的输入消息 action 为http://example.org/purchaseorder(PurchaseOrder.wsdl),服务器端正是靠匹配这个 action 来识别消息类型; - 服务:
PurchaseOrderService绑定PurchaseOrderBinding,SOAP 绑定为 document/literal 风格。
本例只是流式演示,因此服务器只关心 PurchaseOrder 消息,不会回复确认消息(单向消息)。
核心实现:逐行剖析 StreamingTcpServer.cpp
StreamingTcpServer.cpp 是一个约 314 行的单文件控制台程序。它的执行可以划分为"初始化 → 接收循环 → 清理"三个阶段,全程使用错误对象(WS_ERROR*)收集丰富的错误信息。
第一步:创建错误对象、监听器与通道
程序首先创建错误对象,然后创建监听器。创建监听器时指定了通道类型与绑定(StreamingTcpServer.cpp):
hr = WsCreateListener(
WS_CHANNEL_TYPE_DUPLEX_SESSION, // 双工会话通道类型
WS_TCP_CHANNEL_BINDING, // TCP 绑定
NULL, 0, NULL, &listener, error);
WS_CHANNEL_TYPE_DUPLEX_SESSION:双工会话通道,允许双向通信且具备会话状态;WS_TCP_CHANNEL_BINDING:将通道绑定到 TCP 传输。
随后用 WsCreateChannelForListener 为监听器创建一个配套通道(L95-L100),再通过 WsOpenListener 将监听器打开到指定 URI:
WS_STRING uri;
uri.chars = L"net.tcp://localhost/example";
uri.length = (ULONG)::wcslen(uri.chars);
hr = WsOpenListener(listener, &uri, NULL, error);
这里的地址 net.tcp://localhost/example 是 WWSAPI 的 TCP 端点地址格式,客户端示例 StreamingTcpClient.cpp 使用完全相同的地址 net.tcp://localhost/example 来建立连接。
第二步:接受客户端通道
监听器就绪后,调用 WsAcceptChannel 阻塞等待客户端连入(L122):
hr = WsAcceptChannel(listener, channel, NULL, error);
接受成功后,为这条通道创建消息对象和堆对象:
hr = WsCreateMessageForChannel(channel, NULL, 0, &message, error);
hr = WsCreateHeap(
/*maxSize*/ 2048, // 堆的最大字节数
/*trimSize*/ 512, // 调用 WsResetHeap 时裁剪到的目标大小
NULL, 0, &heap, error);
WsCreateHeap 的两个参数值得说明:maxSize(2048)限制堆最多分配的内存;trimSize(512)决定每次 WsResetHeap 后堆收缩到的基准大小。堆用于存放反序列化(deserialized)出来的数据,是后续 WsReadElement 读取消息元素时的目标内存来源。
第三步:流式接收循环
核心是一个无限 for 循环(L153),每一轮完成"读消息头 → 校验 action → 流式读 body → 收尾"四个动作:
- 读消息开始(头):
WsReadMessageStart返回WS_S_END表示已无更多消息,跳出循环(L156-L170); - 校验 action 头:用
WsGetHeader读取WS_ACTION_HEADER,再用WsXmlStringEquals与 WSDL 生成的PurchaseOrder_wsdl.messages.PurchaseOrder.action(即http://example.org/purchaseorder)比对,不匹配则返回WS_E_ENDPOINT_ACTION_NOT_SUPPORTED(L172-L198); - 流式读取消息体:通过
WsGetMessageProperty获取消息体读取器WS_MESSAGE_PROPERTY_BODY_READER,然后在内层循环中用WsReadElement逐个读取PurchaseOrderType元素(L201-L251):
for (;;)
{
_PurchaseOrderType* purchaseOrder;
hr = WsReadElement(
reader,
&PurchaseOrder_wsdl.globalElements.PurchaseOrderType,
WS_READ_OPTIONAL_POINTER, // 读不到更多元素时返回 NULL
heap, &purchaseOrder, sizeof(purchaseOrder), error);
...
if (purchaseOrder == NULL) break; // NULL 表示没有更多采购订单
wprintf(L"%ld, %s\n", purchaseOrder->quantity, purchaseOrder->productName);
hr = WsResetHeap(heap, error); // 释放堆,为下一个元素腾出空间
}
这就是"应用层流式"的体现:消息体不是一次性整体反序列化,而是边读边处理——每读出一个 PurchaseOrder 元素就立即打印其 quantity 与 productName,随后 WsResetHeap 清空堆以便容纳下一个元素。一个消息体内可以携带多个连续的 PurchaseOrderType 元素,服务器逐个消费,内存占用被控制在很小范围。
- 读消息结束并复位:
WsReadMessageEnd结束当前消息,WsResetMessage复位消息对象以复用(L253-L270)。
第四步:收尾清理
退出循环后,示例按逆序释放资源(L273-L313):先 WsCloseChannel、WsCloseListener,再依次 WsFreeChannel、WsFreeListener、WsFreeMessage、WsFreeHeap、WsFreeError。失败时调用 PrintError 打印错误码,并通过 WsGetErrorProperty/WsGetErrorString 输出错误对象内全部错误字符串。
与客户端示例的收发配对
为了更好地理解服务器端每个 API 的语义,可以对照 StreamingTcpClient.cpp:
| 服务器端(本例) | 客户端(StreamingTcpClient) | 说明 |
|---|---|---|
WsCreateListener + WsCreateChannelForListener |
WsCreateChannel |
服务器先建监听器,客户端直接建通道 |
WsOpenListener |
WsOpenChannel |
打开监听器 / 打开到端点的通道 |
WsAcceptChannel |
— | 服务器接受入站通道 |
WsReadMessageStart / WsReadElement |
WsWriteMessageStart / WsWriteElement |
流式读取 body / 流式写入 body |
WsReadMessageEnd |
WsWriteMessageEnd |
结束消息 |
WsResetMessage |
WsResetMessage |
双方都复位消息以便复用 |
客户端会连续发送 100 条消息,每条消息体中包含 100 个 PurchaseOrder 元素(productName 为 "Pencil"),服务器端则持续消费这些元素并在控制台打印。两个示例配合使用即可观察到完整的流式收发过程。
关键 API 速查表
README 的 Related topics 列出了本示例涉及的全部 WWSAPI 函数,这里整理成带用途说明的速查表:
| API | 在本例中的用途 |
|---|---|
WsCreateListener |
创建监听器,指定双工会话 + TCP 绑定 |
WsCreateChannelForListener |
创建与监听器配套的通道 |
WsOpenListener |
将监听器打开到 net.tcp://localhost/example |
WsAcceptChannel |
阻塞接受客户端通道 |
WsCreateMessageForChannel |
为通道创建消息对象 |
WsReadMessageStart / WsReadMessageEnd |
读取消息头 / 结束消息读取 |
WsGetHeader |
读取 action 等消息头 |
WsXmlStringEquals |
比对收到的 action 与契约定义是否一致 |
WsGetMessageProperty |
取出消息体读取器 WS_MESSAGE_PROPERTY_BODY_READER |
WsReadElement |
从 body 流式读取 PurchaseOrderType 元素 |
WsResetMessage |
复位消息,供下一轮复用 |
WsCreateHeap / WsResetHeap |
创建/复位反序列化数据堆 |
WsCloseChannel / WsCloseListener |
关闭通道与监听器 |
WsGetErrorProperty / WsGetErrorString |
从错误对象提取并打印错误信息 |
构建、运行与环境要求
系统要求
README 明确给出的操作系统要求为:
- 客户端:Windows 7
- 服务器:Windows Server 2008 R2
此外,readme.txt 补充说明:在 Windows XP、Windows Vista、Windows Server 2003 与 Windows Server 2008 上运行,可能需要先安装包含 Windows Web Services API 运行时 DLL 的 Windows Update。示例本身创建于 Windows 8.1 / Windows Server 2012 R2、Visual Studio 2013 环境,README 同时指出它在多数情况下无需改动即可在更新版本上运行。
构建步骤
- 打开解决方案文件 StreamingTcpServer.sln;
- 在"生成"菜单的"配置管理器"中切换到目标平台(工程支持 Win32 / ARM / x64 三种平台,Debug 与 Release 两种配置,见 StreamingTcpServer.vcxproj);
- 按 F7(Visual Studio 2013 为 F6),或从菜单选择"生成 → 生成解决方案";
- 生成产物默认输出到平台对应的
\bin\Debug或\bin\Release目录(由 vcxproj 中$(SolutionDir)$(Platform)\bin\$(Configuration)\决定)。
运行步骤
构建成功后,按 F5(调试运行)或 Ctrl+F5(不调试运行),或从"调试"菜单选择对应选项。程序启动后阻塞在 WsAcceptChannel,等待客户端(如 StreamingTcpClient)连入,之后持续打印接收到的采购订单。
预留 HTTP URL(仅在需要时)
readme.txt 特别指出:若要以非管理员身份从 Visual Studio 运行 HTTP 类服务器示例,需要先在提升权限的 CMD 中预留 HTTP URL。虽然本例监听的是 net.tcp:// 端点,但这组命令是 WWSAPI 服务器示例共用的通用准备步骤,建议一并掌握:
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 Web Services API 的用法,未经修改与生产环境测试,不得直接用于生产环境,Microsoft 对将其用于其他用途造成的损失不承担任何责任。生产部署时应自行补充身份验证、传输安全、异常处理与压力测试。
从源码结构可推断的扩展方向
从当前示例的代码组织看,可以进一步推断出若干可落地的扩展方向:
- 异步化:本示例使用同步阻塞调用。仓库中的 AsyncStreamingTcpServer 与 AsyncStreamingTcpClient 展示了同一场景的异步实现,适合高并发服务场景参考;
- 请求-响应升级:
PurchaseOrder.wsdl中已定义OrderConfirmationType与OrderStatus操作,但本示例只读不写。若需回复客户端,可在读取后用WsWriteMessageStart/WsWriteElement回写确认消息,形成完整的双工会话; - 错误恢复:
PrintError已封装了WsGetErrorProperty与WsGetErrorString的调用模式,可在生产代码中复用这套错误信息收集逻辑,并结合WS_E_ENDPOINT_ACTION_NOT_SUPPORTED等错误码做分支处理。
小结
StreamingTcpServer 是理解 WWSAPI 服务端编程模型的最小而完整的范例:一条清晰的对象生命周期主线(错误对象 → 监听器 → 通道 → 消息 → 堆),一对关键的流式读取 API(WsReadMessageStart/WsReadElement),以及"应用层流式、传输层缓冲、流式仅限 HTTP 通道"这三个需要牢记的概念边界。配合仓库内的 StreamingTcpServer.cpp 源码、PurchaseOrder.wsdl 契约与 StreamingTcpClient 客户端示例,即可快速搭建并验证一套基于 TCP 的流式消息服务。