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();
相关推荐
追逐时光者4 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_4 小时前
敏捷开发流程-精简版
前端·后端
苏打水com5 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧6 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧6 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧6 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧6 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧6 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng7 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6017 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring