您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

最简单易懂的Gzip压缩实现,最清晰的OkHttp的Gzip压缩详解

时间:02-04来源:作者:点击数:

Gzip压缩和解压的实现

Gzip压缩使用起来很简单,以前我也只是在客户端使用,服务器端不用管,所以我只用过GZIPInputStream来读取,用起来也没有问题。后来OkHttp开始流行,后来听说OkHttp会自动处理Gzip压缩的数据,不需要我们使用GZIPInputStream来处理,于是我想验证一下是否真的是这样的,这时我就需要写个服务器端Demo了,发现行不通,会报错,找不到原因,老办法,先写个最简单Demo,把Gzip流用起来,整体思路如下:

  1. 创建一个很长的数据,一长串1,如:1111111111111111,1万个1
  2. 使用GZIPOutputStream压缩数据
  3. 使用GZIPInputStream解压数据

具体代码如下(使用Kotlin语言编写):

fun main() {
    // 原始数据
    val sb = StringBuffer()
    repeat(10000) { sb.append(1) } // 生成1万个1的字符串
    val rawBytes = sb.toString() .toByteArray(Charsets.UTF_8) // 原始数据
    println("压缩前size = ${rawBytes.size},  数据 = ${rawBytes.map { byteToHex(it) }}")

    // 压缩数据
    var baos = ByteArrayOutputStream()
    val gzipOut = GZIPOutputStream(baos)
    gzipOut.write(rawBytes)
    val gzipBytes = baos.toByteArray() // 拿到压缩后的数据
    println("压缩后size = ${gzipBytes.size},  数据 = ${gzipBytes.map { byteToHex(it) }}")

    // 解压数据
    val gzipIn = GZIPInputStream(ByteArrayInputStream(gzipBytes))
    baos.reset() // 重置内存流,以便重新使用
    var byte: Int
    while (gzipIn.read().also { byte = it } != -1) baos.write(byte)
    println("解压结果:size = ${baos.size()}, 数据 = ${String(baos.toByteArray(), Charsets.UTF_8)}")
}

/** 把字byte转换为十六进制的表现形式,如ff  */
fun byteToHex(byte: Byte) = String.format("%02x", byte.toInt() and 0xFF)

代码很简单,看似没什么问题,运行结果却出了异常,如下:

压缩前size = 10000,  数据 = [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31]
压缩后size = 10,  数据 = [1f, 8b, 08, 00, 00, 00, 00, 00, 00, 00]
Exception in thread "main" java.io.EOFException: Unexpected end of ZLIB input stream
	at java.base/java.util.zip.InflaterInputStream.fill(InflaterInputStream.java:245)
	at java.base/java.util.zip.InflaterInputStream.read(InflaterInputStream.java:159)
	at java.base/java.util.zip.GZIPInputStream.read(GZIPInputStream.java:118)
	at java.base/java.util.zip.InflaterInputStream.read(InflaterInputStream.java:123)
	at KtMainKt.main(KtMain.kt:31)
	at KtMainKt.main(KtMain.kt)

想不出这么简单的代码哪里会出问题,不就是一个读一个写吗?而且我发现不论我写什么数据,压缩后的数据结果都是一样的,说明是在写数据(压缩)的时候出了问题,百度也找不到原因,百度Gzip压缩的相关知识并没有得到答案,百度上面报的异常信息也没有答案,最后是读了一下JDK文档才发现了问题的原因(所以JDK文档是个好东西,要多看看),在GZIPOutputStream文档上有这么一个方法:

finish() 完成将压缩数据写入输出流的操作,无需关闭底层流。

之前我有试过调用输出流的flush方法,没想到要调用的竟然是finish方法,在GZIPOutputStream的wirte方法执行之后再调用一下finish方法即可,运行结果如下:

压缩前size = 10000,  数据 = [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31]
压缩后size = 46,  数据 = [1f, 8b, 08, 00, 00, 00, 00, 00, 00, 00, ed, c1, 01, 0d, 00, 00, 00, c2, a0, 4c, ef, 5f, ce, 1c, 6e, 40, 01, 00, 00, 00, 00, 00, 00, 00, 00, c0, bf, 01, 5e, 62, 1a, 8f, 10, 27, 00, 00]
解压结果:size = 10000,  数据 = 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

从结果可以看到,压缩前的数据长度为10000,压缩后的长度为46,真是牛B呀。解压结果正确还原了1万个1,因为1万个1太长,上面的打印结果我是进行了删除的,所以大家不要觉得结果不对。

学会了Gzip大家平时做传输时就可以节省大量的流量了,比如获取服务器端数据时可以让服务器压缩后再传输,我们给服务器传参数时,如果参数很大也可Gzip压缩后再传给服务器,又比如上传文本文件(如bug文件、log日志等)到服务器时,也可以Gzip压缩后再上传,节省流量、加快访问、提高用户体验耿耿的!

OkHttp中的Gzip压缩处理

听说Gzip压缩是自动处理的,我也是最近看到别人文章上说的,我之前一直是在请求头上加上gzip的,读流的时候就自己用GZIPInputStream来解压,也是没问题的,但关键是既然OkHttp自动处理了,则我们就不要自己处理了,于是我要写个Demo来验证一下。

服务器端

打开陈年老旧的Eclipse(因为在IntelliJ上写JavaEE不熟),写了个JavaEE的Demo,创建了一个Servlet,如下:

/** 服务器端 */
public class Hello extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 数据内容:一段Json
		byte[] datas = "{name:\"张三\",age:18}".getBytes("UTF-8");							
		
		// 告诉客户端我们发送的数据是经过Gzip压缩的
		response.setHeader("Content-Encoding", "gzip");		
		
		// 告诉客户端我们发送的数据是什么类型,以及用的什么编码
		response.setContentType("application/json; charset=UTF-8"); 
		
		// 创建一个内存流,用于保存压缩后的数据
		ByteArrayOutputStream baos = new ByteArrayOutputStream();	
		
		// 压缩数据
		GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
		gzipOut.write(datas);
		gzipOut.finish();
		
		// 告诉客户端我们发送的数据总共的长度
		response.setContentLength(baos.size()); 
		
		// 把压缩后的数据传给客户端
		response.getOutputStream().write(baos.toByteArray());
	}


}

客户端

客户端就使用了IntelliJ,Kotlin语言编写,OkHttp进行数据请求,代码如下:

fun main() {
    val url = "http://localhost:8080/WebDemo/Hello"
    val request = Request.Builder().url(url).build()

    OkHttpClient().newCall(request).execute().use { response ->
        println(response.body()?.string())
    }
}

Kotlin是个好东西,OkHttp也是个好东西,写个请求就是这么简单,几行代码搞定,运行结果如下:

{name:"张三",age:18}

没有出现乱码,实验证明OkHttp自动帮我们进行了Gzip解压,我们不需要特殊处理,而且我们看打印结果有中文,中文并没有显示成乱码,说明OkHttp还会根据服务器端指定的编码来处理String。这样在我们去面试时,如果别人要问OkHttp的优点时这里就有两点可说了:

  1. OkHttp会自动进行Gzip处理
  2. OkHttp会根据响应头中指定的编码来处理字符数据

接下来我们继续实验上面的两条优点

1、实验:OkHttp对Gzip数据的处理

我们在客户端代码中打印一下响应体中的所有响应头:

OkHttpClient().newCall(request).execute().use { response ->
    response.headers()?.names()?.forEach { key -> println("$key=${response.header(key)}")}
    println(response.body()?.string())
}

运行客户端,打印结果如下:

Content-Type=application/json;charset=UTF-8
Date=Mon, 23 Mar 2020 09:22:38 GMT
Server=Apache-Coyote/1.1
{name:"张三",age:18}

姨,奇怪了!服务器明明告诉了客户端数据是经过Gzip压缩的,怎么响应头里看不到Gzip呢?这其是OkHttp把这个Gzip的Header给删除了,因为它已经帮我们把Gzip的数据给解压了,所以Header里面就没必要有Gzip的Header存在,意思就是告诉你数据没有压缩了,你直接使用就行了,千万别自己拿个GZIPInputStream再解压了,如果再解压一次肯定就乱码了。

接下来把服务器端代码中的这行代码注释掉:

response.setHeader("Content-Encoding", "gzip");		

再次运行客户端,打印结果如下:

Content-Length=42
Content-Type=application/json;charset=UTF-8
Date=Mon, 23 Mar 2020 09:23:54 GMT
Server=Apache-Coyote/1.1
       ��K�M�Rz�g���J:��V�� )F��     

乱码,因为服务器没有告诉客户端数据是经过Gzip压缩的,所以OkHttp就不会使用Gzip来解压,不解压直接当字符串来用肯定是乱码啊。

这时,我们把上面注释的代码再恢复回来,修改一下客户端,以告诉服务器我们可以处理Gzip压缩的数据:

val request = Request.Builder().url(url).header("Accept-Encoding", "gzip").build()

再次运行,客户端,打印结果如下:

Content-Encoding=gzip
Content-Length=42
Content-Type=application/json;charset=UTF-8
Date=Mon, 23 Mar 2020 09:25:35 GMT
Server=Apache-Coyote/1.1
       ��K�M�Rz�g���J:��V�� )F��    

从上面打印的响应头中我们看到了关于Gzip的Header,这就说明数据是经过压缩的,需要进行解压,然而我们并没有进行解压,所以数据乱码了,这说明只要我们在请求头里加上Gzip的Header,就表示我们希望要自己处理Gzip,需要自己解压,OkHttp就不会帮我们处理了。这样有什么用呢?有时候想自己解压啊,比如我们在获取数据的时候想显示获取进度的百分比,这种情况就需要自己去读流,而不是OKHttp帮我们读流。

对于Gzip请求头,服务器在收到请求时会读取这个请求头,如果有gzip头,则服务器把数据gzip压缩后再传给客户端,如果没有gzip头,则服务器不会压缩,直接把数据传给客户端。这时你可能会觉得郁闷,如果我不加Gzip请求头,则服务器不会给我压缩数据,如果我加了Gzip请求头,虽然服务器给我压缩数据了,但是OkHttp又不会自动给我解压,怎么解?其实不存在这个问题,虽然我们在请求时没有加入Gzip请求头,但是OkHttp会自动帮我们加入的,怎么验证呢?使用Fiddler抓包工具抓包一看就知道,具体如何抓包这里就不讲解了,大家可以百度一下很多教程的,这里就截图给大家看一下抓包OkHttp请求,OkHttp确实是自动给我们加入了Gzip请求头的:

在这里插入图片描述

2、实验:OkHttp对字符编码的处理

把服务器端的这行代码注释掉:

response.setContentType("application/json; charset=UTF-8"); 

运行客户端,打印结果如下:

Date=Mon, 23 Mar 2020 10:01:57 GMT
Server=Apache-Coyote/1.1
{name:"张三",age:18}

在响应头中看不到关于数据编码的响应头了,但是客户端的中文显示正常,说明OkHttp默认使用UTF-8进行编码,我们再把服务器端设置编码为GBK,如下:

response.setContentType("application/json; charset=GBK"); 

再次运行客户端,打印结果如下:

Content-Type=application/json;charset=GBK
Date=Mon, 23 Mar 2020 10:07:25 GMT
Server=Apache-Coyote/1.1
{name:"寮犱笁",age:18}

结果中文显示为乱码,因为服务器的数据是以UTF-8编码进行传输的,却告诉客户端使用GBK编码来解析,肯定是乱码的。我们修改服务器的数据以GBK进行编码,如下:

byte[] datas = "{name:\"张三\",age:18}".getBytes("GBK");

再次运行,结果如下:

Content-Type=application/json;charset=GBK
Date=Mon, 23 Mar 2020 10:09:40 GMT
Server=Apache-Coyote/1.1
{name:"张三",age:18}

OK,结果正常,这些实验充分说明了OkHttp会以响应头中指定的编码来处理字符数据。

总结

  1. 在发送请求时,OkHttp会自动加入Gzip请求头,当返回的数据带有Gzip响应头时,OkHttp会自动帮我们解压数据。也就是说,对于Gzip,我们不需要做任何处理。如果我们在请求里加入Gzip请求头,则表明我们想要自己处理Gzip数据,此时OkHttp就不会给我们解压数据了。
  2. 在处理字符数据时,如果是使用OkHttp的response.body()?.string()方法,或者使用OkHttp的response.body().charStream()来读取字符,则OkHttp会根据响应头中的编码来处理字符,如果响应头中没有编码,则默认使用UTF-8编码来处理字符,也可以认为对于字符数据的编码我们不需要做任何处理。除非服务器端没有指定字符编码,比如服务器使用GBK编码发送数据,但是又没在响应头中声明编码,则OkHttp会以UTF-8处理则会乱码,这样的情况应该很少出现,除非是小学生写服务器端。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门