Gzip压缩使用起来很简单,以前我也只是在客户端使用,服务器端不用管,所以我只用过GZIPInputStream来读取,用起来也没有问题。后来OkHttp开始流行,后来听说OkHttp会自动处理Gzip压缩的数据,不需要我们使用GZIPInputStream来处理,于是我想验证一下是否真的是这样的,这时我就需要写个服务器端Demo了,发现行不通,会报错,找不到原因,老办法,先写个最简单Demo,把Gzip流用起来,整体思路如下:
具体代码如下(使用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压缩后再上传,节省流量、加快访问、提高用户体验耿耿的!
听说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的优点时这里就有两点可说了:
接下来我们继续实验上面的两条优点
我们在客户端代码中打印一下响应体中的所有响应头:
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请求头的:
把服务器端的这行代码注释掉:
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会以响应头中指定的编码来处理字符数据。