Windows Web Services API 流式 TCP 服务器实战:基于 Windows-classic-samples 的 StreamingTcpServer 深入解析

原创2026-10-15 14:43:421,637 阅读
文章标签:示例工程

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)消息。

这里有两个必须澄清的技术细节,它们决定了这个示例的价值:

  1. 流式发生在应用层,而非传输层:示例只在应用层做流式处理——消息体(body)按元素逐个读入内存,读完一个就处理一个;而在传输层,数据仍是被缓冲的,不会在客户端尚未发完时就被"回流"给客户端。
  2. 流式模式并非所有通道类型都支持: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 → 收尾"四个动作:

  1. 读消息开始(头):WsReadMessageStart 返回 WS_S_END 表示已无更多消息,跳出循环(L156-L170);
  2. 校验 action 头:用 WsGetHeader 读取 WS_ACTION_HEADER,再用 WsXmlStringEquals 与 WSDL 生成的 PurchaseOrder_wsdl.messages.PurchaseOrder.action(即 http://example.org/purchaseorder)比对,不匹配则返回 WS_E_ENDPOINT_ACTION_NOT_SUPPORTED(L172-L198);
  3. 流式读取消息体:通过 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 元素,服务器逐个消费,内存占用被控制在很小范围。

  1. 读消息结束并复位: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 同时指出它在多数情况下无需改动即可在更新版本上运行。

构建步骤

  1. 打开解决方案文件 StreamingTcpServer.sln;
  2. 在"生成"菜单的"配置管理器"中切换到目标平台(工程支持 Win32 / ARM / x64 三种平台,Debug 与 Release 两种配置,见 StreamingTcpServer.vcxproj);
  3. 按 F7(Visual Studio 2013 为 F6),或从菜单选择"生成 → 生成解决方案";
  4. 生成产物默认输出到平台对应的 \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 的流式消息服务。

登录后查看全文
Windows-classic-samples