OkHttp 源码分析 (一) 整体设计

902 阅读12分钟

为什么选择OkHttp

  • 默认支持了Http2.0,以支持2.0的所有优点
  • 连接池减少了请求延迟(如果 HTTP/2 不可用)
  • 进行透明 GZIP 可缩小下载大小
  • 默认实现了前端的缓存,响应缓存完全避免网络重复请求
  • 良好的功能封装和整体结构解耦

源码分析目的

网络知识对于一个程序员是非常重要的。我们可以通过书籍学习网络知识,但是最好的学习方法还是阅读源码。
平时的开发,虽然底层都是网络层的TCP/UDP等,但是这些都是内核实现好的组件,不能进行自由的定制,我们能接触操作到的是应用层协议,也就是最常见的HTTP/FTP等。可以自由的实现自己的HTTP框架。开发自己的Http库。
作为安卓的开发者,耳熟能详的网络请求框架就是OKHttp了,他已经被集成到了安卓的系统源码中,代替了HttpClient,可见它的权威性。所以安卓的开发者可以通过阅读OKHttp的源码学习网络知识,这是一条捷径,站在巨人的肩膀上。
我们不光要学习网络知识,还可以学习一下OKHttp的设计架构知识。这系列文章,会先从顶层使用上简单介绍下每个组件,对整个网络请求的流程有一个大致的了解,知道每一个组件的意义和使用方法。后面的章节再详细介绍每一个组件。由浅入深。
看完这系列文章,相信你肯定对OKHttp的内部有了很深入的理解。 我们先从框架外部的的使用讲起。
源码分析基于 com.squareup.okhttp3:okhttp:3.12.6版本。

使用OkHttp

作为一个框架,简单的使用是非常简单的。只需要简单的几步就可以。先给出要请求的url,然后就可以轻松的拿到网络请求的数据。

OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url("请求的地址").build();
Call call = okHttpClient.newCall(request);

//同步请求
try {
    Response response = call.execute();
} catch (IOException e) {
    e.printStackTrace();
}

//异步请求
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        //异步请求失败回调
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        //异步请求成功回调
    }
});

通过上面的简单的请求代码,可以看出总共使用了3个对象:OkHttpClient、Request、Call。最后通过Call的同步execute或者异步enqueue方法进行请求,拿到Response,请求就结束了。我们逐一了解下各个模块的功能和用途。以后的章节会详细讲解各个模块。

OkHttp顶层模块介绍

1. OkHttpClient

1.1 职责

OkHttpClient主要负责两部分功能:

  1. 负责创建Call,作为Call的工厂。通过静态工厂newCall方法创建一个Call对象,Call是一个接口,具体实现类是NewCall,Call主要负责请求的发送和接受,下面会具体讲解。
  2. 接受一些外部可配置的参数,相当于一个配置类。 框架建议我们共享一个OkHttpClient对象,因为可以重用连接池ConnectionPool和线程池,提高性能。如果每个连接都创建一个OkHttpClient,那么这些资源就浪费了。我们可以直接使用构造方法创建一个OkHttpClient,或者使用Builder模式创建一个OkHttpClient。当然使用构造方法内部也是使用了一个拥有默认参数的Builder。

OkHttpClient内部的属性都是包私有的,并且都是final的,这是一个比较好的实践,尽一切可能减小可见性和可变形。

1.2 实现细节

上面说了OkHttpClient接受一些外部可配置的参数,主要为了接收参数,我们详细了解一下。

  1. protocols
    ALPN协议,协商的应用层协议。最常用的就是HTTP,HTTP有很多版本,从最初的1.0到1.1到最新的2.0。还有SPDYQUIC等协议。都是为了解决不同的问题而产生的。OkHttp是怎么设置支持的应用层协议的,就是通过protocols变量,他是一个Protocol类型的集合。
    默认的赋值如下,也就是默认支持HTTP1.1和HTTP2.0版本,我们也可以使用Builder的protocols()进行定制。现在不支持SPDYQUIC

    static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(
    Protocol.HTTP_2, Protocol.HTTP_1_1);
    
  2. connectionSpecs
    连接的配置规格,包括是否使用的SSL、TSL和一些版本和密码的配置。默认支持两种配置,一个是HTTP明文规格和TSL规格。TSL规格配置是默认使用的密码套件是APPROVED_CIPHER_SUITES,安全协议版本是TSL1.3和1.2。并且支持TSL的扩展。这套TSL适用于大多数客户端平台。

  3. dispatcher
    请求的调度器,内部会使用一个线程池,决定异步请求什么时候开始,默认最大同时请求数64,每个主机地址最大同时请求5。同时同步请求也会存储在内部的runningSyncCalls。系统默认包含一个实现,我们也可以通过dispatcher(Dispatcher dispatcher),传入自己的配置,包括设置一个空闲的回调和请求数量的限制。

  4. proxyproxySelector
    使用的代理,proxy的优先级比proxySelector要高,如果没有设置proxy就回调用proxySelectorselect()选择一个代理服务器。代理服务器在OKHttp内部是Proxy类,内部存储了代理服务器的类型和地址。类型包括直接请求(无代理),HTTP和SOCKS

  5. interceptorsnetworkInterceptors
    自定义的拦截器,OKHttp的网络请求就是一个个拦截器实现的,拦截器程链式分布。外部也允许我们自己创建拦截器,参与请求的过程。interceptors 可以拦截整个请求过程的拦截,包括重试、缓存等过程,而networkInterceptors只可以拦截真正网络调用的过程。

  6. eventListenerFactoryeventListener
    事件的监听器,OKHttp在运行中,会发出一些事件,我们可以实现自己的工厂,返回自己的实现进行监听。内部提供了很多个回调,包括请求开始结束,dns开始结束等状态。

  7. cookieJar
    提供cookie的支持,包括获取和存储cookie。默认的实现不做任何处理。

  8. cacheinternalCache
    前端缓存,框架内部具体的实现是Cache,默认实现了Http缓存的策略,内部使用了LRU策略进行内存管理。这两个的值是互斥的,设置一个,会把另一个置为null。我们可以实现internalCache 这个接口,或者创建一个Cache传入Builder,系统默认是没有Cache的。如果需要缓存,我们可以cache(@Nullable Cache cache) 或者setInternalCache(@Nullable InternalCache internalCache)进行配置。

  9. socketFactorysslSocketFactory
    创建Socket和sslSocket的工厂,socketFactory默认是DefaultSocketFactory,生成的是java.net.Socket。这些都是java中提供的网络库。 sslSocketFactory 默认为null,设置sslSocketFactory主要为了配置Https的请求,如果我们设置的ConnectionSpec支持TSL,那么会有一个默认的实现。后面讲Https时会讲这部分。

  10. certificateChainCleaner
    和上面的 sslSocketFactory 有关,如果我们设置了 sslSocketFactory 会通过 Platform.get().buildCertificateChainCleaner(sslSocketFactory);进行获取。主要用来处理证书链。

  11. hostnameVerifier
    域名验证,默认是OkHostnameVerifier。使用verify函数效验服务器主机名的合法性,

  12. certificatePinner
    固定证书,我们可以配置一些固定的证书,比如抓包的证书等。获取的证书必须满足固定证书的要求。

  13. proxyAuthenticatorauthenticator
    认证组件,用于服务器返回407,重新进行身份验证。包括普通的认证和代理认证。

  14. connectionPool
    connection的复用池,可以提高效率,节省内存。后面会具体讲这个控件。

  15. dns
    配置DNS,获取IP地址。默认是Dns.SYSTEM,内部调用java的InetAddress.getAllByName获取IP地址。

  16. 超时时间
    一共有四种超时时间。

int callTimeout; //整个请求的时间
int connectTimeout; //连接的时间
int readTimeout; // 写入Socket时间
int writeTimeout; // 读取时间

17. retryOnConnectionFailure
重新连接配置,请求失败是否可以重试。默认是true 18. followRedirects
重定向配置,发生重定向时,是否允许重定向。 以上就是整个网络请求过程中,比较重要的构建参数。我们现在只需要知道有这个东西,知道大体的功能即可。后续会详细讲解每个控件。

2.Request

Request主要封装我们的网络请求。使用了Builder模式,内部有几个我们熟悉的字段。

final HttpUrl url;
final String method;
final Headers headers;
final @Nullable RequestBody body;
final Map<Class<?>, Object> tags;
  1. url表示请求的统一资源定位器,类型是HttpUrl,我们可以使用HttpUrl的Builder进行创建。
    https://www.google.com/search?q=polar%20bears
    HttpUrl url = new HttpUrl.Builder()
       .scheme("https")
       .host("www.google.com")
       .addPathSegment("search")
       .addQueryParameter("q", "polar bears")
       .build();
    
    这样我们可以很方便的创建一个Url。
  2. method表示请求的类型,默认是GET请求,我们可以设施POST、DELETE等方式
  3. headers表示请求的头,请求头就是key:value对。在Headers类中存储了一个字符串的数组
    String[] namesAndValues;
    
    这里面存储字符串对,分别存储key和value。我们同样可以使用Builder来创建这个Header。
  4. body表示请求体,下面的方法判断了是否需要请求体。例如我们设置method设置为post时,需要一个请求体,放置我们的参数。
    public static boolean requiresRequestBody(String method) {
     return method.equals("POST")
         || method.equals("PUT")
         || method.equals("PATCH")
         || method.equals("PROPPATCH") // WebDAV
         || method.equals("REPORT");   // CalDAV/CardDAV (defined in WebDAV Versioning)
    }
    
  5. tags表示本地的一些标记,可以对这次请求打一个标签。作为以后处理的标记。

创建完成上面两个对象,我们就可以使用这两个对象,创建一个call对象。这里使用OkHttpClient的newCall方法。Call是一个接口,具体的实现类时RealCall对象。他依赖上面两个对象。下面我们详细看下Call。

@Override public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

3.Call

Call依赖了我们上面创建的两个对象。OkHttpClient可以理解为OkHttp请求需要的框架参数信息,Request是请求的具体细节。使用框架参数信息,并借助请求细节,万事俱备,再利用RealCall就可以完成具体的请求了。可见具体的请求入口是在Call内,负责具体的逻辑,不管是同步还是异步请求。

3.1 构造方法

private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
  this.client = client;
  this.originalRequest = originalRequest;
  this.forWebSocket = forWebSocket;
  this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  this.timeout = new AsyncTimeout() {
    @Override protected void timedOut() {
      cancel();
    }
  };
  this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);
}

这里还创建了一个拦截器,重试和重定向的拦截器,主要处理重试和重定向。并处理超时相关的逻辑。这里使用了AsyncTimeout完成超时细节。并通过timeout方法设置超时的时间,后面详细分析具体细节。

3.2 同步请求

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  timeout.enter();
  eventListener.callStart(this);
  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } catch (IOException e) {
    e = timeoutExit(e);
    eventListener.callFailed(this, e);
    throw e;
  } finally {
    client.dispatcher().finished(this);
  }
}

同步请求调用RealCall的execute()方法,是在我们调用的线程执行请求操作。如果在主线程,那会阻塞主线程,所以一定在异步线程进行调用。我们详细看下每一个细节。

  • 不能重复请求:executed变量标记是否已经执行过,如果我们重复调用的话,会抛出一个异常。也就是说一个Call不能重复的执行。
  • 超时: 调用timeout.enter()进行计时,开始超时的判断。后面的timeoutExit(e)方法就停止了计时,防止内存泄漏。超时的逻辑在timer内部里。
  • 事件监听: 触发事件的监听,调用callStart表示请求开始事件
  • 数据请求: 先调用dispatcher的executed缓存这个请求,并在finally块中调用finish方法进行清除,这里dispatcher并没有负责请求工作,而是负责统计和统一控制。获取Response直接调用getResponseWithInterceptorChain方法就行了,就这么简单。
Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());
  interceptors.add(retryAndFollowUpInterceptor);
  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, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

  Response response = chain.proceed(originalRequest);
  if (retryAndFollowUpInterceptor.isCanceled()) {
    closeQuietly(response);
    throw new IOException("Canceled");
  }
  return response;
}

上面可以说是OKHttp请求数据的核心方法,逻辑比较清晰,直接创建了很多的拦截器,并集成为一个链。首先是我们自定义的interceptors拦截器,后面依次是重试/重定向、桥接、缓存、连接、自定义networkInterceptors拦截器、请求拦截器。很显然这是一种责任链模式,从这条链上走完,哎!数据就出来。。。
从上面的方法可以看出interceptors拦截器拦截了整个请求过程,而networkInterceptors拦截器只对最后一部请求拦截器做处理。验证我们对自定义拦截器的介绍。 这条连接器链是怎么运行的呢

3.3 拦截器链的运行

主要通过RealInterceptorChain这个对象,进行链上的传递和运行。通过proceed方法,触发运行链路。

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  if (index >= interceptors.size()) throw new AssertionError();
  。。。

  RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
      connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
      writeTimeout);
  Interceptor interceptor = interceptors.get(index);
  Response response = interceptor.intercept(next);
    。。。

  return response;
}

proceed方法中,直接调用了拦截器的intercept方法,每个拦截器的intercept方法实现了各自的功能。 并创建了下一个RealInterceptorChain,不同就是传递了 index + 1 这个index。并在interceptors.get(index)获取具体拦截器。这样一步一步,每次都 + 1。就完成了拦截器链的执行。也就是没执行完自己拦截器的前置代码,再执行下一个拦截器,下一个拦截器返回后,再执行自己的后置代码,这种架构是非常灵活的。各个网络请求中的功能都很成功的解耦了。这就是OkHttp最核心的运行方式,后面的分析都是基于他的,很重要的点。

3.4 异步请求

异步请求是调用了enqueue方法。

@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  eventListener.callStart(this);
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

和同步请求一样,也是不允许执行两次的。并调用eventListener.callStart(this)触发Start的事件。请求主要是通过dispatcher的enqueue的方法,这个和同步是不同的,异步交给了dispatcher进行处理。也就是交给里面的线程池进行处理。直接执行方就是AsyncCall,直接执行方就是AsyncCall是一个NamedRunnable继承类。NamedRunnable是一个Runnable,run方法直接调用抽象execute方法。AsyncCall的execute方法实现如下。

@Override protected void execute() {
  boolean signalledCallback = false;
  timeout.enter();
  try {
    Response response = getResponseWithInterceptorChain();
    signalledCallback = true;
    responseCallback.onResponse(RealCall.this, response);
  } catch (IOException e) {
    e = timeoutExit(e);
    if (signalledCallback) {
      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      eventListener.callFailed(RealCall.this, e);
      responseCallback.onFailure(RealCall.this, e);
    }
  } catch (Throwable t) {
    cancel();
    if (!signalledCallback) {
      IOException canceledException = new IOException("canceled due to " + t);
      responseCallback.onFailure(RealCall.this, canceledException);
    }
    throw t;
  } finally {
    client.dispatcher().finished(this);
  }
}

同样的逻辑也是调用getResponseWithInterceptorChain方法进行数据请求,和同步请求的逻辑是一致的。执行完成后,调用responseCallback,也就是我们调用enqueue方法传入的回调,数据请求成功和请求失败都会回调回来。可以看出异步和同步只是差别在dispatcher的线程池。

4. 总结:

框架外部使用OkHttp是非常简单的,总结就是下面的图表

stateDiagram-v2
[*] --> 创建OkHttpClient

创建OkHttpClient --> 创建Request
创建Request --> 通过OkHttpClient+Request创建Call
通过OkHttpClient+Request创建Call --> 同步execute请求
通过OkHttpClient+Request创建Call --> 异步enqueue请求
同步execute请求 --> 执行拦截责任链
异步enqueue请求 --> 放入dipatcher线程池
放入dipatcher线程池--> 执行拦截责任链
执行拦截责任链 --> [*]

5. 思考:

  1. OKHttp为什么使用责任链模式呢?

通过本节的分析,应该对OKHttp大体的请求流程有了把握。接下来的每一章节都会具体讲解每一个控件。包括最核心的拦截责任链的每一个拦截器。