OkHttp源码:gzip压缩和OkHttp调优

一、透明压缩

OkHttp支持透明压缩。在构建请求时,我们无需设置请求头Accept-Encoding,在BridgeInterceptor.intercept()方法中会主动添加请求头Accept-Encoding:gzip 布尔值transparentGzip只有在我们没设置Accept-EncodingRange为null(即不是分批下载或断点续传)时,才会为true。进而当响应带有Content-Encoding:gzip时,OkHttp将自动帮我们解压数据,并移除Content-Encoding响应头(避免调用方再次解压响应体 )。 可见,如果我们主动添加了Accept-Encoding:gzip请求头,transparentGzip=false,OkHttp将不会尝试解压响应体,需要我们自己处理。

二、OkHttp调优

2.1 Interceptors和NetworkInterceptors区别

OkHttpClient.Builder可以通过addInterceptor和addNetworkdInterceptor添加自定义的拦截器。虽然NetworkdInterceptor也是指Interceptor接口实例,当两者作用范围有区别。

  • Interceptors位于拦截器链的头部,用于观察HTTP请求的整个过程,从连接建立到获得响应。
  • NetworkdInterceptors位于CallServerInterceptor之前,仅用于观察网络I/O阶段。 可见,在请求重试或重定向时,Interceptors不会被再次执行,而NetworkdInterceptors将被再次执行。

2.2 复用OkhttpClient

反例

如下面代码,某个公司对Okhttp进行封装,每发起一个HTTP请求,都会创建一个ThirdHttpInvoker实例,进而新建一个OkhttpClient实例。 这儿创建OkhttpClient实例时,没有主动提供ConnectionPool,将由框架创建一个新的连接池,而每个连接池都有一个cleanup线程。 当程序对外HTTP请求并发高时,将频繁创建没有复用的OkhttpClient实例、连接池、Dispatcher异步请求线程池等,浪费资源;且同一域名连接不能复用,严重降低了Okhttp性能

正确做法

使用Okhttp时,OkhttpClient应当是全局单列。还可以自己配置并创建单例的ConnectionPool、Dispatcher。

java 复制代码
public static final ConnectionPool POOL = new ConnectionPool(256, 5, TimeUnit.MINUTES);

public static final Dispatcher DISPATCHER = new Dispatcher();
static {
  // AsyncCall并发度,默认为64
  DISPATCHER.setMaxRequests(256);
  // 对每个域名AsyncCall并发度,默认为5
  DISPATCHER.setMaxRequestsPerHost(32);
  // Dispatcher空闲时的回调逻辑
  DISPATCHER.setIdleCallback(() -> {
    // do something
  });
}

public static final OkHttpClient client = new OkHttpClient.Builder()
    .connectionPool(POOL)
    .dispatcher(DISPATCHER)
    .connectTimeout(5L, TimeUnit.SECONDS)
    .readTimeout(20L, TimeUnit.SECONDS)
    .build();

2.3 并发度调整

Dispatcher类有两个属性,限制了OkhttpClient实例异步请求最大并发数、对每个Host异步请求的最大并发数。超过限制时,请求需要在队列中排队依次等候。 并发度默认值较低,我们应当根据实际场景进行调整。

java 复制代码
// 异步请求最大并发数
private int maxRequests = 64;
// 每个Host异步请求最大并发数
private int maxRequestsPerHost = 5;

2.4 自定义请求超时时间

设置全局超时时间

在创建OkHttpClient时,我们可以设置3个超时时间(默认都是10秒),很显然这是全局设置。

java 复制代码
public static final OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(5L, TimeUnit.SECONDS)
    .writeTimeout(20L, TimeUnit.SECONDS)
    .readTimeout(20L, TimeUnit.SECONDS)
    .build();

处理每个请求时会创建Interceptor.Chain,将超时配置传递给了拦截器链。 当执行ConnectInterceptor拦截逻辑时,ExchangeFinder#findConnection中创建Socket连接,使用了超时配置。

  • writeTimeout在使用https时才会生效,对http请求writeTimeout会被忽略;
  • connectTimeout、readTimeout是Socket的方法参数。
java 复制代码
// java.net.Socket中方法
Socket.connect(address, connectTimeout);
Socket.setSoTimeout(readTimeout);

而真实场景中,不同请求对超时时间可能会有不同要求,如何设置请求级别超时时间呢?

设置请求级别超时

超时时间是通过接口Chain向下传递,而该接口提供了3个超时时间的赋值器。那么,我们可以自定义一个Interceptor,将通过请求头声明的超时时间设置给Chain,来覆盖全局的超时设置。 例如RequestTimeoutInterceptor实现,在创建OkHttpClient时引用它。

java 复制代码
public static final OkHttpClient client = new OkHttpClient.Builder()
    // 全局默认超时设置
    .connectTimeout(5L, TimeUnit.SECONDS)
    .writeTimeout(20L, TimeUnit.SECONDS)
    .readTimeout(20L, TimeUnit.SECONDS)
    // 处理请求级别超时设置
    .addInterceptor(new RequestTimeoutInterceptor())
    .build();
java 复制代码
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
 * 请求级别超时时间拦截器
 */
public class RequestTimeoutInterceptor implements Interceptor {

  // 超时时间请求头,单位毫秒
  public static final String CONNECT_TIMEOUT = "connectTimeout";
  public static final String WRITE_TIMEOUT = "writeTimeout";
  public static final String READ_TIMEOUT = "readTimeout";

  @Override
  public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    Request.Builder builder = request.newBuilder();
    String connectTimeoutStr = request.header(CONNECT_TIMEOUT);
    // 请求没有声明超时时间,则使用全局配置
    int connectTimeout = chain.connectTimeoutMillis();
    if (Objects.nonNull(connectTimeoutStr)) {
      connectTimeout = Integer.parseInt(connectTimeoutStr);
      builder.removeHeader(CONNECT_TIMEOUT);
    }

    String writeTimeoutStr = request.header(WRITE_TIMEOUT);
    int writeTimeout = chain.writeTimeoutMillis();
    if (Objects.nonNull(writeTimeoutStr)) {
      writeTimeout = Integer.parseInt(writeTimeoutStr);
      builder.removeHeader(WRITE_TIMEOUT);
    }

    String readTimeoutStr = request.header(READ_TIMEOUT);
    int readTimeout = chain.readTimeoutMillis();
    if (Objects.nonNull(readTimeoutStr)) {
      readTimeout = Integer.parseInt(readTimeoutStr);
      builder.removeHeader(READ_TIMEOUT);
    }

    return chain.withConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
        .withWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS)
        .withReadTimeout(readTimeout, TimeUnit.MILLISECONDS)
        .proceed(builder.build());
  }
}

创建Request对象时,添加超时时间请求头。

java 复制代码
Request request = new Request.Builder()
    .url("http://publicobject.com/helloworld.txt")
    .addHeader(RequestTimeoutInterceptor.CONNECT_TIMEOUT, "3000")
    .addHeader(RequestTimeoutInterceptor.WRITE_TIMEOUT, "10000")
    .addHeader(RequestTimeoutInterceptor.READ_TIMEOUT, "10000")
    .build();
相关推荐
名字还没想好☜13 分钟前
Spring Boot 优雅停机实战:等在途请求处理完再退出,配合 K8s preStop 别丢请求
java·后端·spring
摇滚侠18 分钟前
《Spring Boot 3:高级与架构设计》第 1 章 Bean 与 BeanDefinition 个人理解 2
java·spring boot·笔记·后端
IT_陈寒34 分钟前
Redis主从切换竟让业务卡了3秒?这个坑我替你踩了
前端·人工智能·后端
右耳朵猫AI1 小时前
Java周刊2026W38 | Micronaut 修补三漏洞、JDK 27 提速 54%、Jetty 修复抖动测试
java·后端·spring
步行cgn1 小时前
BeanFactory 与 FactoryBean 的区别:面试深度解析
java·后端·spring
mldong1 小时前
一套审批流要写多少代码:13 个框架的接入 diff 我数了一遍,最少 422 行,最多 1798 行
后端·架构
GreenTea10 小时前
GrokBot 核心成员 Lauren Tan:每月交付 2000 个 PR 的人,是怎么用 AI 的
前端·后端·架构
码事漫谈10 小时前
FDE:一个缩写,两种命运
后端
名字还没想好☜11 小时前
Go context.AfterFunc 实战(Go 1.21):context 一取消就自动跑清理,告别手写 goroutine 监听 Done
后端·golang·go
明月_清风12 小时前
为什么最近开始关注 JEV?几个实战案例告诉你答案
人工智能·后端