前言
WCF作為通迅框架可以很容易地實現對消息的壓縮,且方法不止一種,主要解決方法主要有以下四種:
1、通過自定義MessageEncoder和MessageEncodingBindingElement 來完成。具體的實現,可以參閱張玉彬的文章《WCF進階:將編碼后的字節流壓縮傳輸》;
2、直接創建用於壓縮和解壓縮的信道,在CodePlex中具有這么一個WCF Extensions;
3、自定義MessageFormatter實現序列化后的壓縮和反序列化前的解壓,詳見WCF大師Artech中的博客有《通過WCF擴展實現消息壓縮》;
4、自定義MessageInspector實現,詳見博客園似若流雲的文章《WCF 消息壓縮性能問題及解決方法》。
這幾種方法實現、配置都很簡單。后兩種方法的內部實現方法很類似,區別在於第三種方法通過自定義MessageFormatter中對消息進行壓縮和解壓縮,而第四種方法是在自定義MessageInspector中對消息進行壓縮和解壓縮。比較而言最后一種是最簡單粗暴的。
幾種方案的適用場景
那么,這幾種方法都適用於什么場景呢?從技術上看,這4種方案基本可以分為兩類。一種是在消息編碼器上動手腳,另一種是在消息上做文章。
第一種是屬於在消息編碼器上動手腳的,實現稍復雜。應用場景比較廣泛,基本上所有的場景都是適用的。
后面三種都是在消息上做文章的,只適合有WCF客戶端的情況,因為如果沒有客戶端壓縮時在消息中加入的壓縮標志,服務端就沒法正確解壓,反之亦然。雖然原理相同,但三種方法的切入點各不相同。同時,第二種方法是可以改成在消息編碼器上進行壓縮/解壓的。
因為RESTFul的WCF的客戶端不僅僅是WCF,所以暫時只能選第一種方案了。
一個問題
雖然有現成的方案可用,但是,如果要完美支持多種客戶端的話,這里面還有幾個問題需要解決。
按照Http協議的規范,客戶端發送/服務端返回一個壓縮的數據,需要在協議頭部加上Content-Encoding,並設置其值為gzip或者deflate。告訴對方數據的壓縮方法,好讓對方能夠正確解壓。
如果客戶端希望返回的數據是壓縮的,那么就在頭部加上Accept-Encoding,並設置其值為gzip或者deflate服務器收到這個信息后,就知道客戶端選擇的壓縮方法,就可以按照客戶端指定的方法去壓縮數據。
然而,無論哪種方案,都是需要事先配置壓縮方式的。也就是說,需要雙方事前約定,無法實現用Content-Encoding的值來告知對方壓縮方式!這不優雅!!在很多時候,這是個大問題!!!
解決的辦法
我們知道,第四種方案的切入點在消息檢查器上,在這個點上,通常會實現一些自定義的攔截功能。一個自定義的消息檢查器需要繼承IDispatchMessageInspector,這個接口類定義了兩個接口:
object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
void BeforeSendReply(ref Message reply, object correlationState)
AfterReceiveRequest作用在收到消息后,BeforeSendReply作用在發送響應消息前。
我們可以通過下面的代碼,來根據請求頭的Accept-Encoding的值,給消息加上對應的壓縮標示,以便消息編碼器選擇正確的壓縮方式;並在返回響應前在響應頭部加上Content-Encoding並設置相應的值,以便客戶端正確解壓。
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
namespace Insight.WCF.CustomEncoder
{
public class CompressInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
var property = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
var accept = property?.Headers[HttpRequestHeader.AcceptEncoding];
switch (accept)
{
case "gzip":
OperationContext.Current.Extensions.Add(new GzipExtension());
break;
case "deflate":
OperationContext.Current.Extensions.Add(new DeflateExtension());
break;
}
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
var property = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
var exts = OperationContext.Current.Extensions;
if (exts.OfType(GzipExtension).Any())
{
property?.Headers.Add(HttpResponseHeader.ContentEncoding, "gzip");
}
else if (exts.OfType(DeflateExtension).Any())
{
property?.Headers.Add(HttpResponseHeader.ContentEncoding, "deflate");
}
}
}
public class GzipExtension : IExtension
{
public void Attach(OperationContext owner)
{
}
public void Detach(OperationContext owner)
{
}
}
public class DeflateExtension : IExtension
{
public void Attach(OperationContext owner)
{
}
public void Detach(OperationContext owner)
{
}
}
}
未徹底解決的問題
因為消息檢查器的AfterReceiveRequest作用在收到消息后,也就是說,對於POST/PUT/DELETE這三類請求,它們如果對提交的數據進行了壓縮的話,我們無法在消息編碼器中根據Content-Encoding的值進行解壓。消息編碼器中的ReadMessage方法是這樣的:
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
所以,如果要實現由客戶端來決定POST/PUT/DELETE數據的壓縮方式的話,只有在Content-Type上面搞點小動作。
這。。。。。。
不夠優雅呀!
看來得研究下改造第二種解決方案了。生命不止,折騰不息
源代碼在這里:https://github.com/xuanbg/Utility/tree/master/CustomEncoder