ichengzi / ichengzi.github.io

ichengzi's blogs and learn notes
https://ichengzi.github.io
1 stars 0 forks source link

okhttp小结 #48

Open ichengzi opened 2 years ago

ichengzi commented 2 years ago

okhttp 是一个java里比较常用的 httpClient, 支持同步、异步请求,socket连接池,资源缓存等。

以下基于3.10讨论。

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.10.0</version>
</dependency>

OkHttpClient

如下,okHttpClient 有几个核心字段

  1. Dispatcher, 拥有一个线程池,异步http请求时使用
  2. ConnectionPool, 拥有一个线程池,最大一个线程
  3. connectTimeout,readTimeout, writeTimeout, 各种超时配置

image

Dispatcher

  1. Dispatcher 有一个线程池,是延迟初始化的,有异步请求时会触发创建线程池
  2. 线程数量0~无限大, 但是队列长度为1, 只有task被消费后,新task才能进入

image

  1. 这里就利用了ThreadPoolExecutor 线程数量可以为0的技巧, 如下是ThreadPoolExecutor.execute()方法。
  2. 因为 coreSize ==0, 不会执行1;
  3. 执行2时, 如果task入队成功;如果线程数为0,则新增一个线程; 如果线程数不为0,则流程结束,等待工作线程来取task;
  4. 如果task入队失败,那么将创建新的线程执行task。 这里实现实现了task提交后就可以立即执行,不用在队列中等待
  5. 因为这里的线程数量没有限制,队列长度又为1,所以线程数量实际是添加task的代码在控制,如果不加限制不停添加task,那么线程数量会很快爆炸,应用崩溃
  6. okHttp 是靠 Dispatcher.maxRequests Dispatcher.maxRequestsPerHost 来控制同时执行的异步请求,也即线程数量
  7. synchronized void enqueue(AsyncCall call) Dispatcher的enqueue是同步执行的,所以不会出现因并发导致的线程数量失控

image

ConnectionPool

ConnectionPool 实现了socket连接共享,以及失效超时连接的释放,核心是一个线程池

  1. 清理失效连接的task是个死循环,当连接已经全被清除时, task结束,线程释放
  2. 新增连接时, 检测是否拿到了锁,再检测是否有清理task, 没有的话,就添加一个清理task
  3. 这里的put是非同步的,但是外部调用 put() 是同步的,其实相当于同步的效果
  4. 这里的线城池和线上相反,最大一个线程,但是task队列是无限大的。 但是通过控制添加task, 实现最多只有一个task在运行

image image

小结

  1. 一个okHttpClient 拥有两个线程池,coreSize都设置为0, 线程都设置了最大空闲时间,所以线程都可以自动释放为0
  2. 请求线程池只有异步请求才会使用,所以是延迟创建的,只有异步请求才创建。 清理过期连接的线程池是直接创建的,因为只要有请求,就会触发清理task。
  3. 最大并发连接数 , 连接超时时间,socket过期时间 都会影响线程的数量和是生命周期,这个有不少坑,要根据自己的业务需求合理配置,默认的可能不是最合适的
  4. 这里的两个线程池都不是常规意义上的线程池,没有队列长度,没有拒绝策略等等。 线程数量都是靠 execute() 添加task来控制的。 这里要注意
  5. 因为这个, okHttpClient 是一个很重的对象,必须单例, 避免资源浪费。

关于超时配置

  1. 但是 okHttpClient 选择把超时配置放在 client里,而不是 request里,导致不同超时配置的request使用起来麻烦, 很容易出错。
  2. 超时配置放到client里, request就不用每次都配置, 使用时简单了点
  3. 超时配置放到reqest里, 每次都要明确配置,使用时麻烦了点
  4. 这个各有利弊,如果改进设计的话, 我会在reqeust中添加超时配置, 实际请求时,优先读取request的超时配置, 未配置时fallback到client的配置
  5. 避免为了不同的超时配置,再创建一个client

使用示例

    private static final OkHttpClient _client = new OkHttpClient();

    public static String httpGet(String url, Long connectTimeout, Long readTimeout) throws Exception {
        OkHttpClient client = _client.newBuilder()
                .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
                .build();
        Request request = new Request.Builder().url(url).build();
        Response response = client.newCall(request).execute();
        return response.body().string();
    }

client非单例情况

假设非单例使用,并且都是同步请求,在默认的配置情况下。

当短期内有大量的请求时1000/min,每次都new一个client,发起一次请求。 那么仅默认的清理线程就有1000个,会引起系统异常。

如果请求时异步的,那么就会有2000个线程, 这个线程消耗情况就更糟糕了。

如果是默认情况配置下, 一个线程线程栈1MB, 2000* 1MB ~~ 2GB, 这个内存消耗更糟糕了。

请求线程, 需要在请求完成后, 1min后才能释放。

清理线程, 需要在socket 5min过期后, 再等1min, 共 5min+ 1min后才能释放。

eg: https://segmentfault.com/a/1190000014498489

线程名称

// 请求线程,  “OkHttp Dispatcher”
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));

// 清理线程, "OkHttp ConnectionPoo"
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));