通道层概述
通道层提供传输通道以及通道上发送的消息的抽象。 它还包含用于将 C 数据类型序列化为 SOAP 结构以及从 SOAP 结构序列化的函数。 通道层通过包含发送或接收的数据、包含正文和标头 的消息 以及抽象消息交换协议并提供自定义设置属性的 通道 ,实现对通信的完全控制。
消息
消息是封装网络数据的对象,具体而言,是通过网络传输或接收的数据。 消息结构由 SOAP 定义,其中包含一组离散的标头和一个消息正文。 标头放置在内存缓冲区中,消息正文是使用流 API 读取或写入的。
尽管消息的数据模型始终是 XML 数据模型,但实际的线路格式是灵活的。 在传输消息之前,将使用特定的编码 ((如文本、二进制或 MTOM) )对其进行编码。 有关编码的详细信息 ,请参阅WS_ENCODING 。
通道
通道是一个对象,用于在网络上在两个或多个终结点之间发送和接收消息。
通道具有关联的数据,用于描述在发送消息时如何 处理 消息。 在通道上发送消息就像将其放在滑槽中一样 - 该通道包含消息应到达的位置以及如何获取该消息的信息。
通道分为 通道类型。 通道类型指定消息可以流动的方向。 通道类型还标识通道是会话通道还是无会话通道。 会话定义为在两个或多个参与方之间关联消息的抽象方式。 会话通道的一个示例是 TCP 通道,它使用 TCP 连接作为具体的会话实现。 无会话通道的一个示例是 UDP,它没有基础会话机制。 尽管 HTTP 确实具有基础 TCP 连接,但此事实不会通过此 API 直接公开,因此 HTTP 也被视为无会话通道。
尽管通道类型描述通道的方向和会话信息,但它们没有指定如何实现通道。 通道应使用哪种协议? 通道尝试传递消息的难度如何? 使用哪种类型的安全性? 是单播还是多播? 这些设置称为通道的“绑定”。 绑定由以下内容组成:
- 一个WS_CHANNEL_BINDING,它标识要 (TCP、UDP、HTTP、NAMEDPIPE) 使用的传输协议。
- WS_SECURITY_DESCRIPTION,指定如何保护通道。
- 一 组WS_CHANNEL_PROPERTY,用于指定其他可选设置。 有关属性列表 ,请参阅WS_CHANNEL_PROPERTY_ID 。
侦听器
为了开始通信,客户端会创建一个 Channel 对象。 但是,服务如何获取其 Channel 对象? 它通过创建 侦听器来执行此操作。 创建侦听器需要创建通道所需的相同绑定信息。 创建侦听器后,应用程序可以接受来自侦听器的通道。 由于应用程序在接受通道时可能会落后,侦听器通常会保留一个通道队列,这些通道已准备好接受 (最多一些配额) 。
启动通信 (客户端)
若要在客户端上启动通信,请使用以下顺序。
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指示给定通道可能的消息交换模式。 支持的类型因绑定而异,如下所示:
- WS_HTTP_CHANNEL_BINDING 支持在客户端上 WS_CHANNEL_TYPE_REQUEST , 在服务器上WS_CHANNEL_TYPE_REPLY 。
- WS_TCP_CHANNEL_BINDING 支持在客户端上 WS_CHANNEL_TYPE_DUPLEX_SESSION , 在服务器上WS_CHANNEL_TYPE_DUPLEX_SESSION 。
- WS_UDP_CHANNEL_BINDING 支持在客户端上 WS_CHANNEL_TYPE_DUPLEX ,在服务器上 WS_CHANNEL_TYPE_INPUT 。
- WS_NAMEDPIPE_CHANNEL_BINDING支持在客户端上WS_CHANNEL_TYPE_DUPLEX,在服务器上WS_CHANNEL_TYPE_INPUT。
消息循环
对于每个消息交换模式,都有一个特定的“循环”可用于发送或接收消息。 循环描述发送/接收多条消息所需的操作的法律顺序。 下面将循环描述为语法制作。 “end”术语是一个接收,其中 返回WS_S_END (请参阅 Windows Web Services 返回值) ,指示通道上没有更多可用的消息。 并行生产指定对于并行 (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_SECURITY_CONTEXT_MESSAGE_SECURITY_BINDING WS_S_END返回值需要成功接收才能发送,即使使用类型为 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支持 SOAP over TCP。 SOAP over TCP 规范基于 .NET Framing 机制构建。
此版本不支持端口共享。 打开的每个侦听器都必须使用不同的端口号。
UDP
WS_UDP_CHANNEL_BINDING支持 SOAP over UDP。
UDP 绑定存在许多限制:
- 不支持安全性。
- 消息可能会丢失或重复。
- 仅支持一种编码: WS_ENCODING_XML_UTF8。
- 消息基本上限制为 64k,如果大小超过网络的 MTU,则丢失的可能性通常更大。
HTTP
WS_HTTP_CHANNEL_BINDING支持 SOAP over HTTP。
若要控制客户端和服务器上的 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_TRANSPORT 和 WS_HTTP_CHANNEL_BINDING 请求消息的回复与请求相关,因为它是 HTTP 响应的实体正文。
- 关联是使用 MessageID 和 RelatesTo 标头完成的。 此机制与 WS_ADDRESSING_VERSION_1_0 一起使用, 即使使用 WS_HTTP_CHANNEL_BINDING) , 也WS_ADDRESSING_VERSION_0_9 ( 。 在这种情况下,请求消息包括 MessageID 标头。 响应消息包含一个 RelatesTo 标头,该标头具有请求的 MessageID 标头的值。 RelatesTo 标头允许客户端将响应与其发送的请求相关联。
以下通道层 API 根据通道 WS_ADDRESSING_VERSION 自动使用适当的关联机制。
如果未使用这些 API,则可以使用 WsSetHeader 或 WsGetHeader 手动添加和访问标头。
自定义通道和侦听器
如果预定义的通道绑定集不满足应用程序的需求,则可以通过在创建通道或侦听器时指定 WS_CUSTOM_CHANNEL_BINDING 来定义自定义通道和侦听器实现。 通道/侦听器的实际实现通过 WS_CHANNEL_PROPERTY_CUSTOM_CHANNEL_CALLBACKS 或 WS_LISTENER_PROPERTY_CUSTOM_LISTENER_CALLBACKS 属性指定为一组回调。 创建自定义通道或侦听器后,结果是可与现有 API 一起使用 的WS_CHANNEL 或 WS_LISTENER 对象。
在创建服务代理或服务主机时,还可以通过指定WS_CHANNEL_BINDING枚举中的WS_CUSTOM_CHANNEL_BINDING值以及WS_CHANNEL_PROPERTY_CUSTOM_CHANNEL_CALLBACKS和WS_LISTENER_PROPERTY_CUSTOM_LISTENER_CALLBACKS属性,将自定义通道和侦听器与服务代理和服务主机一起使用。
安全性
通道允许通过属性限制用于操作的各个方面的内存量,例如:
- WS_CHANNEL_PROPERTY_MAX_BUFFERED_MESSAGE_SIZE,
- WS_CHANNEL_PROPERTY_MAX_STREAMED_MESSAGE_SIZE,
- WS_CHANNEL_PROPERTY_MAX_STREAMED_START_SIZE,
- WS_CHANNEL_PROPERTY_MAX_HTTP_REQUEST_HEADERS_BUFFER_SIZE,
- WS_CHANNEL_PROPERTY_MAX_SESSION_DICTIONARY_SIZE。
这些属性具有默认值,在大多数情况下都是保守的且安全的。 应针对可能导致远程用户拒绝服务的潜在攻击途径仔细评估默认值和对它们所做的任何修改。
通道允许通过属性为操作的各个方面设置超时值,例如:
- WS_CHANNEL_PROPERTY_CONNECT_TIMEOUT,
- WS_CHANNEL_PROPERTY_SEND_TIMEOUT,
- WS_CHANNEL_PROPERTY_RECEIVE_RESPONSE_TIMEOUT,
- WS_CHANNEL_PROPERTY_RECEIVE_TIMEOUT,
- WS_CHANNEL_PROPERTY_RESOLVE_TIMEOUT,
- WS_CHANNEL_PROPERTY_CLOSE_TIMEOUT。
这些属性具有默认值,在大多数情况下都是保守的且安全的。 增加超时值会增加远程一方可以保持本地资源活动状态的时间量,例如内存、套接字和执行同步 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) 时查找标头 (所需的时间。