基於WCF的RESTFul WebAPI如何對傳輸內容實現壓縮


前言

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM