藉由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的頻寬瓶頸. 所以要視情況去選擇要壓縮的作業.
參考資料 :