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();
相关推荐
指令集梦境1 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
码云之上2 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js
IT_陈寒3 小时前
Vite项目build后路由404了?你可能漏了这个小配置
前端·人工智能·后端
宸津-代码粉碎机3 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
吴佳浩3 小时前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
喵个咪4 小时前
实时游戏网络协议深度对比:KCP vs WebRTC vs WebSocket
后端·websocket·webrtc
普通网友4 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
QuZero4 小时前
Guava Cache Deep Dive
java·后端·算法·guava
leeyi4 小时前
SSE 实时推流 —— Token 怎么一个个蹦出来
后端·agent
leeyi4 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
后端·agent