WCF 4.0 进阶系列 – 第十三章以更好的性能实现WCF服务(下)

启用MTOM传输数据

MTOM 是一种机制,用来以原始字节形式传输包含 SOAP 消息的较大二进制附件,从而使所传输的消息较小。一条SOAP消息经常由消息头和消息体构成;消息头提供地址,路由信息和安全信息;消息体提供数据,以及消息的负载。消息体由XML构成,包含在传输至服务的请求或者返回客户端程序的响应信息中。消息体信息的真正结构是由WSDL对操作的描述而指定,这些操作由你在服务中指定的操作合约衍生而来。举例来说,你在本书第一章创建的ProductService服务,在IProductsService服务合约中定义了ChangeStockLevel操作:
当客户端程序调用ChangeStockLevel操作,WCF运行时创建一条如下的消息:
你可以看到消息体包含了操作所需的参数,这些参数被编码为XML信息集合。这种数据结构使得参数以一种非常直观的方式呈现。但是,请记住,当XML消息在网络中传输时,它会被转换成一系列的文本字符,而非文本数据。比如上面例子中的<newStockLevel>和<bin>元素,在客户端发送时必须转化为文本,在被服务接收到时必须转化为数字。这种转化在两个层面增加了系统开销:
1. 把二进制格式的整数转化文本和从文本转化回整数会消耗时间,内存和计算能力。
2. 当数据被转换为文本后在网络中传输时可能没有原始的数据类型精简;原始数据的值越大,那么转换为文本后需要更多的文本字符。
在上面的例子中,增加的系统开销是微弱的。但是,你应该如何处理二进制数据,比如一个图片类型的数据。一种可能的解决方案是转化二进制格式为仅包含对应的0和1这两个字符的文本格式。但是考虑到这种方式增加的系统开销。转化一兆的二进制数据将包含一百万个字符的字符串,这要求大量的内存和时间。如果WCF转化二进制数据为一个Base64位编码的字符串而非0和1这两个字符构成的字符串文本,那么会发生什么?结果是转化为Base64编码的字符串后,文本数据更精简。但是,平均起来,Base64编码机制的字符串比原始的字符串长度增加40%。此外,接收方接收到数据后还必须转会数据原始的类型。很明显,当需要传输大的二进制数据消息时,我们需要寻找另外的解决方案。
MTOM规范就是另外一种解决方案。当你使用MTOM传输包含二进制数据的消息时,二进制数据不会编码为文本,而是其作为一个附件,并以双方均知晓的MIME(多功能Internet邮件扩充规范)规定的格式传输至接收方。原始消息中的所有文本信息都被编码为XML信息集合,但二进制信息被作为MIME附件的引用。下图展示了MTOM如何编码二进制数据:
像往常一样,是要MTOM时也必须考虑安全因素。当签名MTOM编码的消息,WCF计算的签名包含了消息的所有MIME附件。如果消息的任何一部分,包括MIME附件,在发送和接收的时候改变了,那么消息的签名就会失效。更多关于签名的详细信息,请回顾第四章"保护企业内部的WCF服务"。
在WCF中,MTOM由一个特定的编码通道处理。如果你使用标准的HTTP绑定(basicHttpBinding, wsDualHttpBinding, wsFederationHttpBinding, 或ws2007HttpBinding),那么你可以在消息编码配置中更改消息编码属性为MTOM。而其他的传输协议,比如TCP,MSMQ和命名管道,默认情况下,它们使用自己特有的二进制编码;其对应的标准绑定没有MessageEncoding属性,但如果你希望在这些绑定上启用MTOM编码消息,那么你必须创建自定义的绑定。

向客户端程序发送二进制数据对象

考虑下面的场景:AdventureWorks希望扩展WCF服务ShoppingCartService,以使用户可以查看公司产品图片。产品图片以二进制方式存贮在数据库中。开发人员已经创建了ShoppingCartPhotoService的服务原型,该服务提供了GetPhoto的操作,该操作从数据库获取图像然后返回给客户端程序。在下面的练习中,你将检查该项目,并学习如何配置该服务使其使用MTOM编码消息。
练习:检查ShoppingCartPhotoService
1. 启动Visual Studio,打开*\WCF\Step.by.Step\Solutions\Chapter13\MTOM文件夹下的MTOMService解决方案。
该方案包含一个名为ShoppingCartPhotoService的服务原型,该服务实现了GetPhoto操作;并且该服务由ASP.NET 开发Web服务器提供宿主。该解决方案还包含一个简单的WPF客户端程序。
2. 打开C:\...\MTOMService项目中App_Code文件夹下的IShoppingCartPhotoService.cs文件。
检查定义服务合约的IShoppingCartPhotoService接口。该接口包含GetPhoto操作,其允许客户端程序发送一个产品编码从而请求一个产品图片。该服务获取图片后将该图片作为返回参数返回至客户端程序。因为图片以二进制格式存贮在数据库中,所以返回参数的类型为byte[]。最后,该操作的返回值为bool值,以指定操作成功还是失败。
3. 继续检查App_Code文件夹下的ShoppingCartPhotoService.cs文件,通过下图你可以看到服务实现类是如何实现GetPhoto方法的。
该方法并未包含任何与WCF相关的东西;它通过ProductsPhotoModel实体执行一个LINQ查存;然后从数据库的ProductPhoto表中根据指定的产品编码获取对应产品的图片;最后该查询返回一个ProductPhoto对象。
图片信息保存在ProductPhoto表中的LargePhoto列中,该列的数据类型为varbinary。实体框架获取该varbinary数据并填充至LINQ查询的返回对象PruductPhoto的LargePhoto属性上(该属性的数据类型为byte[])。该对象的LargePhoto属性的值赋给输出参数photo。如果从数据库找到产品并获取到图片,那么该方法返回true;如果发生异常则返回false。
4. 打开ShoppingCartGUIClient程序中的ClientWindow.xaml文件。该XAML文件定义了一个WPF窗口,其包含一个图片控件,该控件占据了整个窗口的大部分。此外,该窗口还包含一个文本标签,一个文本输入框和一个按钮控件。用户可以在文本框内输入产品编码,然后点击按钮获取产品图片。
5. 打开ClientWindows.xaml.cs文件。你可以看到当用户点击获取图片按钮之后,将运行getPhoto_Click方法。
上述方法创建一个客户端代理实例,读取用户输入的产品编码,然后创建一个新的byte数组,然后调用GetPhoto操作,并将byte数组和产品编码作为参数传递给操作。如果操作返回true,那么方法将使用包含产品图片的byte数组去填充BitmapImage对象,该对象负责显示产品图片。
6. 在非调适模式下运行解决方案。ShoppingCartPhotoService启动ASP.NET开发Web服务器并在9080端口侦听客户端的请求。
当ShoppingCartGUI客户端窗口出现,在产品编号文本框内输入WB-H098,然后点击获取照片按钮。将显示一张包含两个水瓶的照片。如下图所示:
7. 关闭ShoppingCartGUI客户端窗口,然后返回到Visual Studio。
8. C:\...\MTOMService项目中选中web.config,然后点击右键,选择使用WCF服务配置管理工具。这将启动WCF服务配置管理工具编辑web.config文件。
9. 在配置面板,点击诊断文件夹。然后在诊断面板,点击启动消息日志。
10. 在配置面板,点击诊断文件夹下的消息日志节点。在消息日志面板,设置LogEntireMessage属性为true。
11. 在配置面板,展开侦听文件夹,然后点击ServiceModelMessageLoggingListener节点。在右边面板中,设置InitData属性为web_messages.svclog,该文件位于*\WCF\Step.by.Step\Solutions\Chapter13\文件夹下。
12. 在配置面板,展开诊断文件夹下的源节点,然后点击ServiceModelMessageLogging节点。在右边面板中,设置追踪级别为Verbose(提供详细的输出信息)。
13. 在配置面板,再次展开诊断文件夹。在诊断面板,点击启动追踪。使用该选项后,你可以捕获WCF运行时生成发送和接收消息的行为信息。
14. 在配置面板,点击侦听文件夹下的ServiceModelTraceListener节点。在右边面板中,设置InitData属性为web_trace.svclog,该文件位于*\WCF\Step.by.Step\Solutions\Chapter13\文件夹下。
15. 在配置面板,点击源文件夹下System.ServiceModel节点。在右边面板,设置追踪级别属性为Verbose。
16. 保存配置文件。
17. 回到Visual Studio中,在非调试模式下运行解决方案。在ShoppingCartGUI客户端程序中,在产品编码文本框内分别输入WB-H098和PU-M044,你将会分别得到两张产品的图片。然后关闭客户端程序,并停止ASP.NET开发Web服务器。
18. 启动服务追踪查看器。在服务追踪查看器中,打开*\WCF\Step.by.Step\Solutions\Chapter13\文件夹下的web_messages.svclog
19. 在右边面板中,点击消息标签。你将看到列出了4条消息:两条请求消息和两条响应消息(因为ShoppingCartPhotoService服务配置实用basicHttpBinding绑定,所以没有生产额外的消息;比如安全凭证或者建立可靠消息会话的消息,等等)。
20. 点击第一条消息,在右下面板中,点击消息标签,然后滚动到消息的底部。你将看到该消息请求产品WB-H098的产品图片。
21. 在左边面板,点击第二条消息。然后在右下面板,点击"消息"标签。这是一条将图片数据的包含在<photo>元素中响应消息。你可以看到该数据包含一个长字符串,其为Base64编码的二进制数据。检查余下的消息,第三条消息请求产品PU-M044的产品图片,第四条响应消息包含Base64编码的图片数据。
22. 打开*\WCF\Step.by.Step\Solutions\Chapter13\文件夹下的web-tracelog.svclog文件。在左边面板中点击"活动"标签。第一个文件包含一条由WCF运行时产生的日志,并且活动面板显示一系列的由服务端的WCF运行时执行的任务。
23. 找到并点击第一条名为"Process action 'http://adventure-works.com/2010/07/01/ShoppingCartPhotoService/GetPhoto'"的活动,右上的面板将显示由该活动所执行的任务,其包含从通道接收消息,打开服务实例,执行操作,创建响应消息,最后关闭服务实例。
24. 在右上面板中,向下滚动任务列表,然后选中"A Message was written"。右下面板将显示该消息的相关信息。在消息属性的头部区域,请注意编码器属性值为text/xml;charset=utf-8。这表明消息使用文本编码:
25. 在服务追踪查看器的菜单中,点击文件,然后选择关闭所有文件。
 
上述练习为你演示了,在默认情况下,ShoppingCartPhotoService服务发送的所有消息—包括含有大量二进制数据的消息—均使用文本编码和传输。对于包含二进制图片的消息,如果ShoppingCartPhotoService服务中包含图片的消息使用MTOM编码将更有效。
在下面的练习中,你将学习如何修改ShoppingCartService服务的绑定配置以使用MTOM编码二进制数据,然后使用HTTP协议从服务端向客户端传输编码后的数据。
练习:配置ShoppingCartPhotoService服务以启动MTOM
1. 返回到WCF服务配置编辑器,该编辑器当前显示ShoppingCartPhotoService服务的web.config文件
2. 在左边面板,点击绑定文件夹。在绑定面板,点击创建新的绑定配置。为basicHttpBinding添加一个新的绑定配置。该绑定的名字为ShoppingCartPhotoServiceBasicHttpBindingConfig,并修改MessageEnconding属性的值为Mtom。
3. 在配置面板,展开服务文件夹下的ShoppingCartPhotoService.ShoppingCartPhotoServiceImpl节点,然后展开端点文件夹,然后点击未命名服务端点。在服务端点面板,设置BindingConfiguration属性为ShoppingCartPhotoServiceBasicHttpBindingConfig。
4. 保存配置文件,然后关闭WCF服务配置编辑器。
5. 在Visual Studio中,使用WCF服务配置编辑器打开ShoppingCartGUIClient项目下的app.config文件。
6. 在配置面板中,展开绑定文件夹,然后点击BasicHttpBinding_ShoppingCartPhotoService绑定配置。客户端端点使用该绑定的配置;该绑定配置在使用添加服务向导时自动生成。
7. 在右边面板中,设置绑定BasicHttpBinding_ShoppingCartPhotoService的MessageEncoding属性为Mtom以匹配服务绑定的配置。
8. 保存配置文件。
9. 使用Windows文件浏览器,删除*\WCF\Step.by.Step\Solutions\Chapter13文件夹下的web_messages.svclog和web_tracelog.svclog文件。
10. 回到Visual Studio中,在非调适模式下运行解决方案。分别输入产品编号WB-H098和PU-M044,并确认这两个产品对于的图片在客户端窗口中显示。然后关闭客户端程序和停止ASP.NET开发Web服务器。
11. 在WCF服务追踪查看器中,打开web_tracelog.svclog文件。在活动标签,找到并选择名为"Process Action 'http://adventure-works.com/2010/07/01/ShoppingCartPhotoService/GetPhoto'"。然后在右上面板中,找到并点击任务"A message was written"。在右下面板中,检查消息属性的头部区域的Encoder属性。这次编码器属性的值为multipart/related;type='application/xop+xml'。这表明服务现在使用使用MTOM编码的MIME多部分消息传输数据。
12. 关闭WCF服务追踪查看器。
 

控制消息大小

你已经看到了配置绑定使用MTOM编码是一项相当简单的任务。启用MTOM不会影响你程序的功能,而且你不必编写任何特别的编码以更改绑定使用MTOM编码。但是,你应该注意到刚才使用MTOM的环境是一个非常适合使用MTOM编码的环境。发送和接收包含大二进制数据消息的服务容易引起DOS攻击;如果一个服务启用MTOM,那么攻击者可能试图发送一些非常大的消息以试图使服务"交通阻塞"。同样地,一个恶意的服务也可能对客户端请求返回大量的响应消息以试图中断用户计算机。因此,WCF运行时允许你限制客户端和服务可接收消息的大小。
在下面的练习中,你将检查当尝试接受一些超出WCF运行时允许的大小的数据时将发生什么情况,并且学习如果配置绑定以使其支持较大的消息。
练习:在客户端接收大消息
1. 在Visual Studio中,在非调适模式下运行解决方案。在ShoppingCartClientGUI程序中,在产品编码出输入BK-M38S-46,然后点击Get Photo按钮。ShoppingCartClientGUI程序将显示下面的异常消息对话框:
异常产生的原因是因为默认情况下,WCF运行时只允许在消息中的数组长度为16384字节(16Kb)。在AdventureWorks数据库中,Mountain-400-W自行车产品的图片大小超过了16Kb。WCF运行时允许服务发送数据(服务可以发送的数据不受限制),但阻止客户端程序接收该数据。
2. 在消息对话框中,点击确认按钮,然后关闭ShoppingCartGUI客户端窗口。
3. 返回到WCF服务配置编辑器,打开的是ShoppingCartGUIClient项目下的app.config。在控制面板中,点击绑定文件夹下的BasicHttpBinding_ShoppingCartPhotoService绑定。
4. 在右边面板,在绑定区域向下滚动内容,找到ReaderQuota属性。请注意该熟悉当前的值为16384。更改其为32768.更改的结果是WCF运行时将允许客户端接收的消息所包含的数组最大为32Kb。保存配置文件,并退出WCF服务配置编辑器。
5. 再次在非调试模式下运行解决方案。在客户端程序的产品编码处,输入BK-M38S-46。这次产品图片将成功的显示。
6. 关闭客户端程序窗口。
 
除了MaxArrayLength属性之外,ReaderQuotas元素还包括所有绑定都具有的其他几个属性:MaxBytesPerRead,MaxDepth,MaxNameTableCharCount和MaxStringContentLength。这些属性确定了WCF运行时在处理消息并抛出异常前的复杂性。消息体所包含的数据是一个XML文档;在内部,WCF运行时使用XmlDictionaryReader对象转化消息体的内容并将这些内容分割成适当的数据,然后把这些数据以其所期望的值返回给服务或客户端程序。WCF运行时使用ReaderQuota属性配置XmlDictionaryReader对象并限制该对象可以处理的消息大小和结构。MaxBytesPerRead属性指定当XmlDictionaryReader处理消息时,一次可以从消息中读取多少字节的数据。MaxDeptch属性指定消息中元素节点的最大深度。MaxNameTableCharCount属性限制XmlDictionaryReader的NameTable中原子化的字符串中的总字符数。(原子化字符串时,会将字符串插入 NameTable 且永远不会移除。 这会导致在 NameTable 中累积大量字符数据。 因此需要对XmlDictionaryReader的 NameTable 中可以缓冲的数据量设置限制)。MaxStringContentLength限制由各种 API 创建和返回的字符串的长度。如果你设置这些属性的值为0,那么XmlDictionaryReader将使用它们的默认值。
ReaderQuotas属性中,你最可能修改的就是MaxArrayLenght属性和MaxStringContentLenght属性,因为这两个属性直接关系到发送消息所包含数据的大小。
但是,这事到此还没结束。一条消息可能含有多个数组或者字符串。ReaderQuotas的属性仅仅限制单个元素的大小,而不是一条消息中所有元素的大小;因此,DOS攻击仍然可能发生。如果你在WCF服务配置编辑器中检查一个绑定配置,你将发现绑定还有一个MaxReceivedMessageSize属性;此属性管理能接收消息的总大小。请注意,该属性的值包含在SOAP消息的头部中或者其他管理信息中(如果帮定没有使用SOAP)。
该属性的默认值大小为65536bytes(65Kb),这对足以适应大多数情况,但如果有必要你可以增加该属性的值。你可以设定的最大值为2,147,483,647(Int32的最大值)。如果没有足够充分的理由,请不要设置改最大值。相反,你应该确保该值至少比MaxArrayLength和MaxStringContentLength大一些。如果改值较小,那么改值将破坏ReaderQuotas属性规定的限制。
还有另外一个属性MaxBufferSize值得关注。当一条消息传输时,它在网络中以字节流传输。当接收到一条消息,流必须重新组建为消息。WCF运行时使用内存缓冲来完成该任务,内存所分配的最大缓存由该属性MaxBufferSize决定。在大多数情况下,MaxBufferSize与MaxReceivedMessageSize是相同的。但是,当你使用流模式后,MexBufferSize的大小将会明显减小。
 

WCF服务中启用流模式传输大数据

MTOM对于编码消息中的大二进制数据对象是非常有用的,但是如果这些对象变得十分巨大,那么它们将消耗寄宿WCF服务的计算机和接收消息客户端的计算机上相当数量的内存。此外,创建和传输巨大的消息都需要花费较长的时间,而且客户端在等待包含大二进制对象的响应消息时,可能发生超时。
在多数情况下,尝试将数据打包到一条单独的消息中是没有意义的。假如一个WCF服务的操作输出音频和视频数据。在这种情况下,比起只用一个"大数据块(所有的数据都放在同一条消息的某一个标签中)"来传输数据,启动流模式发送和接收数据将更有效。流模式允许客户端程序在服务传输完整条消息之前就开始接收和处理数据,这样服务和客户端均不需要大缓存来容纳整条消息,同时还解决了超时的问题。

在WCF服务和客户端程序中启动流模式

若要WCF的操作支持流,你只需要修改基于basicHttpBinding, netTcpBinding, netNamedPipeBinding绑定对应的绑定配置的TransferMode属性。你可以设置该属性的值为:
  • Buffered,这是TransferMode属性的默认值。在内存中整个消息创建完成后,然后开始传输。
  • StreamedRequest, 以流模式发送请求消息,以缓冲方式接受和返回响应。
  • StreamedResponse,以缓冲方式发送和接收请求,但以流模式返回响应。
  • Streamed,两个方向均使用流模式发送/接收请求/响应。
启用流模式后,可接收消息的最大值仍由绑定的MaxReceivedMessageSize属性决定。但是,当你对绑定启动流模式后,仅仅消息头需要在接收方缓冲,因此你应该减少MaxBufferSize属性的值。

设计操作以支持流模式

如果启用流模式,除了更改TransferMode属性的值之外,还有其他更多的工作,并且不是所有的操作都可以利用流模式。为了支持以流模式发送请求,一个操作仅仅能接收一个输入参数,而且该参数要么是一个流对象,要么是一个消息对象或者能够被序列化为XML的对象。为了支持以流模式返回响应,一个操作要么没有任何返回类型,要么通过一个输出参数返回;与输入参数一样,输出参数也必须是一个流对象或者能序列化为XML的对象。为什么有这个限制呢?这是因为输入参数(或者输出参数)必须可以构成一条完整的输入消息或者输出消息。
练习:在ShoppingCartPhotoService中实现流模式返回响应消息
1. 打开C:\...\MTOMService项目中App_Code文件夹下的IShoppingCartPhotoService.cs文件,添加如下的一个新版本的获取产品图片的操作,其用于支持流模式:
2. 打开App_Code文件夹中的服务实现类ShoppingCartPhotoService.cs文件,添加一个新的GetPhoto方法,其返回一个MemoryStream对象,该对象包含图片数据:
3. 使用WCF服务配置编辑器修改ShoppingCartPhotoServcie服务和ShoppingCartGUIClient程序的的绑定配置,修改两者的TransferMode为StreamResponse以使服务支持流模式返回响应消息:
请注意,ShoppingCartGUI程序的绑定设置也必须设置为StreamedResponse;否则,客户端的WCF运行时会在消息传递给客户端程序之前,把接收的响应消息缓冲起来。
4. 在ShoppingCartGUIClient中更新服务引用,然后修改getPhoto_Click方法:
5. 删除*\WCF\Step.by.Step\Solutions\Chapter13文件夹下的web_messages.svclog和web_tracelog.svclog文件。
6. 在非调适模式下运行解决方案,然后客户端产品编码处输入WB-H098,如果一切正常,客户端窗口将显示该产品的图片。
7. 关闭客户端程序,并停止ASP.NET开发Web服务器。
8. 启动WCF服务追踪查看工具,在左边面板中点击消息标签,然后点击第一个动作,在右上的面板中,选择第二条消息,然后在右下点击消息标签,你将看到该消息的消息体部分如下图所示:

启动流模式后需考虑的安全事项

消息层面的安全特性,比如签名和加密在ws2007HttpBinding绑定中广泛使用,并且它们要求WCF运行时可以访问整个消息。当你在绑定上启动流模式后,WCF运行时将不再可能访问整个消息。因为这个原因,ws2007HttpBinding和其他相关的绑定都不支持流模式。解决方法是使用在传输级别的实现安全basicHttpBinding绑定。
此外,你还不能运用可靠的消息传输。因为该特性依赖于缓冲,以使协议能知道整个消息的传递情况(如果在绑定配置中设置了排序,还会对消息进行排序)。同样地,这对于ws2007HttpBinding系列绑定又成为一个问题。这是因为TCP传输协议和命名管道自身提供了可靠的传递机制,它们独立于WCF实现的WS-ReliableMessaing协议。
安全事项的最后一个关注点,在默认情况下WCF创建的绑定允许接收最大为64Kb大小的消息。如果一条正在接收的消息超出了该限制,WCF运行时将抛出一个异常并终止操作。正如早期提到的,该限制主要是减少DOS攻击的范围。该值对大多数面向消息的操作已经足够,但对于多数支持流模式的场景而言该值太低。在这些场景下,你需要增加绑定配置中MaxReceivedMessageSize属性的值。但是,该属性是对应绑定的一个全局变量,因此它将会影响该绑定上暴露给客户端的所有操作;因此最好为不支持流模式的操作定义一个单独的服务合约。
更多关于大型数据和流的信息,请参考MSDN http://msdn.microsoft.com/zh-cn/library/ms733742.aspx。
 
 
posted @ 2011-08-25 17:11  On the road....  阅读(2522)  评论(0编辑  收藏  举报