消息作為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進行創建,在這裡就不一一細說了。