藉由GZipStream的壓縮,來減少Web Service的傳輸量

藉由GZipStream的壓縮,來減少Web Service的傳輸量

  不知道有多少人有遇到跟我一樣的問題,就是Web Service的資料回傳量太大了,如果都是走區域網路的話,除非量很大,不然還感覺不太出來,可是,如果是透過ADSL的頻寬的話,那就很驚人了,以30MB的資料量來說,透過2M/512K的網路上傳,加上網路品質的損耗以65%來看,也大概要花738秒左右來上傳. (30*1024)/((512*0.65)/8)=738.46. 這樣的時間,看起來很嚇人,何況在上傳滿載時,下載的頻寬可能剩40%不到,如果今天能壓縮這資料量,讓資料量剩25%左右,那上傳時間就只要185秒左右,這樣的差異就很大了,如果今天我們採用的是n-tier架構,Client是透過Web Service在要資料,那要怎麼去壓縮這傳輸過程中的資料? 這時就可以拿GZipStream出來用了.

 

  起初,GZipStream只是被我拿來當檔案壓縮用的,剛好這段時間遇到公司ERP系統效能調校,突然產生了一個想法,如果我能拿它來壓縮資料,那傳輸的資料量不就小很多,尤其是XML的文字檔,效果一定更明顯,就在這個念頭下,著手開始寫了測試的程式,在這裡,我做了兩種不同的做法,一個是純壓縮,不Serialize,另一種做法是壓縮與Serialize,而這兩種做法,是會產生些微的資料量差異.

 

在提到程式內容之前,先說明這做法所造成的資料量差異,詳見下表 :

正常資料量

壓縮後

壓縮+Serialize

18,368,067 Bytes

4,755,570 Bytes

4,445,278 Bytes

17,937.57 Kb

4,644.11 Kb

4,341.09 Kb

 

 

接下來,就是程式碼部份,分兩個部份說明:

1 .純壓縮 :

WebService Side :

using System.IO.Compression;



        [WebMethod]
        public byte[] getZipData()
        {
            DataSet ds = LoadData().Copy();
            MemoryStream unMS = new MemoryStream();
            ds.WriteXml(unMS);

            byte[] bytes = unMS.ToArray();
            int lenbyte = bytes.Length; 

            MemoryStream compMs = new MemoryStream();
            GZipStream compStream = new GZipStream(compMs, CompressionMode.Compress, true);
            compStream.Write(bytes, 0, lenbyte); 

            compStream.Close();
            unMS.Close();
            compMs.Close();
            byte[] zipData = compMs.ToArray();
            return zipData; 
        }

        private DataSet LoadData()
        {//產生測試資料用
            DataSet ds = new DataSet();
            DataTable dt = new DataTable("Test");
            dt.Columns.Add("ProID",typeof(int));
            dt.Columns.Add("ProName", typeof(string));
            dt.Columns.Add("CreateTime", typeof(DateTime));
            dt.Columns["ProID"].AutoIncrement = true;
            for (int i = 0; i < 100000; i++)
            {
                DataRow dr = dt.NewRow();
                dr["ProName"] = Guid.NewGuid().ToString();
                dr["CreateTime"] = DateTime.Now.ToString();
                dt.Rows.Add(dr);
            }
            ds.Tables.Add(dt);
            ds.AcceptChanges();
            return ds;
        }

 

 

 

 

Client Side :

using System.IO;

       private void btn_ZipGet_Click(object sender, EventArgs e)
       {
           try
           {
               WS.Service1 wss = new WSZipDemo.WS.Service1();//WebReference
               byte[] da = wss.getZipData(); 

               MemoryStream input = new MemoryStream();
               input.Write(da, 0, da.Length);
               input.Position = 0;
               GZipStream gzip = new GZipStream(input, CompressionMode.Decompress, true); 

               MemoryStream output = new MemoryStream();
               byte[] buff = new byte[4096];
               int read = -1;
               read = gzip.Read(buff, 0, buff.Length);
               while (read > 0)
               {
                   output.Write(buff, 0, read);
                   read = gzip.Read(buff, 0, buff.Length);
               }
               gzip.Close();
               byte[] rebytes = output.ToArray();
               output.Close();
               input.Close(); 

               MemoryStream ms = new MemoryStream(rebytes);
               DataSet ds = new DataSet();
               ds.ReadXml(ms);
               dataGridView1.DataSource = ds.Tables[0];
           }
           catch (Exception ex)
           {
               MessageBox.Show(ex.Message);
           }
       }

 

 

 

 

2. 壓縮+Serialize

Web Service Side :

using System.IO.Compression;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;



[WebMethod]
public byte[] getZipData()
{
    DataSet ds = LoadData().Copy();
    ds.RemotingFormat = SerializationFormat.Binary;
    BinaryFormatter ser = new BinaryFormatter();
    MemoryStream unMS = new MemoryStream();
    ser.Serialize(unMS, ds);

    byte[] bytes = unMS.ToArray();
    int lenbyte = bytes.Length; 

    MemoryStream compMs = new MemoryStream();
    GZipStream compStream = new GZipStream(compMs, CompressionMode.Compress, true);
    compStream.Write(bytes, 0, lenbyte); 

    compStream.Close();
    unMS.Close();
    compMs.Close();
    byte[] zipData = compMs.ToArray();
    return zipData; 
}

private DataSet LoadData()
{//產生測試資料用
    DataSet ds = new DataSet();
    DataTable dt = new DataTable("Test");
    dt.Columns.Add("ProID",typeof(int));
    dt.Columns.Add("ProName", typeof(string));
    dt.Columns.Add("CreateTime", typeof(DateTime));
    dt.Columns["ProID"].AutoIncrement = true;
    for (int i = 0; i < 100000; i++)
    {
        DataRow dr = dt.NewRow();
        dr["ProName"] = Guid.NewGuid().ToString();
        dr["CreateTime"] = DateTime.Now.ToString();
        dt.Rows.Add(dr);
    }
    ds.Tables.Add(dt);
    ds.AcceptChanges();
    return ds;
}

 

 

 

 

Client Side :

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;



private void btn_ZipGet_Click(object sender, EventArgs e)
{
    try
    {
        WS.Service1 wss = new WSZipDemo.WS.Service1();//WebReference
        byte[] da = wss.getZipData(); 

        MemoryStream input = new MemoryStream();
        input.Write(da, 0, da.Length);
        input.Position = 0;
        GZipStream gzip = new GZipStream(input, CompressionMode.Decompress, true); 

        MemoryStream output = new MemoryStream();
        byte[] buff = new byte[4096];
        int read = -1;
        read = gzip.Read(buff, 0, buff.Length);
        while (read > 0)
        {
            output.Write(buff, 0, read);
            read = gzip.Read(buff, 0, buff.Length);
        }
        gzip.Close();
        byte[] rebytes = output.ToArray();
        output.Close();
        input.Close(); 

        MemoryStream ms = new MemoryStream(rebytes);
        BinaryFormatter bf = new BinaryFormatter();
        object obj = bf.Deserialize(ms);
        DataSet ds = (DataSet)obj;
        dataGridView1.DataSource = ds.Tables[0];
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

 

 

 

 

  這兩種做法只是在部份的程式碼不一樣,但其它大多相同,如果是方法一,在WebService壓縮後Return,Client收到資料後,解壓縮即可,方法二則多了Serialize這部份,所以在WebService這邊,先Serialize,再壓縮,Client端收到後,先解壓縮再Deserialize. 不過,這個壓縮後的量,是很讓人滿意,如果在頻寬有限,又需要傳輸大量資料時,這個方法可以考慮看看. 因為這個是.Net 2.0以後才有的東西,公司現在的ERP還在.Net 1.0,所以.........殘念~

 

2008/04/29 補充 :

  最近有網友看到這篇文章,有些問題問我朋友,而朋友再來轉問我這個問題(真巧,網友問我朋友,我朋友再來問我),而提出的問題是,壓縮跟解壓是否很耗用CPU的效能? 答案是,不可能不用到CPU的效能,但它也不致於到"很耗用"的地步. 而這耗用率只能視設備狀況來判斷,以我的環境來說,Client端的電腦是P4 3GHT, 上面的這個例子跑的時候有個瞬間最高47%,不到一秒的時間,如果是未壓縮版的,瞬間最高為37%左右,所以大概多個10%吧.

  或許有人還是有疑慮怎麼可以增加CPU的Loading呢,這樣就不好了,這時我們就要換個角度來思考這個問題了,"效能成本"所在為何,我們的Bottleneck在那.CPU在科技的進步下,雙核四核的都推出了,CPU的效能是快速的在倍增中,而我們的網路呢? 絕大多數區域還在100MB,部份Server與Server是1G在連,而ADSL呢? 10M/1M或者是只能2M/512K,因為申請不到更快的頻寬,如果要更快,每月的費用就更高, 注意囉,上傳是1MB或512K哦,而且是多人共用的,所以就目前國內的網路環境來看,ADSL要到100M/100M,似乎還要很長的一段時間,而這段時間的CPU,也不知道已經成長到幾核心了,兩者之間怎麼取捨,就看大家的看法囉.

 

2008/5/30 補充

  因為一些朋友對這個方式感到有興趣,也在網路上找了一些文章,可是卻又發現了一些疑問,到底這個壓縮技術能用在什麼樣的情況下. 其中一位朋友傳給了我一個網址.Net DataTable 大量資料壓縮加密實測,也是說明用壓縮的方式來減少流量,一些測試結果也很詳盡,有興趣的人建議參考,只是朋友在他的文章開頭的地方看到 [開發 Web 或分散式系統],這是否代表Web網站也可以用? 這個答案當然是沒用的,試想,我們在Web Server壓縮,Client端用IE或Firefox怎麼解壓縮,網頁是無法用Gzipstream的方式來解壓縮來減少Web Server到Client的資料量,能壓一定要能解才有用,所以就不用再去列出那些可以及那些不行了,畢竟這還關係的架構上的問題,所以應用的關鍵就是"能壓要能解才能用".

  另一個問題就是選擇性的使用,壓縮的動作勢必用到系統的效能,如果全面性100%的採用壓縮,當使用者多,或操作頻率高時,資料量大,系統效能也會變的更加吃重,所以必需挑選幾個關鍵的傳輸來壓縮即可,不用連1K不到的資料量也在壓縮,除非Server很猛的,那就另當別論. 依我的情況,我會挑選幾個使用者的查詢作業來壓,依目前手頭上的分析資料來看,有個查詢作業使用頻率較高,資料量也是驚人,最高的資料量就一次高達33MB,平均起來跟其它作業比較,這作業的資料量佔了不小的比率,光這作業一天平均傳輸量650MB,壓縮後只剩162.5MB在傳,所以只壓縮這個作業的查詢動作,就可明顯的改善ADSL的頻寬瓶頸. 所以要視情況去選擇要壓縮的作業.

 

參考資料 :

MSDN GZipStream 類別

MSDN BinaryFormatter 建構函式 ()