六谈这个话题,是因为很多时间都忽略了这个因素,网络传输数据的压缩很少有人去关注,然而有时间提到这个问题的时间却一时不知道怎么回答,或者已经忘掉了这个概念...
进入正题,首先来聊聊Gzip。
一、Gzip概念
Gzip是GNUZip的缩写,他是一个GNU自由软件的文件圧缩程序。
二、为什么要用Gzip
我们在进行网络传输数据时,经常用到json、xml等格式的数据,这些数据在传输前可以进行压缩,这时候就会涉及到一种压缩格式—Gzip。Gzip的压缩比率非常大,有的甚至能达到99.9%以上,可以大大减少传输内容,提高用户的传输速度,进而提高用户的体验。
三、检测是否使用Gzip压缩以及压缩比例
比如我们通过第一个链接看一下“开源中国的新闻页”,网址如下:
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