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();
相关推荐
paopaokaka_luck3 分钟前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn2 小时前
依赖注入注解
java·后端·spring
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪2 小时前
Django:从入门到精通
后端·python·django
一个小坑货2 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet272 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom2 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep3 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE3 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang