Windows Communication Foundation(WCF)是基于 XML 的通信基础结构。 由于 XML 数据通常采用 XML 1.0 规范中定义的标准文本格式进行编码,因此连接的系统开发人员和架构师通常担心跨网络发送的消息的线路占用量(或大小),并且基于文本的 XML 编码对高效传输二进制数据带来了特殊挑战。
基本注意事项
为了提供有关 WCF 以下信息的背景信息,本节重点介绍了编码、二进制数据和流式处理(通常适用于连接的系统基础结构)的一些一般问题和注意事项。
编码数据:文本与二进制
通常表示的开发人员关注点包括,由于开始标记和结束标记的重复性质,XML 在与二进制格式相比具有显著的开销,因为数字值的编码被认为大得多,因为它们以文本值表示,并且二进制数据无法有效表示,因为它必须经过特殊编码才能嵌入文本格式。
虽然其中许多和类似的问题有效,但 XML Web 服务环境中的 XML 文本编码消息与旧式远程过程调用(RPC)环境中的二进制编码消息之间的实际区别通常远不如初始考虑所建议的要少得多。
虽然 XML 文本编码的消息是透明的,并且“人可读”,但二进制消息在比较中往往相当模糊,并且很难在没有工具的情况下解码。 这种易读性的差异导致人们忽略二进制消息也经常在有效负载中携带内联元数据,这增加了开销,就像使用 XML 文本消息一样。 对于旨在提供松散耦合和动态调用功能的二进制格式,这尤其如此。
但是,二进制格式通常在“标头”中携带此类描述性元数据信息,它还声明以下数据记录的数据布局。 之后负载将遵循这一公共元数据块声明,这样就将进一步的开销控制在最小的程度。 相比之下,XML 将每个数据项括在元素或属性中,以便为每个序列化的有效负载对象重复包含封闭元数据。 因此,比较文本与二进制表示形式时,单个序列化有效负载对象的大小是类似的,因为两者都必须表达一些描述性元数据。然而,二进制格式由于总体开销较低以及每个附加传输的有效负载对象共享的元数据描述而受益。
不过,对于某些数据类型(如数字),使用固定大小、二进制数字表示形式(如 128 位十进制类型而不是纯文本)可能有缺点,因为纯文本表示形式可能更小几字节。 文本数据可能也受益于通常更灵活的 XML 文本编码选项,而某些二进制格式可能默认为 16 位甚至 32 位 Unicode,而该格式不适用于 .NET 二进制 XML 格式。
因此,在文本或二进制之间做出决定并不像假设二进制消息总是小于 XML 文本消息那么容易。
XML 文本消息的一个明显优势是,它们是基于标准的,提供互作性选项和平台支持的最广泛的选择。 有关详细信息,请参阅本主题后面的“编码”部分。
二进制内容
在生成的消息大小方面,二进制编码优于基于文本的编码的区域是大型二进制数据项,例如图片、视频、声音剪辑或任何其他形式的不透明二进制数据,这些二进制数据必须在服务与其使用者之间交换。 为了将这些类型的数据放入 XML 文本中,常见方法是使用 Base64 编码对其进行编码。
在 Base64 编码的字符串中,每个字符表示原始 8 位数据的 6 位,这会导致 Base64 的编码开销比为 4:3,但不包括通常通过约定添加的额外格式字符(回车符/换行符)。 虽然 XML 编码与二进制编码之间的差异显著与否通常视具体情况而定,但当传送 500 MB 负载时大小增加超过 33% 通常是不可接受的。
为避免这种编码开销,消息传输优化机制(MTOM)标准允许外部化消息中包含的大型数据元素,并将它们作为二进制数据携带,而无需任何特殊编码。 使用 MTOM 时,邮件以类似于简单邮件传输协议(SMTP)电子邮件,其中包含附件或嵌入内容(图片和其他嵌入内容):MTOM 消息打包为多部分/相关的 MIME 序列,根部分是实际的 SOAP 消息。
MTOM SOAP 消息从其未编码的版本进行修改,以便引用相应 MIME 部件的特殊元素标记取代包含二进制数据的消息中的原始元素。 因此,SOAP 消息通过指向随它发送的 MIME 部分来引用二进制内容,但除此之外只是携带 XML 文本数据。 由于此模型与建立良好的 SMTP 模型密切相关,因此在许多平台上对 MTOM 消息进行编码和解码提供了广泛的工具支持,这使得它成为极其可互作的选择。
不过,与 Base64 一样,MTOM 还附带了 MIME 格式的一些必要开销,因此仅当二进制数据元素的大小超过 1 KB 时,才会看到使用 MTOM 的优势。 由于开销,使用 MTOM 编码的消息可能会比使用 Base64 编码的消息要更大,特别是当二进制数据负载保持在该阈值以下时。 有关详细信息,请参阅本主题后面的“编码”部分。
大规模数据内容
除了线路占用空间外,前面提到的 500 MB 有效负载也为服务和客户端带来了巨大的本地挑战。 默认情况下,WCF 在 缓冲模式下处理消息。 这意味着消息的整个内容在发送或接收消息之前存在于内存中。 虽然这是大多数方案的良好策略,并且对于数字签名和可靠传递等消息传递功能是必需的,但大型消息可能会耗尽系统的资源。
处理大型有效负载的策略是流式处理。 虽然消息(尤其是以 XML 表示的消息)通常被视为相对压缩的数据包,但消息的大小可能为多千兆字节,类似于连续数据流,而不是一个数据包。 当数据以流式传输模式而不是缓冲模式传输时,发送方会以流的形式向收件人提供邮件正文的内容,并且消息基础结构在数据可用时会持续将数据从发送方转发到接收方。
发生此类大型数据传输的最常见方案是传输二进制数据对象,这些对象:
无法轻易分解为消息序列。
必须及时交付。
当开始传输时,还不是已全部就绪。
对于没有这些约束的数据,通常最好在会话范围内发送消息序列,而不是一条大型消息。 有关详细信息,请参阅本主题后面的“流数据”部分。
发送大量数据时,需要设置 maxAllowedContentLength
IIS 设置(有关详细信息,请参阅 配置 IIS 请求限制)和 maxReceivedMessageSize
绑定设置(例如 System.ServiceModel.BasicHttpBinding.MaxReceivedMessageSize 或 MaxReceivedMessageSize)。 该 maxAllowedContentLength
属性默认为 28.6 MB,属性 maxReceivedMessageSize
默认为 64KB。
编码
编码定义有关如何在网络上显示消息的一组规则。 编码器实现此类编码,并负责在发送方将内存Message中转换为可跨网络发送的字节流或字节缓冲区。 在接收方端,编码器将字节序列转换为内存中消息。
WCF 包括三个编码器,并允许根据需要编写和插入自己的编码器。
每个标准绑定都包含一个预配置的编码器,其中,使用 Net* 前缀的绑定使用二进制编码器(包括BinaryMessageEncodingBindingElement类),而BasicHttpBindingWSHttpBinding类默认使用文本消息编码器(通过TextMessageEncodingBindingElement类)。
编码器绑定元素 | DESCRIPTION |
---|---|
TextMessageEncodingBindingElement | 文本消息编码器是所有基于 HTTP 的绑定的默认编码器,也是所有自定义绑定的适当选择,其中互作性是最关心的问题。 此编码器读取和写入标准 SOAP 1.1/SOAP 1.2 文本消息,对二进制数据没有特殊处理。 System.ServiceModel.Channels.MessageVersion如果消息的属性设置为MessageVersion.None,则从输出中省略 SOAP 信封包装器,并且仅序列化消息正文内容。 |
MtomMessageEncodingBindingElement | MTOM 消息编码器是一个文本编码器,用于实现二进制数据的特殊处理,默认情况下不会在任何标准绑定中使用,因为它严格是逐个情况优化实用工具。 如果消息包含超过 MTOM 编码产生优势的阈值的二进制数据,则数据将外部化为消息信封后面的 MIME 部分。 请参阅本节后面的“启用 MTOM”。 |
BinaryMessageEncodingBindingElement | 每当两个通信方都基于 WCF 时,二进制消息编码器是 Net* 绑定的默认编码器,也是适当的选择。 二进制消息编码器使用 .NET 二进制 XML 格式,这是 XML 信息集(Infosets)的特定于Microsoft二进制表示形式,通常生成比等效的 XML 1.0 表示形式更小的占用空间,并将二进制数据编码为字节流。 |
文本消息编码通常是任何需要互作性的通信路径的最佳选择,而二进制消息编码是任何其他通信路径的最佳选择。 与单个消息的文本相比,二进制消息编码通常生成较小的消息大小,并在通信会话期间逐渐减小消息大小。 与文本编码不同,二进制编码不必对二进制数据使用特殊处理,例如使用 Base64,而是将字节表示为字节。
如果解决方案不需要互作性,但仍想要使用 HTTP 传输,则可以将它组合 BinaryMessageEncodingBindingElement 到使用 HttpTransportBindingElement 类进行传输的自定义绑定中。 如果服务上的许多客户端需要互作性,建议公开并行终结点,每个终结点都有相应的传输和编码选项,以便启用相应的客户端。
启用 MTOM
当互作性是一项要求,并且必须发送大型二进制数据时,MTOM 消息编码是一种替代编码策略,可以通过将相应的BasicHttpBinding属性设置为WSHttpBinding或组合MessageEncoding
到其中Mtom来启用标准MtomMessageEncodingBindingElement或CustomBinding绑定。 以下示例代码(从 MTOM 编码 示例中提取)演示如何在配置中启用 MTOM。
<system.serviceModel>
…
<bindings>
<wsHttpBinding>
<binding name="ExampleBinding" messageEncoding="Mtom"/>
</wsHttpBinding>
</bindings>
…
</system.serviceModel>
如前所述,使用 MTOM 编码的决定取决于要发送的数据量。 此外,由于 MTOM 在绑定级别启用,因此启用 MTOM 会影响给定终结点上的所有操作。
由于 MTOM 编码器始终发出 MTOM 编码的 MIME/多部分消息,而不考虑二进制数据是否最终被外部化,因此通常只应为使用 1 KB 以上的二进制数据交换消息的终结点启用 MTOM。 另外,在可能的情况下,应限定设计为与启用 MTOM 的终结点一起使用的服务协定仅指定此类数据传输操作。 相关控件功能应驻留在单独的协定中。 此“仅限 MTOM”规则仅适用于通过启用了 MTOM 的终结点发送的消息;MTOM 编码器还可以解码和分析传入的非 MTOM 消息。
使用 MTOM 编码器符合所有其他 WCF 功能。 请注意,在所有情况下(例如,需要会话支持时),都可能无法观察此规则。
编程模型
无论在应用程序中使用的三个内置编码器中的哪一个,编程体验都与传输二进制数据相同。 区别在于 WCF 如何根据其数据类型处理数据。
[DataContract]
class MyData
{
[DataMember]
byte[] binaryBuffer;
[DataMember]
string someStringData;
}
使用 MTOM 时,根据以下规则序列化上述数据协定:
如果
binaryBuffer
不是null
,并且有个别包含足够的数据,使得需要 Base64 编码所没有的 MTOM 外部化开销(MIME 头等等),则这些数据将外部化并作为二进制 MIME 部分随消息一起传送。 如果未超过阈值,数据将编码为 Base64。无论大小如何,字符串(以及不是二进制的所有其他类型)始终表示为消息正文中的字符串。
无论使用显式数据协定、描述操作中的参数列表、增加嵌套的数据协定,还是传输数据协定对象至集合中,对 MTOM 编码的影响都是相同的,如前面的示例所示。 字节数组始终是优化候选项,如果满足优化阈值,则对其进行优化。
注释
不应在 System.IO.Stream 数据协定中使用派生类型。 流数据应使用流式处理模型进行通信,如以下“流数据”部分所述。
数据的流模式
在需要传输大量数据时,WCF 中的流传输模式是一个可行的选择,它可以替代将消息全部缓存在内存中进行处理的默认行为。
如前所述,仅当无法分段数据时,仅对大型消息(包含文本或二进制内容)启用流式处理;如果必须及时传递消息,或者启动传输时数据尚未完全可用。
限制
启用流式处理时,不能使用大量 WCF 功能:
无法执行消息正文的数字签名,因为它们需要计算整个邮件内容的哈希。 在流媒体过程中,构造并发送消息头时,内容并不完全可用,因此无法计算数字签名。
加密取决于数字签名来验证数据是否已正确重建。
为了确保在消息丢失时能够再次传送,可靠会话必须在客户端缓冲发送的消息。此外,为了在接收到无序消息时保持消息的顺序,必须在将消息交给服务实现之前将其保留在服务端。
由于这些功能约束,只能使用仅限于传输级别的安全选项来进行流式处理,并且不能启用可靠的会话。 流处理仅在下列系统定义的绑定中可用:
由于 NetTcpBinding 和 NetNamedPipeBinding 的基础传输具有内在的可靠传递和基于连接的会话支持,因此与 HTTP 不同,这两个绑定在实践中受上述约束的影响非常小。
流模式在消息队列 (MSMQ) 传输中不可用,因此不能与 NetMsmqBinding 或 MsmqIntegrationBinding 类一起使用。 消息队列传输仅支持具有受约束消息大小的缓冲数据传输,而所有其他传输在绝大多数情况下都没有任何实际消息大小限制。
当使用对等通道传输时,流模式也不可用,因此流模式在 NetPeerTcpBinding 中不可用。
流和会话
在流与基于会话的绑定一起调用时可能会产生意外行为。 可通过单一通道(数据报通道)执行所有流调用,该通道不支持会话,即使将正在使用的绑定配置为使用会话也是如此。 如果多个客户端通过基于会话的绑定对同一服务对象进行流式处理调用,并且服务对象的并发模式设置为单个,并且其实例上下文模式设置为 PerSession,则所有调用都必须通过数据报通道进行,因此每次只处理一个调用。 一个或多个客户端因此可能会超时。通过将该服务对象的实例上下文模式设置为 PerCall 或将“并发”设置为“多个”,即可解决此问题。
注释
在这种情况下,MaxConcurrentSessions 不起作用,因为只有一个“会话”可用。
启用流模式
可以通过以下方式启用数据流:
在流式处理模式下发送和接受请求,并在缓冲模式下接受和返回响应(StreamedRequest)。
在缓冲模式下发送和接受请求,并在流模式(StreamedResponse)中接受和返回响应。
在两个方向的流式传输模式下发送和接收请求和响应。 (Streamed)。
可以通过将传输模式设置为 Buffered禁用流式处理,这是所有绑定的默认设置。 以下代码演示如何在配置中设置传输模式。
<system.serviceModel>
…
<bindings>
<basicHttpBinding>
<binding name="ExampleBinding" transferMode="Streamed"/>
</basicHttpBinding>
</bindings>
…
</system.serviceModel>
在代码中实例化绑定时,必须将绑定的相应 TransferMode
属性(或者编写自定义绑定时传输绑定元素)设置为上述值之一。
可以针对请求和答复启用流式处理,也可以在通信方两端独立地针对两个方向启用流式处理,而不会影响功能。 但是,您应始终假设传输的数据量很大,以至于在通信链路的两个端点启用流式处理是合理的。 对于其中一个终结点不是使用 WCF 实现的跨平台通信,能否使用流模式取决于平台的流功能。 另一种罕见的情况可能是一种受到内存消耗影响的场景,其中客户端或服务必须最小化其工作集,并且只能接受较小的缓冲区大小。
启用异步流处理
若要启用异步流式处理,请将 DispatcherSynchronizationBehavior 终结点行为添加到服务主机,并将其 AsynchronousSendEnabled 属性设置为 true
。 我们还在发送端添加了真实异步流式传输的能力。 这提高了服务的可伸缩性,在将消息流式传输到多个客户端的情况下,某些客户端可能由于网络拥塞或根本不读取而读取速度缓慢。 在这些方案中,我们现在不针对每个客户端对服务阻止各个线程。 这可确保服务能够处理更多客户端,从而提高服务的可伸缩性。
流传输编程模型
流式传输的编程模型非常简单。 若要接收流式数据,请指定具有单个 Stream 类型化输入参数的操作契约。 若要返回流式传输的数据,请返回 Stream 引用。
[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
public interface IStreamedService
{
[OperationContract]
Stream Echo(Stream data);
[OperationContract]
Stream RequestInfo(string query);
[OperationContract(OneWay=true)]
void ProvideInfo(Stream data);
}
前面的示例中的操作 Echo
接收并返回一个流,因此应在 Streamed 绑定上使用。 对于操作 RequestInfo
,StreamedResponse 最为合适,因为它只返回一个 Stream。 单向操作最适合于 StreamedRequest。
请注意,将第二个参数添加到以下 Echo
或 ProvideInfo
操作会导致服务模型转回到缓冲策略,并使用流的运行时序列化表示形式。 只有具有单个输入流参数的操作才能与端到端请求流式处理兼容。
此规则同样适用于邮件协定。 如下面的消息协定所示,在流模式的消息协定中,只能有一个正文成员。 如果要与流通信其他信息,则必须在消息标头中传递此信息。 消息正文是专为流内容保留的。
[MessageContract]
public class UploadStreamMessage
{
[MessageHeader]
public string appRef;
[MessageBodyMember]
public Stream data;
}
流式传输结束,当流到达文件结尾(EOF)时,消息将关闭。 发送消息(返回一个值或调用一个操作)时,可以传递 FileStream,随后 WCF 基础结构会从该流中拉取所有数据,直到流已完全读取并到达 EOF。 要为不存在此类预置 Stream 派生类的源传输流数据,请构造这样一个类,用该类覆盖流源,并将其用作自变量或返回值。
接收消息时,WCF 会在 Base64 编码消息正文内容上构造一个流(如果使用 MTOM,则构造相应的 MIME 部分),当读取完内容时流到达 EOF。
传输级流式处理也适用于任何其他消息协定类型(参数列表、数据协定参数和显式消息协定),但由于此类类型化消息的序列化和反序列化需要序列化程序缓冲,因此不建议使用此类协定变体。
大数据的特殊安全注意事项
所有绑定都允许限制传入消息的大小,以防止拒绝服务攻击。 例如,公开BasicHttpBinding一个 System.ServiceModel.BasicHttpBinding.MaxReceivedMessageSize 属性,该属性绑定传入消息的大小,因此也会限制处理消息时访问的最大内存量。 此单位以字节为单位设置,默认值为 65,536 字节。
大型数据流情形所特有的安全威胁会在接收方希望数据以流模式发送时导致数据缓冲,从而促使拒绝服务。 例如,WCF 始终缓冲消息的 SOAP 标头,因此攻击者可能会构造一条由完全包含标头组成的大型恶意消息,以强制缓冲数据。 启用流式处理后,MaxReceivedMessageSize
可能会被设置为非常大的值,因为接收方从不期望整个消息会同时缓存在内存中。 如果 WCF 被迫缓冲消息,则会发生内存溢出。
因此,在这种情况下,限制最大传入消息大小是不够的。 要限制 WCF 缓冲的内存量,必须使用 MaxBufferSize
属性。 请务必将此值设置为安全值(或在流式处理时将其保留为默认值)。 例如,假设服务必须接收大小高达 4 GB 的文件,并将其存储在本地磁盘上。 假设你的内存有限,以至于一次只能缓冲 64 KB 的数据。 然后将设置为 MaxReceivedMessageSize
4 GB 和 MaxBufferSize
64 KB。 此外,在服务实现中,必须确保仅以 64 KB 为单位从传入流中读取,并且在先前的区块被写入磁盘并从内存中清除之前,不要读取下一个区块。
此外,请务必了解,此配额仅限制 WCF 执行的缓冲,并且无法保护你在自己的服务或客户端实现中执行的任何缓冲。 有关其他安全注意事项的详细信息,请参阅 数据的安全注意事项。
注释
使用缓冲传输或流传输的决定是终结点的本地决策。 对于 HTTP 传输,传输模式不会跨连接传播,也不会传播到代理服务器和其他中介。 设置传输模式不会反映在服务接口的说明中。 将 WCF 客户端生成到服务后,必须编辑要用于流传输的服务的配置文件以设置模式。 对于 TCP 和命名管道传输协议,该传输模式将作为策略断言传播。
另请参阅
- 如何启用流媒体