OkHttpClient源码——同步/异步请求

如官网(square.github.io/okhttp/) 介绍,OkHttpClient有很多优点。

OkHttp是一个HTTP客户端,以下特性默认有效:

  • HTTP/2支持允许所有请求到同一主机共享一个套接字。
  • 连接池减少了请求延迟(如果HTTP/2不可用)。
  • 透明GZIP缩小下载大小。
  • 响应缓存完全避免了网络的重复请求。

当网络出现问题时,OkHttp会从常见的连接问题中静默地恢复。如果服务有多个IP地址,如果第一次连接失败,OkHttp将尝试替代地址。

OkHttp非常易用,请求/响应API设计具有链式调用和不变性。它既支持同步阻塞调用,也支持带回调的异步调用。

现在,我们一起来看看OkHttpClient源码实现,本文采用3.14.0版本。

js 复制代码
<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.14.0</version>
</dependency>

一、执行同步请求

先看一个同步请求示例。

java 复制代码
public static OkHttpClient client = new OkHttpClient.Builder()
  .connectTimeout(10L, TimeUnit.SECONDS)
  .readTimeout(30L, TimeUnit.SECONDS)
  .build();

private static String doPost(String url, String param) throws IOException {
  RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), param);
  Request request = new Request.Builder()
      .url(url)
      .post(requestBody)
      .addHeader("Accept-Type", "application/json")
      .build();
  try (Response response = client.newCall(request).execute()) {
    if (response.isSuccessful()) {
      ResponseBody body = response.body();
      return Objects.nonNull(body) ? body.string() : "";
    }
    return "";
  }

同步请求整个流程如下图。

创建OkHttpClient

首先,需要创建一个OkHttpClient,可使用OkHttpClient.Builder内部类来构建。当采用new OkHttpClient()时,所有属性都使用默认值。

java 复制代码
OkHttpClient的几个重要属性
  // 调度器
  final Dispatcher dispatcher;
  // 网络代理
  final @Nullable Proxy proxy;
  // 连接器,默认为空
  final List<Interceptor> interceptors;
  // 事件监听器
  final EventListener.Factory eventListenerFactory;
  // 响应缓存
  final Cache cache;
  // 连接池
  final ConnectionPool connectionPool;
  // 连接失败是否重试,默认为TRUE
  final boolean retryOnConnectionFailure;
  // 总超时时间,包括connect, write, read在内,默认为0即无限制
  final int callTimeout;
  // 默认为10秒
  final int connectTimeout;
  // 默认为10秒
  final int readTimeout;
  // 默认为10秒
  final int writeTimeout;
java 复制代码
  public OkHttpClient() {
    this(new Builder());
  }

RealCall同步执行和Interceptor实现

执行client.newCall(Request request),会调用RealCall.newRealCall()创建RealCall对象。

java 复制代码
  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }
java 复制代码
final class RealCall implements Call {
  final OkHttpClient client;
  private Transmitter transmitter;
  final Request originalRequest;
  final boolean forWebSocket;
  private boolean executed;
  // 省略......
  
  @Override
  public Response execute() throws IOException {
    // 防止并发
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.timeoutEnter();
    transmitter.callStart();
    try {
      // 放入runningSyncCalls队列
      client.dispatcher().executed(this);
      // 通过拦截器链执行
      return getResponseWithInterceptorChain();
    } finally {
      client.dispatcher().finished(this);
    }
  }
}

RealCall中构建了InterceptorChain,开始执行,在最后一个拦截器CallServerInterceptor中发出网络请求。

java 复制代码
    Response getResponseWithInterceptorChain() throws IOException {
    // 添加拦截器
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    // 默认添加的拦截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

        // 构建拦截器链
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
        // 执行
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }
  • RetryAndFollowUpInterceptor:将请求从失败中恢复,并在必要时执行重定向
  • BridgeInterceptor:补充请求头,处理gzip编码的响应
  • CacheInterceptor:尝试从缓存中获取响应,没有是继续执行连接器链,并将响应缓存。使用文件系统存放数据。
  • ConnectInterceptor:与目标主机建立Socket连接
  • CallServerInterceptor:最后一个拦截器,发起网络请求,等待并构建响应。

Transmitter

OkHttp的应用程序层和网络层之间的桥梁。该类公开了高级应用层原语:连接、请求、响应和流。

Exchange

交换机,处理单个HTTP请求和响应,负责连接管理和事件,通过ExchangeCodec处理网络I/O。

ExchangeCodec

编码请求、解码响应,支持HTTP/1.1、HTTP/2.0,处理网络I/O。

二、执行异步请求

下面是一个异步请求的示例,回调逻辑是Callback接口的一个匿名内部类。

java 复制代码
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class OkHttp3ClientTest {
public static OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(10L, TimeUnit.SECONDS)
        .readTimeout(30L, TimeUnit.SECONDS)
        .build();

  public static void main(String[] args) throws IOException {
    doAsyncGet("http://localhost:8010/user/queryAll");

  }

  private static void doAsyncGet(String url) {
    Request request = new Request.Builder()
        .url(url)
        .get()
        .addHeader("Accept-Type", "application/json")
        .build();

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

      }

      @Override
      public void onResponse(Call call, Response response) throws IOException {
        assert response.body() != null;
        System.out.println(response.body().string());
      }
    });
  }
}

异步请求整个流程如下图。

创建AsyncCall

通过RealCall.enqueue(Callback responseCallback),指定回调逻辑,构建AsyncCall对象,启动异步请求,该方法无返回值。

java 复制代码
@Override public void enqueue(Callback responseCallback) {
  // 防止重复执行
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.callStart();
  // 封装AsyncCall,放入readyAsyncCalls队列
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

AsyncCall间接实现了Runnable接口,run()方法中调用了AsyncCall.execute(),发起http请求也是调用上面看过的getResponseWithInterceptorChain(),只是增加了成功或失败的回调。

scala 复制代码
final class AsyncCall extends NamedRunnable {
  private final Callback responseCallback;
  // 对某个host的调用计数
  private volatile AtomicInteger callsPerHost = new AtomicInteger(0);
  // 省略......
  
  @Override protected void execute() {
    boolean signalledCallback = false;
    transmitter.timeoutEnter();
    try {
      // 与同步请求时逻辑相同
      Response response = getResponseWithInterceptorChain();
      signalledCallback = true;
      // 成功回调
      responseCallback.onResponse(RealCall.this, response);
    } catch (IOException e) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
      } else {
        // 失败回调
        responseCallback.onFailure(RealCall.this, e);
      }
    } finally {
      // 其中会将host并发计数减1
      client.dispatcher().finished(this);
    }
  }
}

Dispatcher调度

该类用于调度异步请求,实现并发度控制。 Dispatcher.enqueue(AsyncCall call)中,通过Dispatcher.promoteAndExecute()触发消费readyAsyncCalls队列中的AsyncCall

每个Dispatcher都持有一个线程池executorService(使用SynchronousQueue),限制了并发执行的最大请求数为64(超过时将排队等候),限制对每个host的最大并发请求数为5。 因此,请求并发度控制,并不是通过线程池中线程数来限制,而是通过执行中的AsyncCall数量来限制。

java 复制代码
public final class Dispatcher {
    private int maxRequests = 64;
    private int maxRequestsPerHost = 5;
    // 队列容量为0,不暂存任务。如果有空闲线程则复用,没有则创建新线程来处理任务
    private ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
        new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));

    // 返回Dispatcher是否在运行中
    private boolean promoteAndExecute() {
      assert (!Thread.holdsLock(this));

      // 筛选一些可执行的异步请求
      List<AsyncCall> executableCalls = new ArrayList<>();
      boolean isRunning;
      synchronized (this) {
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
          AsyncCall asyncCall = i.next();

          if (runningAsyncCalls.size() >= maxRequests) break; // 超过请求并发限制时,不会立即处理新的AsyncCall。
          if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // 超过host请求并发限制

          i.remove();
          // 对某个host的正在进行中的请求数+1
          asyncCall.callsPerHost().incrementAndGet();
          executableCalls.add(asyncCall);
          // 移动到runningAsyncCalls队列
          runningAsyncCalls.add(asyncCall);
        }
        isRunning = runningCallsCount() > 0;
      }

      // 遍历执行每个异步请求
      for (int i = 0, size = executableCalls.size(); i < size; i++) {
        AsyncCall asyncCall = executableCalls.get(i);
        // 向executorService提交AsyncCall
        asyncCall.executeOn(executorService());
      }

      return isRunning;
    }
    
    // 省略......
}

当一个AsyncCall执行结束时,会调用Dispatcher.finished()方法从队列中移除该call,同时触发Dispatcher.promoteAndExecute()继续消费readyAsyncCalls队列。

下一篇中,我们一起来看看连接池、响应缓存等特性的实现。

相关推荐
XINGTECODE14 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码19 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶20 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺24 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序31 分钟前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、1 小时前
Spring Boot 注解
java·spring boot
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring