WCF技术剖析之十九:深度剖析消息编码(Encoding)实现(上篇)

时间:2022-04-22
本文章向大家介绍WCF技术剖析之十九:深度剖析消息编码(Encoding)实现(上篇),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

消息作为WCF进行通信的唯一媒介,最终需要通过写入传输层进行传递。而对消息进行传输的一个前提或者是一项必不可少的工作是对消息进行相应的编码。WCF提供了一系列可供选择的编码方式,它们分别在互操作和性能各具优势。在本篇文章我们将对各种编码方式进行消息的讨论。

从互操作性的角度来看,编码方法很大程度上决定了跨平台支持的能力。有的编码方式是平台无关的,有的则仅限于某种特定的平台。WCF提供了3种典型的编码方式:Binary、Text和MTOM。Binrary以二进制的方式进行消息的编码,但是仅限于.NET平台之间的通信;Text则提供平台无关的基于文本的编码方式。MTOM编码基于WS-MTOM规范,对于改善大规模二进制数据在SOAP消息的传输性能具有重大的意义,既然该编码方式遵循相应的规范,无疑这也是一种跨平台的编码方式。

在正式介绍WCF消息编码之前,我们很有必要了解如下几个实现编码的核心对象:XmlDictionary、XmlDictionary和XmlDIctionaryWriter。

一、XmlDictionary

XmlDictionary,顾名思义,它是一个字典,它是从事编码和解码双方共享的一份“词汇表”。这样的说法可能有点抽象,我们不妨做一个类比。比如我说“WCF是.NET平台下基于SOA的消息通信框架”,对于各位读者来说,这句话很好理解。如果我向另一个对计算机一窍不通的人说这句话,毫无疑问,对方是无论如何不能理解的。读者和我之间之所以能够通过这样的语言进行交流,是因为我们之间具有相似的知识背景,在我们之间共享相同的词汇表,对每个单词的含义具有一致的理解。而别人不能理解,是在于我和他之间的信息不对称,如果要使它能都理解,我必须用他所能理解的方式进行交流,在这种情形之下,我可能要花很多文字对这句话的一些术语进行详细的解释,比如什么是.NET平台,什么是SOA,什么又是通信框架。所以,交流的前提是双方具有相同的“词汇表”,双方就某个主题共享越多的“词汇”,交流就越容易,你说的话将越简洁。

数据的编码也像我们日常的沟通和交流一样,编码的一方是“说”的一方,解码的一方是“听”的一方。说的一方按照它所掌握的“词汇表”对信息进行编码,对方只有具有相同的“词汇表”才能正常地解码。如果这个“词汇表”越详尽,编码后的内容容量就越小。内容的浓缩意味着什么?意味着网络流量的减少,意味着为你节省更多的带宽。而XmlDictionary就是这样的一个词汇表。

XmlDictionary定义在System.Xml命名空间下,它是System.Xml.XmlDictionaryString的集合。XmlDictionaryString相当于一个KeyValuePair<int,string>对象,是一个键-值对,键和值的类型为int和string。下面是XmlDictionaryString和XmlDictionary的定义。

   1: public class XmlDictionaryString
   2: {
   3:     //其他成员
   4:     public XmlDictionaryString(IXmlDictionary dictionary, string value, int key);    
   5:     public IXmlDictionary Dictionary { get; }
   6:     public static XmlDictionaryString Empty { get; }
   7:     public int Key { get; }
   8:     public string Value { get; }  
   9:  
  10: }
   1: public class XmlDictionary : IXmlDictionary
   2: {
   3:     
   4:     //其他成员
   5:     public XmlDictionary();
   6:     public XmlDictionary(int capacity);
   7:     public virtual XmlDictionaryString Add(string value);
   8:  
   9:     public virtual bool TryLookup(int key, out XmlDictionaryString result);
  10:     public virtual bool TryLookup(string value, out XmlDictionaryString result);
  11:     public virtual bool TryLookup(XmlDictionaryString value, out XmlDictionaryString result);
  12: }

通过下面的代码,创建了一个XmlDictionary对象,通过Add方法添加了3个XmlDictionaryString。严格说来XmlDictionary并不是一个集合对象,因为它没有实现IEnumerable接口。通过Add方法你只能指定XmlDictionaryString的Value,Key的值会以自增长的方式自动赋上。所以Customer、Name、Company 3个元素的Key分别为0,1,2,这可以从最终输出结果中看出来。

   1: IList<XmlDictionaryString> dictionaryStringList = new List<XmlDictionaryString>();
   2: XmlDictionary dictionary = new XmlDictionary();
   3: dictionaryStringList.Add(dictionary.Add("Customer"));
   4: dictionaryStringList.Add(dictionary.Add("Name"));
   5: dictionaryStringList.Add(dictionary.Add("Company"));
   6: foreach (XmlDictionaryString dictionaryString in dictionaryStringList)
   7: {
   8:     Console.WriteLine("Key:{0}tValue:{1}", dictionaryString.Key, dictionaryString.Value);
   9: }

输出结果:

   1: Key:0    Value:Customer
   2: Key:1    Value:Name
   3: Key:2    Value:Company

二、XmlDictionaryWriter

System.Xml.XmlDictionaryWriter和后面介绍的System.Xml.XmlDictionaryReader,在WCF编码(解码)过程中具有举足轻重的地位,因为最终的编码和解码工作分别落在这个两个类上面。XmlDictionaryWriter将XML InfoSet进行编码写入到流中,而XmlDictionaryReader将数据从流中读出并进行解码,生成相应的XML InfoSet。

XmlDictionaryWriter是一个继承自System.Xml.XmlWriter的抽象类,WCF中定义了一系列具体的XmlDictionaryWriter,它们直接或者间接地继承自XmlDictionaryWriter,为编码和解码提供了不同的实现。典型的XmlDictionaryWriter包括以下3个:

  • XmlUTF8TextWriter:提供基于文本的编码和解码实现;
  • XmlBinaryWriter:提供基于二进制的编码和解码实现;
  • XmlMtomWriter:提供基于MTOM(Message Transmission Optimized Mechanism)的编码和解码实现。

上面3个类型定义在System.Runtime.Serialization 程序集的internal类型,所以不通直接使用。XmlDictionaryWriter定义了一系列的工厂方法以方便开发者创建这些对象。其中上面3种类型XmlDictionaryWriter对应的工厂方法分别为:CreateTextWriter、CreateBinaryWriter和CreateMtomWriter。

   1: public abstract class XmlDictionaryWriter : XmlWriter
   2: {
   3:     //其他成员
   4:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream);
   5:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary);
   6:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary, XmlBinaryWriterSession session);
   7:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary, XmlBinaryWriterSession session, bool ownsStream);
   8:  
   9:     public static XmlDictionaryWriter CreateDictionaryWriter(XmlWriter writer);
  10:  
  11:     public static XmlDictionaryWriter CreateMtomWriter(Stream stream, Encoding encoding, int maxSizeInBytes, string startInfo);
  12:     
  13:     public static XmlDictionaryWriter CreateMtomWriter(Stream stream, Encoding encoding, int maxSizeInBytes, string startInfo, string boundary, string startUri, bool writeMessageHeaders, bool ownsStream);
  14:     public static XmlDictionaryWriter CreateTextWriter(Stream stream);
  15:     public static XmlDictionaryWriter CreateTextWriter(Stream stream, Encoding encoding);
  16:     public static XmlDictionaryWriter CreateTextWriter(Stream stream, Encoding encoding, bool ownsStream);
  17: }

这3种类型的XmlDictionaryWriter代表了WCF目前支持的3种典型的消息编码方式:Text、Binary和MTOM。接下来,我们将通过一个个具体的例子,来比较这3种不同的XmlDictionaryWriter经过编码后,产生的内容到底有何不同。

1、XmlUTF8TextWriter(CreateTextWriter)

由于基于纯文本的编码是平台无关的,故而能够为不同的厂商所支持,这和SOA跨平台的互操作的主张一致,所以基于文本的编码是最为常用的编码方式。WCF的BasicHttpBinding、WsHttpBinding以及WsDualHttpBinding都采用基于文本的编码。在WCF中,所有基于文本的编码工作最终都落在XmlUTF8TextWriter上面,由于该类是一个内部类型,我们只能通过XmlDictionaryWriter提供的3个静态工厂方法CreateTextWriter来创建XmlUTF8TextWriter对象。CreateTextWriter方法的参数stream便是经过编码的二进制数组需要写入的流;encoding表明采用的字符编码方式,在这里只有两种类型的字符编码是支持的:UTF8和Unicode,这从XmlUTF8TextWriter的命名就可以看出来;至于ownsStream,表明XmlUTF8TextWriter对象是否拥有对应的stream对象,如果是true,则表明XmlUTF8TextWriter是stream的拥有者,XmlUTF8TextWriter关闭将伴随着stream的关闭,默认为true。

   1: public abstract class XmlDictionaryWriter : XmlWriter
   2: {
   3:     //其他成员
   4:     public static XmlDictionaryWriter CreateTextWriter(Stream stream);
   5:     public static XmlDictionaryWriter CreateTextWriter(Stream stream, Encoding encoding);
   6:     public static XmlDictionaryWriter CreateTextWriter(Stream stream, Encoding encoding, bool ownsStream);
   7: }

下面是一个简单地使用XmlUTF8TextWriter进行编码的例子。在这里我使用XmlDictionary的CreateTextWriter方法创建XmlUTF8TextWriter对象,对一个简单的XML文档(文档中仅仅具有一个XML元素)进行编码,然后输出经过编码后的字节长度、二进制表示和以文本显示的文档内容。代码后面是真实的输出。

   1: MemoryStream stream = new MemoryStream();
   2: using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream,Encoding.UTF8))
   3: {
   4:     writer.WriteStartDocument();
   5:     writer.WriteElementString("Customer", "http://www.artech.com/", "Foo");
   6:     writer.Flush(); 
   7:  
   8:     long count = stream.Position;
   9:     byte[] bytes = stream.ToArray();
  10:     StreamReader reader = new StreamReader(stream);
  11:     stream.Position = 0;
  12:     string content = reader.ReadToEnd(); 
  13:  
  14:     Console.WriteLine("字节数为:{0}", count);
  15:     Console.WriteLine("编码后的二进制表示为:n{0}", BitConverter.ToString(bytes));
  16:     Console.WriteLine("编码后的文本表示为:n{0}", content);
  17: }

输出结果:

字节数为:93
编码后的二进制表示为:
3C-3F-78-6D-6C-20-76-65-72-73-69-6F-6E-3D-22-31-2E-30-22-20-65-6E-63-6F-64-69-6E-67-3D-22-75-74-66-2D-38-22-3F-3E-3C-43-75-73-74-6F-6D-65-72-20-78-6D-6C-6E-73-3D-22-68-74-74-70-3A-2F-2F-77-77-77-2E-61-72-74-65-63-68-2E-63-6F-6D-2F-22-3E-46-6F-6F-3C-2F-43-75-73-74-6F-6D-65-72-3E
编码后的文本表示为:
<?xml version="1.0" encoding="utf-8"?><Customer xmlns="http://www.artech.com/">Foo</Customer>

2、XmlBinaryWriter(CreateBinraryWriter)

XmlBinraryWriter通过二进制的方式进行编码,所以它能够极大地减少编码后字节的大小,在进行网络传输的时候能够极大地节约网络带宽,获得最好的传输性能。但是,这种形式的编码并不具备跨平台的特性,仅限于客户端和服务端采用WCF的应用场景。

为了演示通过XmlBinaryWriter进行编码,我将上面的代码略加改动:通过调用CreateBinaryWriter创建XmlBinaryWriter对象。从最终的输出结果我们可以看出来,较之通过TextUTF8TextWriter,通过XmlBinary编码后的字节数得到了极大的压缩(从原来的93变成了39),压缩率超过了50%。 

   1: MemoryStream stream = new MemoryStream();
   2: using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(stream))
   3: {
   4:     //省略成员
   5: }

输出结果:

字节数为:39
编码后的二进制表示为:
40-08-43-75-73-74-6F-6D-65-72-08-16-68-74-74-70-3A-2F-2F-77-77-77-2E-61-72-74-65
-63-68-2E-63-6F-6D-2F-99-03-46-6F-6F
编码后的文本表示为:
[省略不可读的编码内容]

如果我们查看XmlDictionaryWriter的WriteElementString方法,会发现其具有5个重载,其中3个是从XmlWriter中继承下来的(我们的代码使用的就是XmlWriter定义的方法),其余两个是XmlDictionaryWriter自定义成员。与XmlWriter中继承下来的方法不同的是,元素名称和命名空间通过XmlDictionaryString类型表示。实际上XmlDictionaryWriter的很多方法都同时提供以字符串和XmlDictionaryString表示的XML元素或属性名称和命名空间。在本节的开始我们就说了,XmlDictionary是编码和解码双方共享的“词汇表”,通过在编码过程中有效地使用它,可以在很大程度上压缩编码后的字节数。

   1: public abstract class XmlDictionaryWriter : XmlWriter
   2: {
   3:     //其他成员
   4:     public void WriteElementString(XmlDictionaryString localName, XmlDictionaryString namespaceUri, string value);
   5:     public void WriteElementString(string prefix, XmlDictionaryString localName, XmlDictionaryString namespaceUri, string value);
   6: }

相应地,XmlDictionary也反映在CreateBinaryWriter静态方法上面。CreateBinaryWriter方法比CreateTextWriter多了一些重载,其中多了一个IXmlDictionary接口类型的参数dictionary。

   1: public abstract class XmlDictionaryWriter : XmlWriter
   2: {
   3:     //其他成员
   4:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream);
   5:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary);
   6:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary, XmlBinaryWriterSession session);
   7:     public static XmlDictionaryWriter CreateBinaryWriter(Stream stream, IXmlDictionary dictionary, XmlBinaryWriterSession session, bool ownsStream);
   8: }

在现有代码的基础上,我做了一些修正,先创建XmlDictionary对象,将后面使用到的XML元素名称(Customer)和命名空间(http://www.artech.com/)定义成相应的XmlDictionaryString,并添加到XmlDictionary中。在调用CreateBinaryWriter的时候指定该XmlDictionary,并在调用WriteElementString方法的时候以DictionaryString的形式制定元素命名和命名空间。如果看了最终的输出结果,你可能会不敢相信自己的眼睛,字节长度变成了9(93=>39=>9)。之所以使用了XmlDictionary后的编码能够得到如此高的压缩率,就在于元素的名称和命名空间通过Key-Value的形式表示在了XmlDictionary中,在编码的时候会将XML中相应的Value内容替换成int型的Key,这样做当然能够使得压缩率得到极大的提升了。

   1: XmlDictionary dictionary = new XmlDictionary();
   2: IList<XmlDictionaryString> dictionaryStrings = new List<XmlDictionaryString>();
   3: dictionaryStrings.Add(dictionary.Add("Customer"));
   4: dictionaryStrings.Add(dictionary.Add("http://www.artech.com/"));
   5:  
   6: MemoryStream stream = new MemoryStream();
   7: using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(stream, dictionary))
   8: {
   9:     writer.WriteStartDocument();
  10:     writer.WriteElementString(dictionaryStrings[0], dictionaryStrings[1], "Foo");
  11:     writer.Flush(); 
  12: //其他操作
  13: }

输出结果:

字节数为:9
编码后的二进制表示为:
42-00-0A-02-99-03-46-6F-6F
编码后的文本表示为:
[省略不可读的编码内容]

3、XmlMtomWriter(CreateMtomWriter)

在很多分布式应用场景中,我们会通过SOAP消息传输一些大规模的二进制数据,比如我们上传文件、图片、MP3甚至是视频。对于这些大块的二进制内容,如果采用Binary的编码方式,固然能够获得最好的编码压缩率,保证数据的快速传输,但是却不能获得跨平台的能力。如果采用纯文本的编码方式,基于Base64的编码方式会使编码后的内容显得非常冗余,而且这些冗余的数据会直接置于SOAP消息的主体中,使得SOAP消息十分庞大,从而影响SOAP消息正常的传输。为了解决这样的问题,MTOM(Message Transmission Optimization Mechanism)应运而生。MTOM兼具文本编码的跨平台能力(因为MTOM是W3C制定一个规范),又具有Binary编码高压缩率的优势。要想深入了解MTOM的消息传输优化机制,读者可以访问W3C的官方网站下载相关的文档。在这里,我仅仅是对该机制的实现作一个简单的介绍。

首先,二进制的内容仍然按照Base64的方式进行编码,然后对包含<xs:base64binary>的元素进行传输优化(Transmission Optimization)。我们可以视这种优化为通过一种标准的、高压缩率的格式对其进行编码,这种格式是基于XOP(XML-binary Optimizated Packaging)。SOAP消息在被传输的时候,通过一种称为MIME Multipart/Related XOP Package的形式发送。MIME Multipart/Related XOP Package,XOP是经过对<xs:base64binary>元素进行优化编码后的数据包,Multipart/Related XOP就是多个关联的XOP,每个XOP数据包和SOAP封套(SOAP Envelope)是分开的,XOP并不内嵌于SOAP封套中,它作为其附件(Attachment)单独传送,SOAP封套保留一份XOP数据包的引用。

在WCF中,所有关于MTOM编码与解码相关的功能都通过XmlMtomWriter来完成,XmlMtomWriter通过XmlDictionaryWriter的CreateMtomWriter静态方法创建。当我们通过XmlMtomWriter对于一个XML Infoset执行写操作时,最终生成的是一个具有报头(Header)和主体(Body)的MIME Multipart/Related XOP Package,XML Infoset的内容经过编码被放到主体部分。参数startInfo表示该XML Infoset对应Content-Type的type属性,对于SOAP自然就是“Application/soap+xml”,而boundary则表示分隔符,startUri作为Content-ID,而writeMessageHeaders参数则表示是否写入MIME Multipart/Related XOP Package的报头内容。

   1: public abstract class XmlDictionaryWriter : XmlWriter
   2: {
   3:     //其他成员
   4:     public static XmlDictionaryWriter CreateMtomWriter(Stream stream, Encoding encoding, int maxSizeInBytes, string startInfo);
   5:     public static XmlDictionaryWriter CreateMtomWriter(Stream stream, Encoding encoding, int maxSizeInBytes, string startInfo, string boundary, string startUri, bool writeMessageHeaders, bool ownsStream);
   6: }

接下来我通过一个简单的例子演示相同的XML元素通过XmlMtomWriter编码后又将具有怎样的格式。在现有演示代码的基础上,通过调用CreateMtomWriter方法创建XmlMtomWriter,并将startInfo、boundary和startUri分别指定为"Application/soap+xml"、http://www.artech.com/binary和http://www.artech.com/contentid。从最后的结果我们可以看到:整个数据包包含两个部分:报头和主体,报头的主要作用在于指定整个数据包的MIME版本和Content-Type。在Content-Type中multipart/related;type="application/xop+xml"是基于整个数据包,而boundary="http://www.artech.com/binary";start="<http://www.artech.com/ contentid>";start-info="Application/soap+xml"则针对主体部分。

   1: MemoryStream stream = new MemoryStream();
   2: using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateMtomWriter(stream, Encoding.UTF8, int.MaxValue, "Application/soap+xml", "http://www.artech.com/binary", "http://www.artech.com/contentid", true, true))
   3: {
   4:  //省略操作
   5: }

输出结果:

字节数为:517
编码后的二进制表示为:
4D-49-4D-45-2D-56-65-72-73-69-6F-6E-3A-20-31-(…省略…)0A
编码后的文本表示为:
MIME-Version: 1.0
Content-Type: multipart/related;type="application/xop+xml";boundary="http://www.
artech.com/binary";start="<http://www.artech.com/contentid>";start-info="Application/soap+xml"

--http://www.artech.com/binary
Content-ID: <http://www.artech.com/contentid>
Content-Transfer-Encoding: 8bit
Content-Type: application/xop+xml;charset=utf-8;type="Application/soap+xml"

<?xml version="1.0" encoding="utf-8"?>
<Customer xmlns="http://www.artech.com/">Foo</Customer>
--http://www.artech.com/binary—

由于MTOM只有在针对大规模的二进制数据的传输时才能显示出优化的能力,对于文本内容反而因为多了很多必须的结构化描述信息,使得最终编码后的数据包都基于纯文本编码方式而冗余。MOTM对于二进制数据的编码,我会在后续的部分为读者作演示。

三、XmlDictionaryReader

有XmlDictionaryWriter就必然有XmlDictionaryReader,XmlDictionaryWriter对XML Infoset进行编码并将编码后的字节写入流中,而XmlDictionaryReader则读取二进制流并对其解码生成相应的XML Infoset。WCF同样定义了3个具体的XmlDictionaryReader:XmlUTF8TextReader、XmlBinaryReader和XmlMtomReader,他们通过定义在XmlDictionaryReader的静态方法CreateTextReader、CreateBinaryReader和CreateMtomReader进行创建,在这里就不一一细说了。