2025年6月8日 星期日 乙巳(蛇)年 三月十二 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

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

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

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处理则会乱码,这样的情况应该很少出现,除非是小学生写服务器端。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门