解决OkHttp接收gzip压缩数据返回乱码问题

 更新时间:2022年06月17日 15:20:44   作者:云飞扬1  
这篇文章主要介绍了解决OkHttp接收gzip压缩数据返回乱码问题,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

问题

Retrofit 是现在最流行的网络开发框架之一,功能十分强大,但是最近确遇到一个十分坑的问题,现在记录下来,希望看到的人能注意下。

众所周知,在 HTTP 传输时是支持 gzip 压缩的,客户端发起请求时在请求头里增加 Accept-Encoding: gzip,服务端响应时在返回的头信息里增加 Content-Encoding: gzip,这表示传输的数据是采用 gzip 压缩的。默认情况下,传输内容是不压缩的,采用 gzip 压缩后可以大幅减少传输内容大小,这样可以提高传输速度,减少流量的使用。

请求头信息

本来 OkHttp 是默认支持 gzip 解压缩的,不需要额外配置的。但是我在拦截器里统一添加了很多请求头信息,大概代码如下:

public class RequestInterceptor implements Interceptor {
    public RequestInterceptor() {
    }
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request.Builder builder = chain.request()
                .newBuilder()
                .addHeader("Accept", "application/json")
                .addHeader("Accept-Encoding", "gzip");
        Request request = builder.build();         
        return chain.proceed(request);
    }
}

以前服务端没有开启 gzip 压缩,一直都没有问题,某天突然运维加了 gzip 压缩,说是为了要省流量带宽,结果就悲剧了,我们 Android APP 里所有的接口都报错了,明明前一秒都是OK的,后一秒就都不能访问了,但是 iOS 里却能正常访问,这是最令人崩溃的事情。

立即进行代码调试,发现 Android 里的 http 请求返回的都是乱码字符串了,其实这些都是 gzip 压缩的数据,不是说 OkHttp 是自动支持 gzip 解压缩的吗?为什么我们的返回数据没有进行 gzip 解压?还有一个奇怪的现象是,当我把这段代码 addHeader("Accept-Encoding", "gzip") 去掉之后,一切又恢复正常了。

BridgeInterceptor拦截器

这是一个很费解的问题,当我手动加上这个头信息时,OkHttp 不会自动解压 gzip 流,当我去掉时 OkHttp 又会自动解压 gzip 流了,秉着刨根究底的精神我翻看了源码,终于找到了原因。原来 OkHttp 在最终构建请求信息以及处理返回信息时,内部使用了一个叫做 BridgeInterceptor 的拦截器,该类的代码如下:

public final class BridgeInterceptor implements Interceptor {
  private final CookieJar cookieJar;
  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }
  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      //自动增添加请求头 Content-Type
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }
      long contentLength = body.contentLength();
      //如果传输长度不为-1,则表示完整传输
      if (contentLength != -1) {
        //设置头信息 Content-Length
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        //如果传输长度为-1,则表示分块传输,自动设置头信息         
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }
    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
    //如果没有设置头信息 Connection,则自动设置为 Keep-Alive
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }
    // 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 && userRequest.header("Range") == null) {
      //如果我们没有在请求头信息里增加Accept-Encoding,在这里会自动设置头信息 Accept-Encoding = gzip
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    Response networkResponse = chain.proceed(requestBuilder.build());
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
    //如果返回的头信息里Content-Encoding = gzip,并且我们没有手动在请求头信息里设置 Accept-Encoding = gzip,则会进行 gzip 解压数据流
    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);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
  }
}

上面代码关键地方我做了注释,OkHttp会额外的增加很多请求头信息,如果我们在代码里没有手动设置Accept-Encoding = gzip,那么OkHttp会自动处理gzip的解压缩;反之,你需要手动对返回的数据流进行gzip解压缩。

以上就是我的代码里 gzip 处理失败的根本原因了,更多关于OkHttp接收gzip压缩数据返回乱码的资料请关注脚本之家其它相关文章!

相关文章

  • Java使用Knife4j优化Swagger接口文档的操作步骤

    Java使用Knife4j优化Swagger接口文档的操作步骤

    在现代微服务开发中,接口文档的质量直接影响了前后端协作效率,Swagger 作为一个主流的接口文档工具,虽然功能强大,但其默认界面和部分功能在实际使用中略显不足,而 Knife4j 的出现为我们提供了一种增强的选择,本篇文章将详细介绍如何在项目中集成和使用 Knife4j
    2024-12-12
  • Spring在代码中获取bean的方法小结

    Spring在代码中获取bean的方法小结

    在工作中有时候我们需要在非spring依赖注入或管理的类中获取service、dao等bean对象,这时候用@Autowired和@Resource显然是不行的,那么下面这篇文章就给大家了整理几种获取bean的方式,对大家的学习和工作具有一定的参考借鉴,下面来一起看看吧。
    2016-11-11
  • mybatis单笔批量保存实体数据的方法

    mybatis单笔批量保存实体数据的方法

    这篇文章主要介绍了mybatis单笔批量保存实体数据的相关知识,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Java生成UUID的常用方式示例代码

    Java生成UUID的常用方式示例代码

    UUID保证对在同一时空中的所有机器都是唯一的,通常平台会提供生成的API,按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字,下面这篇文章主要给大家介绍了关于Java生成UUID的常用方式,需要的朋友可以参考下
    2023-05-05
  • 一文带你熟练掌握Java中的日期时间相关类

    一文带你熟练掌握Java中的日期时间相关类

    我们在开发时,除了数字、数学这样的常用API之外,还有日期时间类,更是会被经常使用,比如我们项目中必备的日志功能,需要记录异常等信息产生的时间,本文就带各位来学习一下相关的日期时间类有哪些
    2023-05-05
  • 利用SpringBoot和LiteFlow解锁复杂流程

    利用SpringBoot和LiteFlow解锁复杂流程

    随着业务的复杂化,企业需要更加高效、便捷地管理自己的业务流程,这就需要借助一些流程引擎实现,今天,我们就来介绍一种基于Java语言开发的轻量级工作流引擎——LiteFlow,以及如何在Spring Boot框架中集成它,从而提高企业的工作效率和开发效率
    2023-06-06
  • MyBatis-Plus 分页查询的实现示例

    MyBatis-Plus 分页查询的实现示例

    本文主要介绍了MyBatis-Plus 分页查询的实现示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Springboot实现人脸识别与WebSocket长连接的实现代码

    Springboot实现人脸识别与WebSocket长连接的实现代码

    这篇文章主要介绍了Springboot实现人脸识别与WebSocket长连接的实现,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-11-11
  • 详解JAVA后端实现统一扫码支付:微信篇

    详解JAVA后端实现统一扫码支付:微信篇

    本篇文章主要介绍了详解JAVA后端实现统一扫码支付:微信篇,这里整理了详细的代码,有需要的小伙伴可以参考下。
    2017-01-01
  • 基于Java实现简单贪吃蛇游戏

    基于Java实现简单贪吃蛇游戏

    这篇文章主要为大家详细介绍了基于Java实现简单贪吃蛇,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04

最新评论