Windows-classic-samples 请求-应答 TCP 服务端示例解析:基于 Windows Web Services API 的消息服务实现
Windows-classic-samples 请求-应答 TCP 服务端示例解析:基于 Windows Web Services API 的消息服务实现
本文以 Windows-classic-samples 仓库中的 RequestReplyTcpServer 示例为核心,系统讲解如何借助 Windows Web Services API(WWSAPI)实现一个基于 TCP 的请求-应答(Request-Reply)服务端:创建并打开监听器、接受客户端通道、循环接收请求消息并按契约匹配反序列化、生成应答或故障(Fault)消息回发给客户端。读完本文,你将掌握 WWSAPI 中 Listener/Channel/Message 三大对象从创建、打开、使用到释放的完整生命周期,并能对照 RequestReplyTcpServer.cpp 写出可运行的 TCP 服务端骨架。
示例概览:它演示了什么
RequestReplyTcpServer 是 Windows-classic-samples 仓库 Samples/WebServices/RequestReplyTcpServer 目录下的一个 C++ 控制台示例。根据 README.md 的说明,该示例演示如何使用 Windows Web Services API 实现一个 TCP 服务器:它接受(accept)一个通道(channel),并持续接收/回复消息(receives/replies to messages)。
从源码结构看,该示例的业务模型是一套"采购订单"(Purchase Order)服务,支持两类请求-应答交互:
- 提交采购订单:客户端发送
PurchaseOrder(含数量与产品名),服务端返回OrderConfirmation(含订单 ID 与预计发货日期); - 查询订单状态:客户端发送
GetOrderStatus(含订单 ID),服务端返回GetOrderStatusResponse(含状态字符串);若订单 ID 无法识别,则返回OrderNotFound故障。
示例同时演示了 WWSAPI 中错误对象(WS_ERROR)的完整用法——包括通过 WsGetErrorString 提取错误字符串、通过 WsSetFaultErrorDetail 写入故障细节、通过 WsSendFaultMessageForError 将错误对象序列化为 Fault 消息发送给客户端。
运行环境与操作系统要求
README.md 明确给出了本示例的运行平台要求:
- 客户端(Client):Windows 7
- 服务端(Server):Windows Server 2008 R2
该示例创建于 Windows 8.1 / Windows Server 2012 R2 时代,使用 Visual Studio 2013 开发,README 同时说明:在多数情况下,使用更高版本的 Visual Studio 与 Windows 可以无需修改直接运行。另据同目录 readme.txt 的说明,若要在更早的系统(Windows XP、Windows Vista、Windows Server 2003/2008)上运行,可能需要先安装包含 Windows Web Services API 运行时 DLL 的 Windows Update 补丁。
需要注意的是,本示例使用的是 net.tcp://localhost/example 这一 TCP 监听地址,因此不需要为 HTTP 服务器示例准备的 URL 保留(URL ACL)操作。readme.txt 中附带的 netsh http add urlacl 说明主要面向同套件中基于 HTTP 的服务器示例(在非提升权限的 Visual Studio 中运行时需要预先保留 http://+:80/example 与 https://+:443/example 等 URL),可作为理解 WWSAPI 各传输绑定差异的背景参考。
构建与运行示例
README.md 给出了标准的构建与运行步骤,归纳如下:
构建步骤
- 使用 Microsoft Visual Studio Express 2013 for Windows 8.1(或更高版本的 Visual Studio,任意 SKU)打开
cpp/RequestReplyTcpServer.sln解决方案文件; - 按
F7(Visual Studio 2013 亦可使用F6),或在顶部菜单选择 Build -> Build Solution; - 构建产物默认输出到工程的
Debug或Release目录。
运行步骤
- 按
F5(启用调试运行),或按Ctrl-F5(不启用调试运行),也可从 Debug 菜单选择对应选项。
工程文件 RequestReplyTcpServer.vcxproj 进一步补充了构建细节:
- 支持 Win32 / x64 / ARM 三种平台,每种平台均有 Debug / Release 两种配置,输出目录为
$(SolutionDir)$(Platform)\bin\$(Configuration)\; - 字符集统一为 Unicode(
CharacterSet=Unicode),控制台子系统(SubSystem=Console); - 链接依赖
webservices.lib(WWSAPI 主库)、crypt32.lib、rpcrt4.lib、Iphlpapi.lib; - 工程通过自定义构建步骤
Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.从 WSDL 契约文件生成PurchaseOrder.wsdl.c/PurchaseOrder.wsdl.h(详见下文"WSDL 契约与代码生成"一节)。
核心 API 全景
README.md 的 "Related topics" 一节列出了本示例使用到的全部核心 API,这是理解示例功能脉络的索引。结合源码,各 API 的实际用途如下:
| API | 在示例中的作用 |
|---|---|
WsCreateListener |
创建监听器对象,指定通道类型为 WS_CHANNEL_TYPE_DUPLEX_SESSION、绑定为 WS_TCP_CHANNEL_BINDING |
WsCreateChannelForListener |
创建与监听器配套的通道对象,供后续 WsAcceptChannel 使用 |
WsOpenListener |
以 net.tcp://localhost/example 地址打开监听器,开始等待客户端连接 |
WsAcceptChannel |
接受一个来自客户端的通道,建立服务会话 |
WsCreateMessageForChannel |
为通道创建请求消息与应答消息对象(示例中分别创建了 requestMessage 和 replyMessage) |
WsReceiveMessage |
接收请求消息,并按一组 WS_MESSAGE_DESCRIPTION 将消息体反序列化到堆上分配的对应结构中 |
WsSendReplyMessage |
发送应答消息,将应答体的值序列化进消息 |
WsSendFaultMessageForError |
在出错时将错误对象中累积的信息序列化为 Fault 消息发送给客户端 |
WsSetFaultErrorDetail |
将故障细节数据写入错误对象,供 Fault 消息携带 |
WsAddErrorString |
向错误对象追加错误字符串,该字符串会包含在发出的 Fault 中 |
WsGetErrorProperty |
查询错误对象属性(示例中用于获取错误字符串条数 WS_ERROR_PROPERTY_STRING_COUNT) |
WsGetErrorString |
按索引提取错误对象中的错误字符串 |
WsGetFaultErrorDetail |
从错误对象中读取 Fault 细节(客户端侧配套使用) |
WsResetError / WsResetMessage / WsResetHeap |
在每次循环后重置错误对象、消息对象与堆,实现资源复用 |
WsCloseListener / WsFreeListener |
关闭并释放监听器 |
WsCloseChannel / WsFreeChannel |
关闭并释放通道 |
WsFreeMessage / WsFreeError / WsFreeHeap |
释放消息、错误对象与堆 |
WsRequestReply |
README 将其列入相关主题;该 API 是客户端示例 WsRequestReply 的核心封装(服务端通过 WsReceiveMessage + WsSendReplyMessage 实现同一请求-应答语义) |
源码深度解析(一):监听器、通道与消息对象的创建
示例入口 wmain()(见 RequestReplyTcpServer.cpp)遵循 WWSAPI 的标准对象创建顺序,每一步失败都会跳转到统一的 Exit 清理路径。
1. 创建错误对象与堆
hr = WsCreateError(NULL, 0, &error); // L89-L92
hr = WsCreateHeap(/*maxSize*/ 2048, /*trimSize*/ 512, NULL, 0, &heap, error); // L99-L105
error(WS_ERROR*)用于承载丰富的错误信息,贯穿整个示例的生命周期;heap(WS_HEAP*)用于存储反序列化数据。示例将最大堆大小设为 2048 字节、回收阈值 trimSize 设为 512 字节,反序列化得到的请求体指针在该堆上分配,直到调用WsResetHeap才失效。
2. 创建监听器与配套通道
hr = WsCreateListener(
WS_CHANNEL_TYPE_DUPLEX_SESSION, // 通道类型:双工会话
WS_TCP_CHANNEL_BINDING, // 通道绑定:TCP
NULL, 0, NULL, &listener, error); // L112-L119
hr = WsCreateChannelForListener(listener, NULL, 0, &channel, error); // L126-L131
- 通道类型
WS_CHANNEL_TYPE_DUPLEX_SESSION表示双工(双向收发)、带会话语义的通道,适合在单一 TCP 连接上持续进行多次请求-应答交互; WS_TCP_CHANNEL_BINDING指定底层传输为 TCP,对应的地址形式为net.tcp://;WsCreateChannelForListener创建的通道与监听器绑定,后续直接交给WsAcceptChannel使用。
3. 打开监听器并接受客户端连接
WS_STRING uri;
uri.chars = L"net.tcp://localhost/example";
uri.length = (ULONG)::wcslen(uri.chars);
hr = WsOpenListener(listener, &uri, NULL, error); // L138-L149
hr = WsAcceptChannel(listener, channel, NULL, error); // L153-L157
- 监听地址为
net.tcp://localhost/example,服务端固定监听本机 80 端口之外的自定义路径/example; WsAcceptChannel为阻塞调用,等待客户端通过相同地址连接。示例只接受单个通道(单客户端演示),接受后进入消息处理循环。
4. 为通道创建请求与应答消息
hr = WsCreateMessageForChannel(channel, NULL, 0, &requestMessage, error); // L159-L164
hr = WsCreateMessageForChannel(channel, NULL, 0, &replyMessage, error); // L170-L175
请求消息与应答消息都绑定到同一通道上,循环过程中通过 WsResetMessage 反复复用。
源码深度解析(二):请求接收与消息分发
消息处理主体是 for (;;) 无限循环(RequestReplyTcpServer.cpp)。
消息描述数组:服务端支持的消息类型
const WS_MESSAGE_DESCRIPTION* requestMessageDescriptions[] =
{
&PurchaseOrder_wsdl.messages.PurchaseOrder, // 消息体为 _PurchaseOrderType
&PurchaseOrder_wsdl.messages.GetOrderStatus, // 消息体为 GetOrderStatus
}; // L186-L190
这个由契约(WSDL)生成器产出的消息描述数组,定义了服务端"认识"的两种请求。接收时按该数组依次匹配。
接收并反序列化请求
hr = WsReceiveMessage(
channel, requestMessage,
requestMessageDescriptions, WsCountOf(requestMessageDescriptions),
WS_RECEIVE_OPTIONAL_MESSAGE, // 允许通道上不再有消息(返回 WS_S_END)
WS_READ_REQUIRED_POINTER, // 消息体必须存在,反序列化为指针
heap,
&requestBodyPointer, sizeof(requestBodyPointer),
&indexOfMatchedMessageDescription,
NULL, error); // L198-L200
要点:
WS_RECEIVE_OPTIONAL_MESSAGE允许接收在通道空闲时返回WS_S_END。示例据此判断"通道上已无更多消息"并跳出循环(if (hr == WS_S_END) break;,见 L202-L206);- 匹配到的消息描述索引保存在
indexOfMatchedMessageDescription,用于后续分派; - 反序列化结果
requestBodyPointer指向heap上的内存,类型由匹配到的消息描述决定。
源码深度解析(三):业务处理与应答发送
收到请求后,示例根据 indexOfMatchedMessageDescription 定位命中的消息描述(RequestReplyTcpServer.cpp),分两条业务路径处理。
路径一:处理采购订单(PurchaseOrder → OrderConfirmation)
_PurchaseOrderType* purchaseOrder = (_PurchaseOrderType*)requestBodyPointer;
wprintf(L"%d, %s\n", purchaseOrder->quantity, purchaseOrder->productName); // L226-L228
orderConfirmation.expectedShipDate = L"1/1/2006";
orderConfirmation.orderID = 123;
replyMessageDescription = &PurchaseOrder_wsdl.messages.OrderConfirmation;
replyBodyPointer = &orderConfirmation;
replyBodySize = sizeof(orderConfirmation); // L230-L237
示例将订单内容打印到控制台,然后初始化 _OrderConfirmationType 应答结构(订单 ID 固定为 123、预计发货日期为 1/1/2006),并指定应答使用 OrderConfirmation 消息描述。
路径二:查询订单状态(GetOrderStatus → GetOrderStatusResponse 或 Fault)
_GetOrderStatusType* getOrderStatus = (_GetOrderStatusType*)requestBodyPointer;
if (getOrderStatus->orderID != 123)
{
// ... 构造 OrderNotFound 故障细节,见下文
}
else
{
getOrderStatusResponse.orderID = getOrderStatus->orderID;
getOrderStatusResponse.status = L"Pending";
replyMessageDescription = &PurchaseOrder_wsdl.messages.GetOrderStatusResponse;
replyBodyPointer = &getOrderStatusResponse;
replyBodySize = sizeof(getOrderStatusResponse); // L296-L302
}
业务逻辑约定:只有订单 ID 等于 123 才被认为有效。无效时走故障路径,有效时返回状态为 "Pending" 的 GetOrderStatusResponse。
发送应答消息
hr = WsSendReplyMessage(
channel, replyMessage,
replyMessageDescription,
WS_WRITE_REQUIRED_VALUE, // 应答体必须存在,序列化其值
replyBodyPointer, replyBodySize,
requestMessage, // 关联原始请求,用于消息关联
NULL, error); // L347-L356
WsSendReplyMessage 会将应答结构序列化进应答消息体,并依据请求消息建立应答与请求的关联(Request-Reply 语义的核心)。
源码深度解析(四):故障(Fault)处理路径
故障处理是本示例最有教学价值的部分,完整展示了 WWSAPI 中"错误对象 → Fault 消息"的流转。
1. 构造故障细节并写入错误对象
当订单 ID 无效时(RequestReplyTcpServer.cpp),示例:
_OrderNotFoundFaultType orderNotFound;
orderNotFound.orderID = getOrderStatus->orderID;
// 描述故障细节元素:名称 OrderNotFound、命名空间 http://example.com、类型 WS_UINT32_TYPE
static const WS_ELEMENT_DESCRIPTION _faultElementDescription = { ... };
static const WS_FAULT_DETAIL_DESCRIPTION orderNotFoundFaultTypeDescription = { ... };
hr = WsSetFaultErrorDetail(error, &orderNotFoundFaultTypeDescription,
WS_WRITE_REQUIRED_VALUE, &orderNotFound, sizeof(orderNotFound)); // L268-L273
static const WS_STRING errorMessage = WS_STRING_VALUE(L"Invalid order ID");
hr = WsAddErrorString(error, &errorMessage); // L282-L283
hr = E_FAIL; // 用失败码指示需要发送故障
WsSetFaultErrorDetail把故障细节(携带错误订单 ID)写入错误对象,故障细节的描述包括元素名称、命名空间、类型(此处为WS_UINT32_TYPE);WsAddErrorString追加人类可读的错误字符串"Invalid order ID",该字符串会进入 Fault 消息;- 将
hr置为E_FAIL,作为"需要发送故障"的分支信号。
2. 将错误对象序列化为 Fault 消息发送
hr = WsSendFaultMessageForError(
channel, replyMessage, error,
hr, // 原始失败码(不直接传输,仅用于生成错误字符串)
WS_FULL_FAULT_DISCLOSURE, // 完整故障披露级别
requestMessage, NULL, error); // L315-L323
WS_FULL_FAULT_DISCLOSURE表示向对端完整披露故障信息;- 失败码本身不会传输给对端,仅当错误对象中没有错误字符串时,用它生成默认错误字符串;
- 发送后依次调用
WsResetError与WsResetMessage,为下一轮复用清理状态(L331-L342)。
3. 客户端侧读取故障细节(配套验证)
客户端示例 RequestReplyTcpClient.cpp 在收到 WS_E_ENDPOINT_FAULT_RECEIVED 后,使用与服务器对称的 WS_FAULT_DETAIL_DESCRIPTION 描述调用 WsGetFaultErrorDetail,取出 _OrderNotFoundFaultType 并打印 Order %lu was not found,印证了服务器发出的 Fault 细节可被客户端正确反序列化还原。
请求-应答循环与资源复用模式
每次迭代结束时,示例依次重置三类资源,这是 WWSAPI 服务端长循环的经典模式(RequestReplyTcpServer.cpp):
hr = WsResetMessage(requestMessage, error); // 重置请求消息,可再次接收
hr = WsResetMessage(replyMessage, error); // 重置应答消息,可再次发送
hr = WsResetHeap(heap, error); // 释放堆上所有反序列化分配
WsResetMessage使消息对象回到可复用状态,避免为每条消息重新创建对象;WsResetHeap一次释放此前WsReceiveMessage/WsSendReplyMessage期间在堆上产生的全部分配(这正是注释"有效直到 WsResetHeap 被调用"的语义)。
退出与清理(Exit 标签,L386-L432):示例按逆序释放所有对象——WsCloseListener/WsFreeListener、WsCloseChannel/WsFreeChannel、WsFreeMessage(两个消息)、WsFreeError、WsFreeHeap,并在开头定义了统一的错误打印函数 PrintError(L18-L55):通过 WsGetErrorProperty 读取 WS_ERROR_PROPERTY_STRING_COUNT 得到错误字符串条数,再逐条用 WsGetErrorString 取出并 wprintf 输出,同时对 E_INVALIDARG、WS_E_INVALID_OPERATION 给出"API 误用(程序 bug)"的提示。
WSDL 契约与代码生成:PurchaseOrder.wsdl 剖析
服务端与客户端共享同一份服务契约 PurchaseOrder.wsdl,其中定义了全部消息类型与操作:
类型(wsdl:types,L11-L48)
| 元素 | 字段 |
|---|---|
PurchaseOrderType |
quantity(xsd:int)、productName(xsd:string) |
OrderConfirmationType |
orderID(xsd:unsignedInt)、expectedShipDate(xsd:string) |
GetOrderStatusType |
orderID(xsd:unsignedInt) |
GetOrderStatusResponseType |
orderID、status(xsd:string) |
OrderNotFoundFaultType |
orderID(xsd:unsignedInt) |
消息与操作(L51-L76):wsdl:message 定义了 PurchaseOrder、OrderConfirmation、GetOrderStatus、GetOrderStatusResponse、OrderNotFoundFault 五种消息;portType IPurchaseOrder 定义了 Order 与 OrderStatus 两个操作,其中 OrderStatus 操作声明了 OrderNotFound 故障,各操作通过 wsaw:Action 指定消息动作 URI(如 http://example.org/purchaseorder)。
代码生成:工程通过自定义构建步骤 Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.(见 RequestReplyTcpServer.vcxproj)在编译时生成 PurchaseOrder.wsdl.c 与 PurchaseOrder.wsdl.h。源码中反复出现的 PurchaseOrder_wsdl.messages.PurchaseOrder、PurchaseOrder_wsdl.messages.OrderConfirmation 等正是生成产物暴露的全局描述符结构,_PurchaseOrderType、_OrderConfirmationType、_GetOrderStatusType、_OrderNotFoundFaultType 等结构类型也来自该生成头文件。这也是源码可以"按契约描述收发消息、由运行时自动完成序列化/反序列化"的关键机制。
配套客户端示例:完整闭环验证
仓库中的 RequestReplyTcpClient 与本服务端示例构成完整闭环,其关键差异在于:
- 客户端使用
WsCreateChannel(L94-L101)+WsOpenChannel连接到同样的net.tcp://localhost/example地址(L108-L124),而服务端使用WsCreateListener/WsOpenListener/WsAcceptChannel; - 客户端用
WsRequestReply单次调用完成"发送请求 + 等待应答"(L159-L173),README 的 Related topics 中列出的WsRequestReply正是在这里体现——它等效于服务端的WsReceiveMessage+WsSendReplyMessage组合; - 客户端循环发送 100 次请求(L149),依次测试:提交采购订单、查询有效订单状态(orderID=123)、查询无效订单状态(orderID=321,L247),用于验证服务端的正常应答与故障应答两条路径;
- 客户端通过检查返回值
WS_E_ENDPOINT_FAULT_RECEIVED识别故障,并用WsGetFaultErrorDetail反序列化故障细节。
运行方式:先运行服务端 RequestReplyTcpServer(监听 net.tcp://localhost/example),再运行 RequestReplyTcpClient,即可在服务端控制台看到订单打印,在客户端控制台看到订单确认信息、状态为 Pending 的查询结果以及"Order 321 was not found"的故障提示。
安全与生产环境使用提示
同目录 readme.txt 明确提示:本示例仅供教学演示,用于说明 Windows Web Services API 的用法,未在生产环境测试,也不建议不加修改直接用于生产环境。结合实际看,示例存在以下仅适合演示的特性:
- 服务端只接受单个通道、单线程顺序处理,不具备并发能力;
- 业务校验极简(订单 ID 固定为 123),无身份认证、消息加密与传输安全(如 TLS/SSL)配置;
- 监听地址固定为
net.tcp://localhost/example,未做参数化。
在生产项目中,应基于 WWSAPI 的安全通道属性(如 WS_CHANNEL_PROPERTY_SECURITY、SSL 证书绑定等)补充传输安全,并引入多通道/多线程的并发处理模型。若要学习安全传输场景,可进一步查阅仓库 Samples/WebServices 目录下以 WithSsl、WithKerberosOverSsl 等命名的同类 Web 服务示例。
小结
RequestReplyTcpServer 示例完整覆盖了 WWSAPI 服务端的标准流程:创建错误/堆 → 创建监听器与配套通道 → 打开监听器 → 接受通道 → 按消息描述接收并分发请求 → 发送应答或故障 → 重置资源循环复用 → 逆序释放全部对象。对照 RequestReplyTcpServer.cpp、PurchaseOrder.wsdl 与配套的 RequestReplyTcpClient.cpp,即可完整掌握 TCP 绑定下请求-应答消息服务的实现原理,并以此为模板扩展到更复杂的服务契约与安全传输场景。