稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

一文带你理解OkHttp中的Gzip压缩

本文译自OkHttp’s Gzip Compression,原文发布于2020年5月1日。本文假定读者对HTTP有基本的了解,以及使用过OkHttp,否则理解起来可能会一点困难。

译注: OkHttp确实会自动添加gzip并处理响应。但如果显示的给请求添加除了gzip之外的字段,如”Accept-Encoding: gzip, deflate, br”,那么就需要手动处理响应。其实一般时候我们并不需要手动设置,除非是模拟浏览器时(比如用了浏览器的UA),或者某些服务器强制deflate字段。更多的信息可以看这里

压缩是一种简单有效的节省带宽和加快移动用户交互速度的方法。当用户点击你的页面屏幕时,会调用你的服务器来提供请求的响应。响应越大,屏幕上显示数据的时间就越长。通过压缩,即使你的访问者的互联网连接速度非常慢并且你的 API 响应过于繁重,他们也能享受快速加载。

这是如何工作的呢?

Gzip 会找到相似的字符串,并用一些占位符临时替换这些字符串,以缩小整体大小。如果你使用大量重复文本,并且有大量空格, 这也没有问题。你可以使用 Gzip 压缩你的响应主体以及请求主体。由于文件小得多,此操作可大大减少传输时间。

注意:如果你尝试使用postman,它会默认在header的隐藏部分中添加 Accept-Encoding: gzip字段。

重要提示:OkHttp 也会自动在请求中添加字段 Accept-Encoding 并自动识别响应中的 Content-Encoding,因此会自行解压缩响应数据,因此无需单独设置,但假设当我们必须将压缩的请求数据发送到服务器时,我们就必须编写自己的拦截器。

这种压缩的棘手之处在于请求者和服务器都知道可以发送压缩文件。你必须告诉服务器您接受这种编码,然后它才会提供。该协议分为两部分:

  • 请求者发送一个header,告知服务器它接受压缩内容:Accept-Encoding:gzip
  • 服务器使用此header确认你的请求:Content-Encoding:gzip

说得够多的了,我想你还有耐心,所以让我们看一些代码。

编写自己的拦截器

先决条件:了解 OkHttp 中的拦截器(Interceptors)

  1. 解开 Gzip 响应:在请求header中添加 Accept-Encoding: gzip,并在获取响应时在其响应header中查找 Content-Encoding: gzip。如果存在则解压缩,否则直接返回响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.io.IOException;

import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.GzipSource;
import okio.Okio;

public class GzipInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request.Builder newRequest = chain.request().newBuilder();
        newRequest.addHeader("Accept-Encoding", "gzip");
        Response response = chain.proceed(newRequest.build());

        if (isGzipped(response)) {
            return unzip(response);
        } else {
            return response;
        }
    }

    private Response unzip(final Response response) throws IOException {

        if (response.body() == null) {
            return response;
        }

        GzipSource gzipSource = new GzipSource(response.body().source());
        String bodyString = Okio.buffer(gzipSource).readUtf8();

        ResponseBody responseBody = ResponseBody.create(response.body().contentType(), bodyString);

        Headers strippedHeaders = response.headers().newBuilder()
                .removeAll("Content-Encoding")
                .removeAll("Content-Length")
                .build();
        return response.newBuilder()
                .headers(strippedHeaders)
                .body(responseBody)
                .message(response.message())
                .build();

    }

    private Boolean isGzipped(Response response) {
        return response.header("Content-Encoding") != null && response.header("Content-Encoding").equals("gzip");
    }
}
  1. 创建 Gzip 请求:如果你的请求过大,那么我们可以使用它来压缩我们的请求。为了通知服务器,我们将在请求header中添加 Content-Encoding: gzip。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import okio.GzipSink;
import okio.Okio;

public class GzipInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Request originalRequest = chain.request();

        if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
            return chain.proceed(originalRequest);
        }

        Request compressedRequest = originalRequest.newBuilder()
                .header("Content-Encoding", "gzip")
                .method(originalRequest.method(), gzip(originalRequest.body()))
                .build();
        return chain.proceed(compressedRequest);
    }

    private RequestBody gzip(final RequestBody body) {
        return new RequestBody() {
            @Override public MediaType contentType() {
                return body.contentType();
            }

            @Override public long contentLength() {
                return -1; // 事先不知道请求内容的长度
            }

            @Override public void writeTo(BufferedSink sink) throws IOException {
                BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
                body.writeTo(gzipSink);
                gzipSink.close();
            }
        };
    }
}

好了,就到这里吧,如果你愿意的话,可以赞一下。

参考资料:

  1. https://square.github.io/okhttp/
  2. https://www.apphp.com/tutorials/index.php?page=gzip-and-deflate- compression-in-web-development
  3. https://en.wikipedia.org/wiki/Gzip
  4. https://www.youtube.com/watch?v=Mjab_aZsdxw

Comments