通道层概述

通道层提供传输通道的抽象以及通道上发送的消息。 它还包括用于序列化 C 数据类型和从 SOAP 结构进行序列化的函数。 通道层允许 通过消息( 包括发送或接收的数据以及包含正文和标头)以及抽象消息交换协议的 通道 以及提供自定义设置的属性来完全控制通信。

消息

消息是封装网络数据的对象,特别是通过网络传输或接收的数据。 消息结构由 SOAP 定义,其中包含一组离散标头和消息正文。 标头放置在内存缓冲区中,消息正文是使用流 API 读取或写入的。

Diagram showing the header and body of a message.

虽然消息的数据模型始终是 XML 数据模型,但实际线路格式是灵活的。 在传输消息之前,将使用特定编码 ((如文本、二进制或 MTOM) )对其进行编码。 有关编码的详细信息 ,请参阅WS_ENCODING

Diagram showing several message encoding formats.

Channel

通道是一个对象,用于在两个或多个终结点之间发送和接收网络上的消息。

通道具有关联数据,用于 描述在发送 消息时如何处理消息。 在通道上发送消息就像将其放在一个滑道中 — 通道包含消息应转到的位置以及如何到达那里的信息。

Diagram showing channels for messages.

通道分类为 通道类型。 通道类型指定消息可以流动的方向。 通道类型还标识通道是会话还是无会话。 会话定义为在两个或更多方之间关联消息的抽象方式。 会话通道的示例是 TCP 通道,该通道使用 TCP 连接作为具体会话实现。 无会话通道的一个示例是 UDP,它没有基础会话机制。 尽管 HTTP 确实具有基础 TCP 连接,但此事实不会通过此 API 直接公开,因此 HTTP 也被视为无会话通道。

Diagram showing sessionful and sessionless channel types.

虽然通道类型描述通道的方向和会话信息,但它们不指定通道的实现方式。 通道应使用什么协议? 通道应尝试传递消息有多困难? 使用哪种安全? 它是单播还是多播? 这些设置称为通道的“绑定”。 绑定由以下内容组成:

Diagram showing a list of channel properties.

侦听器

若要开始通信,客户端将创建 Channel 对象。 但是该服务如何获取其 Channel 对象? 它通过创建 侦听器来执行此操作。 创建侦听器需要创建通道所需的相同绑定信息。 创建侦听器后,应用程序可以从侦听器接受通道。 由于应用程序可能落后于接受通道,侦听器通常会保留一组可供接受 (最多) 的通道队列。

Diagram showing channels in the Listener queue.

启动通信 (客户端)

若要在客户端上启动通信,请使用以下序列。

WsCreateChannel
for each address being sent to
{
    WsOpenChannel           // open channel to address
    // send and/or receive messages
    WsCloseChannel          // close channel
    WsResetChannel?         // reset if opening again
}
WsFreeChannel

接受通信 (服务器)

若要接受服务器上的传入通信,请使用以下序列。

WsCreateListener
WsOpenListener
for each channel being accepted (can be done in parallel)
{
    WsCreateChannelForListener
    for each accept
    {
        WsAcceptChannel     // accept the channel
        // send and/or receive messages
        WsCloseChannel      // close the channel
        WsResetChannel?     // reset if accepting again
    }
    WsFreeChannel
}
WsCloseListener
WsFreeListener

(客户端或服务器) 发送消息

若要发送消息,请使用以下序列。

WsCreateMessageForChannel
for each message being sent
{
    WsSendMessage       // send message
    WsResetMessage?     // reset if sending another message
}
WsFreeMessage

WsSendMessage 函数不允许流式处理,并且假定正文仅包含一个元素。 若要避免这些约束,请使用以下序列而不是 WsSendMessage

WsInitializeMessage     // initialize message to WS_BLANK_MESSAGE
WsSetHeader             // serialize action header into header buffer
WsAddressMessage?       // optionally address message
for each application defined header
{
    WsAddCustomHeader   // serialize application-defined headers into header buffer
}
WsWriteMessageStart     // write out the headers of the message
for each element of the body
{
    WsWriteBody         // serialize the element of the body
    WsFlushBody?        // optionally flush the body
}
WsWriteMessageEnd       // write the end of the message

WsWriteBody 函数使用序列化来写入正文元素。 若要将数据直接写入 XML 编写器,请使用以下序列而不是 WsWriteBody

WS_MESSAGE_PROPERTY_BODY_WRITER     // get the writer used to write the body
WsWriteStartElement
// use the writer functions to write the body
WsWriteEndElement
// optionally flush the body
WsFlushBody?        

WsAddCustomHeader 函数使用序列化将标头设置为消息的标头缓冲区。 若要使用 XML 编写器编写标头,请使用以下序列,而不是 WsAddCustomHeader

WS_MESSAGE_PROPERTY_HEADER_BUFFER   // get the header buffer 
WsCreateWriter                      // create an xml writer
WsSetOutputToBuffer                 // specify output of writer should go to buffer
WsMoveWriter*                       // move to inside envelope header element
WsWriteStartElement                 // write application header start element
// use the writer functions to write the header 
WsWriteEndElement                   // write appilcation header end element

接收消息 (客户端或服务器)

若要接收消息,请使用以下序列。

WsCreateMessageForChannel
for each message being received
{
    WsReceiveMessage            // receive a message
    WsGetHeader*                // optionally access standard headers such as To or Action
    WsResetMessage              // reset if reading another message
}
WsFreeMessage

WsReceiveMessage 函数不允许流式处理,并且假定正文仅包含一个元素,并且消息的类型 (操作和正文架构) 前面已知。 若要避免这些约束,请使用以下序列而不是 WsReceiveMessage

WsReadMessageStart              // read all headers into header buffer
for each standard header
{
    WsGetHeader                 // deserialize standard header such as To or Action
}
for each application defined header
{
    WsGetCustomHeader           // deserialize application defined header
}
for each element of the body
{
    WsFillBody?                 // optionally fill the body
    WsReadBody                  // deserialize element of body
}
WsReadMessageEnd                // read end of message

WsReadBody 函数使用序列化来读取正文元素。 若要直接从 XML 读取器读取数据,请使用以下序列而不是 WsReadBody

WS_MESSAGE_PROPERTY_BODY_READER     // get the reader used to read the body
WsFillBody?                         // optionally fill the body
WsReadToStartElement                // read up to the body element
WsReadStartElement                  // consume the start of the body element
// use the read functions to read the contents of the body element
WsReadEndElement                    // consume the end of the body element

WsGetCustomHeader 函数使用序列化从消息的标头缓冲区获取标头。 若要使用 XML 读取器 读取标头,请使用以下序列而不是 WsGetCustomHeader

WS_MESSAGE_PROPERTY_HEADER_BUFFER   // get the header buffer 
WsCreateReader                      // create an xml reader
WsSetInputToBuffer                  // specify input of reader should be buffer
WsMoveReader*                       // move to inside header element
while looking for header to read
{
    WsReadToStartElement            // see if the header matches the application header
    if header matched
    {
        WsGetHeaderAttributes?      // get the standard header attributes
        WsReadStartElement          // consume the start of the header element
        // use the read functions to read the contents of the header element
        WsReadEndElement            // consume the end of the header element
    }
    else
    {
        WsSkipNode                  // skip the header element
    }
}                

请求回复 (客户端)

可以在客户端上执行请求答复,但可以按以下顺序完成。

WsCreateMessageForChannel               // create request message 
WsCreateMessageForChannel               // create reply message 
for each request reply
{
    WsRequestReply                      // send request, receive reply
    WsResetMessage?                     // reset request message (if repeating)
    WsResetMessage?                     // reset reply message (if repeating)
}
WsFreeMessage                           // free request message
WsFreeMessage                           // free reply message

WsRequestReply 函数假定请求和答复消息正文的单个元素,并且消息的类型 (操作和正文架构) 前面已知。 为避免这些限制,可以手动发送请求和答复消息,如以下顺序所示。 此序列与前面用于发送和接收消息的序列匹配,但未指出。

WsInitializeMessage     // initialize message to WS_BLANK_MESSAGE
WsSetHeader             // serialize action header into header buffer
WsAddressMessage?       // optionally address message

// the following block is specific to sending a request
{
    generate a unique MessageID for request
    WsSetHeader         // set the message ID            
}

for each application defined header
{
    WsAddCustomHeader   // serialize application-defined headers into header buffer
}
WsWriteMessageStart     // write out the headers of the message
for each element of the body
{
    WsWriteBody         // serialize the element of the body
    WsFlushBody?        // optionally flush the body
}
WsWriteMessageEnd       // write the end of the message

WsReadMessageStart      // read all headers into header buffer

// the following is specific to receiving a reply
{
    WsGetHeader         // deserialize RelatesTo ID of reply
    verify request MessageID is equal to RelatesTo ID
}

for each standard header
{
    WsGetHeader         // deserialize standard header such as To or Action
}
for each application defined header
{
    WsGetCustomHeader   // deserialize application defined header
}
for each element of the body
{
    WsFillBody?         // optionally fill the body
    WsReadBody          // deserialize element of body
}
WsReadMessageEnd        // read end of message                

请求回复 (服务器)

若要在服务器上接收请求消息,请使用与上一节中概述的接收消息相同的顺序。

若要发送回复或错误消息,请使用以下序列。

WsCreateMessageForChannel
for each reply being sent
{
    WsSendReplyMessage | WsSendFaultMessageForError  // send reply or fault message
    WsResetMessage?     // reset if sending another message
}
WsFreeMessage

WsSendReplyMessage 函数假定正文中的单个元素,不允许流式处理。 若要避免这些限制,请使用以下序列。 这与发送消息的早期序列相同,但在初始化时使用 WS_REPLY_MESSAGE 而不是 WS_BLANK_MESSAGE

// the following block is specific to sending a reply
{
    WsInitializeMessage // initialize message to WS_REPLY_MESSAGE
}
WsSetHeader             // serialize action header into header buffer                                
WsAddressMessage?       // optionally address message
for each application defined header
{
    WsAddCustomHeader   // serialize application-defined headers into header buffer
}
WsWriteMessageStart     // write out the headers of the message
for each element of the body
{
    WsWriteBody         // serialize the element of the body
    WsFlushBody?        // optionally flush the body
}
WsWriteMessageEnd       // write the end of the message

消息交换模式

WS_CHANNEL_TYPE指示给定通道可能的消息交换模式。 支持的类型因绑定而异,如下所示:

消息循环

对于每个消息交换模式,都有一个可用于发送和接收消息的特定“循环”。 循环描述了发送和接收多个消息所需的操作的法律顺序。 循环描述为语法生产。 “end”术语是返回WS_S_END的接收, (请参阅Windows Web 服务返回值) ,指示频道上没有更多消息可用。 并行生产指定,对于并行 (x y) 操作 x & 可以使用 y 并发完成。

客户端使用以下循环:

client-loop := client-request-loop | client-duplex-session-loop | client-duplex-loop
client-request-loop := open (send (receive | end))* close // WS_CHANNEL_TYPE_REQUEST
client-duplex-session-loop := open parallel(send* & receive*) parallel(send? & end*) close // WS_CHANNEL_TYPE_DUPLEX_SESSION
client-duplex-loop := open parallel(send & receive)* close // WS_CHANNEL_TYPE_DUPLEX

在服务器上使用以下循环:

server-loop: server-reply-loop | server-duplex-session-loop | server-duplex-loop
server-reply-loop := accept receive end* send? end* close // WS_CHANNEL_TYPE_REPLY
server-duplex-session-loop := accept parallel(send* & receive*) parallel(send* & end*) close // WS_CHANNEL_TYPE_DUPLEX_SESSION
server-input-loop := accept receive end* close // WS_CHANNEL_TYPE_INPUT

在服务器上使用 WS_SECURITY_CONTEXT_MESSAGE_SECURITY_BINDING 需要成功接收,然后才允许发送,即使具有类型 为WS_CHANNEL_TYPE_DUPLEX_SESSION的通道也是如此。 第一次接收后。 常规循环适用。

请注意,WS_CHANNEL_TYPE_REQUEST和WS_CHANNEL_TYPE_REPLY类型的通道可用于发送和接收单向消息 (以及标准请求-回复模式) 。 这是通过关闭回复通道而不发送答复来实现的。 在这种情况下,请求通道上不会收到任何答复。 返回值 WS_S_END 使用服务器上的 WS_SECURITY_CONTEXT_MESSAGE_SECURITY_BINDING 需要成功接收,然后再允许发送,即使具有类型 为WS_CHANNEL_TYPE_DUPLEX_SESSION的通道也是如此。 第一次接收常规循环后,应用常规循环。

将返回,指示没有可用的消息。

可以使用多个通道实例并行执行客户端或服务器循环。

parallel-client: parallel(client-loop(channel1) & client-loop(channel2) & ...)
parallel-server: parallel(server-loop(channel1) & server-loop(channel2) & ...)

消息筛选

服务器通道可以筛选未用于应用程序的消息,例如建立 安全上下文的消息。 在这种情况下 ,WS_S_END 将从 WsReadMessageStart 返回,该通道上不会提供任何应用程序消息。 但是,这不会表明客户端打算结束与服务器的通信。 其他通道上可能有更多消息可用。 请参阅 WsShutdownSessionChannel

取消

WsAbortChannel 函数用于取消通道挂起的 IO。 此 API 不会等待 IO 操作 () 完成。 有关详细信息,请参阅 WsAbortChannelWS_CHANNEL_STATE状态图和文档。

WsAbortListener API 用于取消侦听器的挂起 IO。 此 API 不会等待 IO 操作 () 完成。 中止侦听器也会导致任何挂起的接受被中止。 有关详细信息,请参阅 WS_LISTENER_STATE 状态图和 WsAbortListener

TCP

WS_TCP_CHANNEL_BINDING支持通过 TCP 的 SOAP。 基于 TCP 规范的 SOAP 基于 .NET 框架机制构建。

此版本不支持端口共享。 打开的每个侦听器都必须使用不同的端口号。

UDP

WS_UDP_CHANNEL_BINDING支持 SOAP over UDP。

UDP 绑定存在许多限制:

  • 不支持安全性。
  • 消息可能会丢失或重复。
  • 仅支持一个编码: WS_ENCODING_XML_UTF8
  • 消息从根本上限制为 64k,如果大小超过网络的 MTU,通常会丢失更大的机会。

HTTP

WS_HTTP_CHANNEL_BINDING支持通过 HTTP 的 SOAP。

若要控制客户端和服务器上的 HTTP 特定标头,请参阅 WS_HTTP_MESSAGE_MAPPING

若要在服务器上发送和接收非 SOAP 消息,请使用 WS_ENCODING_RAW 进行 WS_CHANNEL_PROPERTY_ENCODING

NAMEDPIPES

WS_NAMEDPIPE_CHANNEL_BINDING支持通过命名管道的 SOAP,允许使用 NetNamedPipeBinding 与 Windows Communication Foundation (WCF) 服务通信。

关联请求/回复消息

请求/回复消息以以下两种方式之一相关联:

  • 使用通道作为关联机制完成关联。 例如,使用 WS_ADDRESSING_VERSION_TRANSPORTWS_HTTP_CHANNEL_BINDING 请求消息的回复与请求相关,因为该请求是 HTTP 响应的实体正文。
  • 关联是使用 MessageID 和 CorrelationsTo 标头完成的。 即使使用WS_HTTP_CHANNEL_BINDING) ,此机制也与WS_ADDRESSING_VERSION_1_0和WS_ADDRESSING_VERSION_0_9 (一起使用。 在这种情况下,请求消息包括 MessageID 标头。 响应消息包含一个 RelatesTo 标头,其中包含请求的 MessageID 标头的值。 RelatesTo 标头允许客户端将响应与发送的请求相关联。

以下通道层 API 根据通道 的WS_ADDRESSING_VERSION 自动使用适当的关联机制。

如果未使用这些 API,可以使用 WsSetHeaderWsGetHeader 手动添加和访问标头。

自定义通道和侦听器

如果预定义的通道绑定集不符合应用程序的需求,则可以通过在创建通道或侦听器时指定 WS_CUSTOM_CHANNEL_BINDING 来定义自定义通道和侦听器实现。 通道/侦听器的实际实现通过 WS_CHANNEL_PROPERTY_CUSTOM_CHANNEL_CALLBACKSWS_LISTENER_PROPERTY_CUSTOM_LISTENER_CALLBACKS 属性指定为一组回调。 创建自定义通道或侦听器后,结果是可用于现有 API 的WS_CHANNELWS_LISTENER 对象。

通过指定WS_CHANNEL_BINDING枚举中的WS_CUSTOM_CHANNEL_BINDING值,以及创建服务代理或服务主机时WS_CHANNEL_PROPERTY_CUSTOM_CHANNEL_CALLBACKS和WS_LISTENER_PROPERTY_CUSTOM_LISTENER_CALLBACKS属性,自定义通道和侦听器还可以与服务代理和服务主机一起使用。

安全性

通道允许通过属性限制用于操作的各个方面的内存量,例如:

对于大多数方案,这些属性具有保守且安全的默认值。 应根据可能导致远程用户拒绝服务的潜在攻击途径仔细评估默认值和对其所做的任何修改。

通道允许通过以下属性为操作的各个方面设置超时值:

对于大多数方案,这些属性具有保守且安全的默认值。 增加超时值会增加远程方可以保持本地资源活动的时间量,例如内存、套接字和执行同步 I/O 的线程。 应用程序应评估默认值,并在增加超时时谨慎,因为它可能会打开可能导致远程计算机拒绝服务的潜在攻击途径。

使用 WWSAPI 通道 API 时应仔细评估的一些其他配置选项和应用程序设计注意事项:

  • 使用通道/侦听器层时,由应用程序在服务器端创建和接受通道。 同样,由应用程序在客户端创建和打开通道。 应用程序应对这些操作施加上限,因为每个通道消耗内存和其他有限的资源(如套接字)。 当创建通道以响应远程方触发的任何操作时,应用程序应特别小心。
  • 由应用程序编写逻辑以创建通道并接受这些通道。 每个通道消耗有限的资源,例如内存和套接字。 应用程序应具有它愿意接受的通道数上限,或者远程方可能会建立许多连接,从而导致 OOM,因此拒绝服务。 它还应使用小超时主动从这些连接接收消息。 如果未收到任何消息,则操作将超时,并且应释放连接。
  • 由应用程序通过解释 ReplyTo 或 FaultTo SOAP 标头发送回复或错误。 安全做法是仅遵循“匿名”的 ReplyTo 或 FaultTo 标头,这意味着应使用现有连接 (TCP、HTTP) 或源 IP (UDP) 发送 SOAP 回复。 应用程序在创建资源 ((例如通道) )时应非常谨慎,以便答复其他地址,除非消息是由可以代表答复发送到的地址的一方签名的。
  • 通道层中完成的验证不能替代通过安全性实现的数据完整性。 应用程序必须依赖于 WWSAPI 的安全功能,以确保它与受信任的实体通信,并且还必须依赖安全性来确保数据完整性。

同样,使用 WWSAPI 消息 API 时,应仔细评估消息配置选项和应用程序设计注意事项:

  • 可以使用 WS_MESSAGE_PROPERTY_HEAP_PROPERTIES 属性配置用于存储消息标头的堆的大小。 增加此值可让消息的标头消耗更多的内存,这可能会导致 OOM。
  • 消息对象的用户必须意识到标头访问 API 是 O (n) ,因为它们检查重复项。 需要消息中许多标头的设计可能会导致 CPU 使用率过高。
  • 可以使用 WS_MESSAGE_PROPERTY_MAX_PROCESSED_HEADERS 属性配置消息中的最大标头数。 根据消息堆的大小,还有隐式限制。 增加这两个值允许存在更多标头,这加剧了在使用标头访问 API) 时查找标头 (所需的时间。