Java Http客户端之OkHttp

OkHttp

Posted by Bug1024 on June 6, 2020

HTTP客户端

java.net 包中提供了一些基本的方法,使用过 http 协议来访问网络资源,但是操作起来比较繁琐不够灵活,因此诞生了一些优秀的HTTP客户端工具,包括 Apache HttpClient、OkHttp,在笔者接触过的项目里又以 OkHttp 使用更加广泛,所以本次重点介绍 OkHttp

OkHttp

关于 OkHttp 这里直接贴官网介绍(https://square.github.io/okhttp/)

  • HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth.OkHttp is an HTTP client that’s efficient by default:
  • HTTP/2 support allows all requests to the same host to share a socket.
  • Connection pooling reduces request latency (if HTTP/2 isn’t available).
  • Transparent GZIP shrinks download sizes.
  • Response caching avoids the network completely for repeat requests

如何使用OkHttp

这里依然贴官网 demo(https://square.github.io/okhttp/recipes/) 同步调用

 private final OkHttpClient client = new OkHttpClient();
  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
      Headers responseHeaders = response.headers();
      for (int i = 0; i < responseHeaders.size(); i++) {
        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
      }
      System.out.println(response.body().string());
    }
  }

异步调用

  private final OkHttpClient client = new OkHttpClient();
  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        try (ResponseBody responseBody = response.body()) {
          if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

          Headers responseHeaders = response.headers();
          for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
          }

          System.out.println(responseBody.string());
        }
      }
    });
  }

OkHttp实例创建

从 demo 里可以得知使用 OkHttp 之前需要 new 一个 OkHttpClient,至于如何创建,OkHttp 实际上提供了 Builder 方式进行个性化的配置,一份比较完整的配置通常是这样的(配置里的参数使用的是自带的默认值,OkHttp 版本3.11.0):

 public OkHttpClient buildOkHttpClient() {
        ExecutorService executor = new ThreadPoolExecutor(50, 200, 30, TimeUnit.SECONDS,
                new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
        Dispatcher dispatcher = new Dispatcher(executor);
        dispatcher.setMaxRequests(64);
        dispatcher.setMaxRequestsPerHost(5);

        return new OkHttpClient.Builder()
                .connectTimeout(10_000, TimeUnit.MILLISECONDS)
                .readTimeout(10_000, TimeUnit.MILLISECONDS)
                .writeTimeout(10_000, TimeUnit.MILLISECONDS)
                .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
                .dispatcher(dispatcher)
                .build();
    }

Dispatcher,直接查看源码注释

Policy on when async requests are executed. Each dispatcher uses an {@link ExecutorService} to run calls internally. If you supply your own executor, it should be able to run {@linkplain #getMaxRequests the configured maximum} number of calls concurrently.

概括来说就是异步调用时 OkHttp 底层实际会使用一个默认的线程池,这个线程池源码为:

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

最大线程数为 Integer.MAX_VALUE,这个是不是太危险了?不过异步调用的时候实际是会经过以下方法的:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

也就是在往这个线程池提交任务的时候是会先做判断的,这里涉及到 maxRequests 和 maxRequestsPerHost 两个配置参数,见名知意,maxRequests 是允许最大的并发数,maxRequestsPerHos 是每个调用域名允许最大的并发数,超过配置的并发数则进入一个就绪队列,后面细节就不具体展开讲,感兴趣可以查看网友的源码分析:https://www.jianshu.com/p/6166d28983a2

ConnectionPool 是指创建的连接池,用源码的注释来说

Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that share the same {@link Address} may share a {@link Connection}. This class implements the policy of which connections to keep open for future use

如果要复用 HTTP 连接,那么可以通过 maxIdleConnections 进行控制,即允许最大的空闲连接数,这个连接数只针对同一个host有效,并非针对总的连接数,需要注意的是当 response header 头里如果返回Connection: close,那么是无法复用连接的,不仅笔者踩过这个坑,也有网友踩过这个坑,具体可见具:https://stackoverflow.com/questions/41011287/why-okhttp-doesnt-reuse-its-connections

而大部分人关注的更多的是 OkHttp 的几个超时时间:

connectTimeout

底层 socket 连接建立的超时时间,当连接 IP 不可达的情况下,需要等待很长一段时间(默认时长)

readTimeout

读超时时间,Source(类似 InputStream)的超时时间,这个时间也是大家普遍认为的第三方接口调用超时时间

writeTimeout

写超时时间, Sink(类似 OutputStream)超时时间,从远程获得数据后再往 socket 写的时间,这个耗时通常非常少,笔者人为打了断点才模拟触发

总结

  • dispachter 异步调用时才有用,通过 maxRequests (允许最大的并发数),maxRequestsPerHos (每个调用域名允许最大的并发数)来进行并发控制
  • connectTimeout 与远程 host 建立连接的超时时间
  • readTimeout 通常意义上的从远程接口获取到数据的超时时间
  • writeTimeout 获得数据后往 socket 写的超时时间
  • connectionPool 用于管理HTTP连接,同一个 host 达到复用的效果降低开销,通过maxIdleConnections 控制同一个 host 允许的最大空闲连接数
  • response header头里返回Connection: close,那么是无法复用连接的