最近使用WCF作為通迅框架開發一套信息系統,系統使用傳統C/S框架,系統有可能會部署在互聯網上,因此決定對傳輸的數據進行GZIP壓縮,原來在使用.NET Remoting時,可以使用插入自定義的ChannelSink來實現數據壓縮,作為.NET Remoting的替代方案的WCF,實現起來也很容易,且方法不止一種,主要解決方法主要有以下四種:
- 通過自定義MessageEncoder和MessageEncodingBindingElement 來完成。具體的實現,可以參閱張玉彬的文章《WCF進階:將編碼后的字節流壓縮傳輸》和MSDN的文章《Custom Message Encoder: Compression Encoder》。
- 直接創建用於壓縮和解壓縮的信道,在CodePlex中具有這么一個WCF Extensions;
- 自定義MessageFormatter實現序列化后的壓縮和反序列化前的解壓,詳見WCF大師Artech中的博客有《通過WCF擴展實現消息壓縮》
- 自定義MessageInspector實現這就是我們今天將要討論的解決方案。
相比較,第三和第四實現相對簡單,配置很簡單,它們的內部實現方法很類似,我的消息壓縮類也來源於WCF大師Artech的博客《通過WCF擴展實現消息壓縮》的消息壓縮類,區別在於第三在自定義MessageFormatter中對消息進行壓縮和解壓縮,而第四是在自定義MessageInspector中對消息進行壓縮和解壓縮。下面給出第四種實現方法(網絡上也很多):
一、Compress-壓縮與解壓縮類

/// <summary> /// 壓縮解壓縮類 /// </summary> public class Compress { public static byte[] Zip(byte[] sourceBytes) { using (MemoryStream mStream = new MemoryStream()) { GZipStream gStream = new GZipStream(mStream, CompressionMode.Compress); gStream.Write(sourceBytes, 0, sourceBytes.Length); gStream.Close(); return mStream.ToArray(); } } public static byte[] UnZip(byte[] sourceBytes) { using (MemoryStream mStream = new MemoryStream()) { using (GZipStream gStream = new GZipStream(new MemoryStream(sourceBytes), CompressionMode.Decompress)) { int readBytes = 0; byte[] buffer = new byte[1024]; while ((readBytes = gStream.Read(buffer, 0, buffer.Length)) > 0) { mStream.Write(buffer, 0, readBytes); } return mStream.ToArray(); } } } }
二、MessageCompressor—消息壓縮與解壓類

/// <summary> /// 消息壓縮類 /// </summary> public static class MessageCompress { public static string Namespace = "http://myjece"; public static Message CompressMessage(Message sourceMessage) { byte[] buffer; string sourceBody; using (XmlDictionaryReader reader1 = sourceMessage.GetReaderAtBodyContents()) { sourceBody = reader1.ReadOuterXml(); buffer = Encoding.UTF8.GetBytes(sourceBody); } XmlTextReader reader; if (buffer.Length > 256) { byte[] compressedData = Compress.Zip(buffer); string compressedBody = CreateCompressedBody(compressedData); reader = new XmlTextReader(new StringReader(compressedBody), new NameTable()); sourceMessage.AddCompressionHeader(); } else { reader = new XmlTextReader(new StringReader(sourceBody), new NameTable()); } Message message = Message.CreateMessage(sourceMessage.Version, null, (XmlReader)reader); message.Headers.CopyHeadersFrom(sourceMessage); message.Properties.CopyProperties(sourceMessage.Properties); sourceMessage.Close(); return message; } public static Message DeCompressMessage(Message sourceMessage) { if (!sourceMessage.IsCompressed()) { return sourceMessage; } else { sourceMessage.RemoveCompressionHeader(); string deCompressedBody = Encoding.UTF8.GetString(Compress.UnZip(sourceMessage.GetCompressedBody())); XmlTextReader reader = new XmlTextReader(new StringReader(deCompressedBody), new NameTable()); Message message = Message.CreateMessage(sourceMessage.Version, null, (XmlReader)reader); message.Headers.CopyHeadersFrom(sourceMessage); message.Properties.CopyProperties(sourceMessage.Properties); message.AddCompressionHeader(); //sourceMessage.Close(); return message; } } public static bool IsCompressed(this Message message) { return message.Headers.FindHeader("Compression", Namespace) > -1; } public static void AddCompressionHeader(this Message message) { message.Headers.Add(MessageHeader.CreateHeader("Compression", Namespace, "GZip")); } public static void RemoveCompressionHeader(this Message message) { message.Headers.RemoveAll("Compression", Namespace); } public static string CreateCompressedBody(byte[] content) { StringWriter output = new StringWriter(); using (XmlWriter writer2 = XmlWriter.Create(output)) { writer2.WriteStartElement("CompressedBody", Namespace); writer2.WriteBase64(content, 0, content.Length); writer2.WriteEndElement(); } return output.ToString(); } public static byte[] GetCompressedBody(this Message message) { byte[] buffer; using (XmlReader reader1 = message.GetReaderAtBodyContents()) { buffer = Convert.FromBase64String(reader1.ReadElementString("CompressedBody", Namespace)); } return buffer; } }
三、ClientCompressionInspector-客戶端對消息進行壓縮與解壓縮的消息檢查器

private class ClientCompressionInspector : IClientMessageInspector { #region IClientMessageInspector Members public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { reply = MessageCompress.DeCompressMessage(reply); } public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel) { //加入一個消息頭,表明客戶端支持gzip消息壓縮與解壓縮 request.Headers.Add(MessageHeader.CreateHeader("AcceptEncoding", "http://myjece", "gzip")); request = MessageCompress.CompressMessage(request); return null; } #endregion }

public class ClientCompressionBehavior : BehaviorExtensionElement, IEndpointBehavior { public override Type BehaviorType { get { return typeof(ClientCompressionBehavior); } } protected override object CreateBehavior() { return new ClientCompressionBehavior(); } #region IEndpointBehavior Members public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { return; } public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new ClientCompressInspector()); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { return; } }
四、ServiceCompressInspector-服務端對消息進行壓縮與解壓縮的消息檢查器

public class ServiceCompressInspector : IDispatchMessageInspector { public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { request = MessageCompress.DeCompressMessage(request); return null; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { if (GetHeader("AcceptEncoding") == "gzip") { reply = MessageCompress.CompressMessage(reply); } } public static string GetHeader(string headerName) { if (OperationContext.Current.IncomingMessageHeaders.FindHeader(headerName, "http://myjece") >= 0) { return OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(headerName, "http://myjece"); } else { return null; } } }

public class ServiceCompressBehavior : BehaviorExtensionElement, IServiceBehavior { public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { //throw new NotImplementedException(); } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher epDisp in chDisp.Endpoints) { epDisp.DispatchRuntime.MessageInspectors.Add(new ServiceCompressInspector()); } } } public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { //throw new NotImplementedException(); } public override Type BehaviorType { get { return typeof(ServiceCompressBehavior); } } protected override object CreateBehavior() { return new ServiceCompressBehavior(); } }
五、服務端配置
在system.serviceModel節點下添加:
<extensions> <behaviorExtensions> <add name="compressBehavior" type="ServiceCompressBehavior, Service, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </behaviorExtensions> </extensions>
<behaviors> <serviceBehaviors> <behavior> <compressBehavior /> </behavior> </serviceBehaviors> </behaviors>
好了,上面的基本的實現,可以通過在客戶端行為器中添加AcceptEncoding=gzip的消息頭來確定服務端返回的消息是否也可以(需要)進行壓縮,實際運行結果也正常,但后來發現在進行大量byte[]類型數據傳輸時,發現有延時,幾百K有數據,在局域網(排除網絡問題)內,盡然達到2秒左右延時,開始懷疑的GZIP壓縮類有問題,后發現,壓縮類的對數據進行壓縮時,耗時極小,一般幾毫秒到幾十毫秒之間,最后,只能逐語句進行排查,發現問題在消息壓縮類中的:
using (XmlDictionaryReader reader1 = sourceMessage.GetReaderAtBodyContents())
{
sourceBody = reader1.ReadOuterXml(); buffer = Encoding.UTF8.GetBytes(sourceBody);
}
Message在經過每一層消息檢查器時,都是以Message方法進行傳遞,只有到達TransportBinding上編碼器時,才會對Message進行編碼,或文本,或二進制,但在我上面的消息壓縮中,先從原Message中獲取Body內容,然后對Body進行壓縮,再把壓縮Body封裝進新的Message中,問題就出在獲取Body內容中,XmlDictionaryReader 的ReadOuterXml()方法相當對Body進行了XML的編碼,所以導致了性能問題。解決問題的根本在於找到一個能獲取到Body內容,又能避免提前對Body內容進行XML編碼的方法。
將上述代碼改為以下代碼后,性能得以大幅提升:
MemoryStream ms = new MemoryStream();
XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(ms, Encoding.UTF8);
sourceMessage.WriteBodyContents(writer);
writer.Flush();
buffer=ms.ToArray();
但最終獲取到的buffer是一致的,是什么原因導致它們之間有巨大的性能差異就不得而知了……