Windows-classic-samples 请求-应答 TCP 服务端示例解析:基于 Windows Web Services API 的消息服务实现

原创2026-10-15 15:46:191,456 阅读
文章标签:示例工程

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)服务,支持两类请求-应答交互:

  1. 提交采购订单:客户端发送 PurchaseOrder(含数量与产品名),服务端返回 OrderConfirmation(含订单 ID 与预计发货日期);
  2. 查询订单状态:客户端发送 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 给出了标准的构建与运行步骤,归纳如下:

构建步骤

  1. 使用 Microsoft Visual Studio Express 2013 for Windows 8.1(或更高版本的 Visual Studio,任意 SKU)打开 cpp/RequestReplyTcpServer.sln 解决方案文件;
  2. 按 F7(Visual Studio 2013 亦可使用 F6),或在顶部菜单选择 Build -> Build Solution;
  3. 构建产物默认输出到工程的 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 绑定下请求-应答消息服务的实现原理,并以此为模板扩展到更复杂的服务契约与安全传输场景。

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