Android 網絡框架解壓縮(gzip)淺談

六談這個話題,是因為很多時間都忽略了這個因素,網絡傳輸數據的壓縮很少有人去關注,然而有時間提到這個問題的時間卻一時不知道怎么回答,或者已經忘掉了這個概念...

進入正題,首先來聊聊Gzip。

一、Gzip概念

Gzip是GNUZip的縮寫,他是一個GNU自由軟件的文件圧縮程序。

二、為什么要用Gzip

我們在進行網絡傳輸數據時,經常用到json、xml等格式的數據,這些數據在傳輸前可以進行壓縮,這時候就會涉及到一種壓縮格式—Gzip。Gzip的壓縮比率非常大,有的甚至能達到99.9%以上,可以大大減少傳輸內容,提高用戶的傳輸速度,進而提高用戶的體驗。

三、檢測是否使用Gzip壓縮以及壓縮比例

http://tool.chinaz.com/Gzips/

https://gzip.51240.com/

比如我們通過第一個鏈接看一下“開源中國的新聞頁”,網址如下:

http://www.oschina.net/action/api/news_list?catalog=1&pageIndex=0&pageSize=20

壓縮結果

結果顯示,這個網頁沒有進行壓縮,源文件大小為12KB,而壓縮后,文件可減小到0.01KB,可以節省99.92%的傳輸控件。這是什么概念呢?相當于100MB的數據經過壓縮后不到1MB。

四、Android中實現Gzip壓縮的原理

說道這里,我們先說一下Http中的Gzip技術細節

HTTP協議上的GZIP編碼是一種用來改進WEB應用程序性能的技術。一般服務器中都安裝有這個功能模塊的,服務器端不需做改動,當瀏覽器支持gzip 格式的時候, 服務器端會傳輸gzip格式的數據。具體講就是 http request 頭中 有 "Accept-Encoding", "gzip" ,response 中就有返回頭Content-Encoding=gzip ,我們現在從瀏覽器上訪問玩啥網站都是gzip格式傳輸的。

同樣的的道理,我們可以在android 客戶端 request 頭中加入 "Accept-Encoding", "gzip" ,來讓服務器傳送gzip 數據。


首先,客戶端發請求給服務端,會帶上請求頭:Accept-Encoding:gzip。第二步,服務端接收到請求頭后,可以選擇壓縮或不壓縮。第三步,服務端選擇壓縮后,文件明顯變小,同時在響應頭加上Content-Encoding:gzip。第四步,客戶端接收到響應后,根據響應頭中是否帶有Content-Encoding:gzip,判斷文件是否被壓縮,如果壓縮就進行解壓,如果沒有壓縮,就按照正常方式讀取數據即可。

五、在Android各網絡框架中表現有什么差異

OKhttp

OKhttp3.4.0開始將這些邏輯抽離到了內置的interceptor中,看起來較為方便

BridgeInterceptor.java這個類里邊可以看到

// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null) {
? ? ? transparentGzip = true;
? ? ? requestBuilder.header("Accept-Encoding", "gzip");
}

如果header中沒有Accept-Encoding,默認自動添加 ,且標記變量transparentGzip為true。

if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)){

? ? ?? GzipSource responseBody = new GzipSource(networkResponse.body().source());

? ? ?? Headers strippedHeaders = networkResponse.headers().newBuilder()

? ? ?? .removeAll("Content-Encoding")

? ? ?? .removeAll("Content-Length")

? ? ?? .build();

? ? ?? responseBuilder.headers(strippedHeaders);

? ? ?? responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));

}

針對返回結果,如果同時滿足以下三個條件:

1.transparentGzip為true,即之前自動添加了Accept-Encoding

2.header中標明了Content-Encoding為gzip

3.有body

移除 Content-Encoding、Content-Length,并對結果進行解壓縮。

可以看到以上邏輯完成了,由此我們通過OkHttp源碼得出以下結論:

1.開發者沒有添加Accept-Encoding時,自動添加Accept-Encoding: gzip

2.自動添加的request,response支持自動解壓

3.手動添加不負責解壓縮

4.自動解壓時移除Content-Length,所以上層Java代碼想要contentLength時為-1

5.自動解壓時移除 Content-Encoding

6.自動解壓時,如果是分塊傳輸編碼,Transfer-Encoding: chunked不受影響。

HttpUrlConnection

由于引用太多源碼就不寫了,直接針對以上6點做結果分析

1.2.3后默認是gzip,不加Accept-Encoding會被自動添加上Accept-Encoding: gzip。

2.自動添加的request,response支持自動解壓

3.手動添加不會負責解壓縮。

4.這里提出一點HttpURLConnection 在Android 4.4以后底層是由OkHttp實現的,所以

? ? ? *4.4之后的版本,Content-Length被移除,getContentLength() = -1

? ? ? *2.3- 4.3之間,Content-Length 沒有移除,getContentLength() = compressed size

5. 自動解壓時的Content-Encoding

與Content-Length對應:

? ? ? *4.4之后的版本,Content-Encoding被移除

? ?? *2.3- 4.3之間,Content-Encoding存在,無變化。

6. 自動解壓時的分塊編碼傳輸

與OkHttp相同,Transfer-Encoding: chunked不受影響。

六、具體代碼實例看下解壓流程

private String getJsonStringFromGZIP(HttpResponse response) {

? ? ?? String jsonString = null;

? ? ?? try {

? ? ? ? ? ? ?? InputStream is = response.getEntity().getContent();

? ? ? ? ? ? ?? BufferedInputStream bis = new BufferedInputStream(is);

? ? ? ? ? ? ?? bis.mark(2);

? ? ? ? ? ? ? // 取前兩個字節

? ? ? ? ? ? ? byte[] header = new byte[2];

? ? ? ? ? ? ? int result = bis.read(header);

? ? ? ? ? ? ? // reset輸入流到開始位置

? ? ? ? ? ?? bis.reset();

? ? ? ? ? ?? // 判斷是否是GZIP格式

? ? ? ? ? ? int headerData = getShort(header);

? ? ? ? ?? // Gzip 流 的前兩個字節是 0x1e8b

? ? ? ? ? if (result != -1 && headerData == 0x1e8b) { LogUtil.d("HttpTask", " use GZIPInputStream? ");

? ? ? ? ? ? ? ? ? is = new GZIPInputStream(bis);

? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? LogUtil.d("HttpTask", " not use GZIPInputStream");

? ? ? ? ? ? ? ? ? is = bis;

? ? ? ?? }

? ? ? ?? InputStreamReader reader = new InputStreamReader(is, "utf-8");

? ? ? ?? char[] data = new char[100];

? ? ? ?? int readSize;

? ? ? ?? StringBuffer sb = new StringBuffer();

? ? ? ?? while ((readSize = reader.read(data)) > 0) {

? ? ? ? ? ? ? ?? sb.append(data, 0, readSize);

? ? ? ?? }

? ? ? ? ? ? ? ? jsonString = sb.toString();

? ? ? ? ? ? ?? bis.close();

? ? ? ? ? ? ?? reader.close();

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? ? ? LogUtil.e("HttpTask", e.toString(),e);

? ? ?? }

? ? ?? LogUtil.d("HttpTask", "getJsonStringFromGZIP net output?: " + jsonString );

? ? ? ? return jsonString;

}

private int getShort(byte[] data) {

? ? ? ?? return (int)((data[0]<<8) | data[1]&0xFF);

}

參考資料

? ? ? ?? Android’s HTTP Clients

? ? ? ?? HttpURLConnection

? ? ? ?? HTTP 協議中的 Transfer-Encoding

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容