OkHttp3的网络编程

OkHttp3的网络编程

简介

如今在大部分的Android应用中都大抵使用OkHttp3作为网络请求的框架,无论是纯使用okhttp进行请求还是在Retrofit2中都能看到okhttp的影子。先简单介绍一下OkHtto的特点:

  1. HTTP/2 支持所有的请求能够在同一个host下复用同一个Socket;
  2. 连接池降低了请求的延长(非 HTTP/2);
  3. 支持 GZIP 传输压缩;
  4. 响应缓存避免重复的请求;

除此以外,当OkHttp在访问一个IP地址失败时会自动尝试下一个IP(前提是你有配置多个ip地址),还处理了代理服务器问题和SSL握手失败问题。

简单使用:

1
2
3
4
5
6
7
8
9
10
11
OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.post(body)
.build();

Response response = client.newCall(request).execute();
return response.body().string();
}

详情介绍请见官方教程:OkHttp

上面的一切都可以从源代码中可以看到如何实现,现在我们就进入代码阅读。

原理

Okhttp不同于 Volley 那样是基于HttpURLConnectionHttpClient基础上实现的一套机制而是自己通过TCP实现的完整一套网络机制,以便更高效的完成Http请求。

OKHttpClient

一切从 OkHttpClient 开始,OkHttpClient 通过 Builder 传入或初始化整个框架所需要的参数和配置,比如请求分发器Dispatcher,拦截器Interceptor等,这些参数和配置都可以借由 OkHttpClient 这个角色被子模块快速的调用,比如 dispatcher,call,cache等等。同时 OkHttpClient 也负责将 Request 转化成实际请求Call方便Dispatcher进行分发。

Dispatcher

如在最上面的代码所示,在发起请求时需要一个Request对象来封装每次的请求参数,而这个Request对象会在 OkHttpClient 中转换为真正的请求对象Call并将其放入Dispatcher中进行分发,以同步或者异步的方式。

同步

Dispatcher 会在同步执行任务队列中 runningSyncCalls 记录当前被执行过得任务Call,同时在当前线程中去执行Call的getResponseWithInterceptorChain()方法,直接获取当前的返回数据Response,完成后会将call从 runningSyncCalls 队列中移除。

getResponseWithInterceptorChain 方法中包含着之后要说的拦截器和链式调用机制。

异步

在异步中 Dispatcher 又维护了两个队列,runningAsyncCallsreadyAsyncCalls,前者为正在请求的队列,后者是当请求队列超过一定大小后的准备队列。
Dispatcher内部实现了懒加载无边界限制的线程池方式,同时该线程池采用了 SynchronousQueue这种阻塞队列。

1
2
3
4
5
6
7
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;
}

这种方式能够快速的传递元素,适用于高频繁请求的场景。
异步执行是通过 Call.enqueue(Callback responseCallback)来执行,在 Dispatcher 中添加一个封装了Callback的Call的匿名内部类Runnable来执行当前的Call。这里的call在调用会转换成其内部类AsyncCall,AsyncCall的execute方法最后还是会回调到Call的 getResponseWithInterceptorChain方法来完成请求,同时将返回数据或者状态通过Callback来完成。

Interceptor拦截链

通过 Dispatcher 已经知道了OkHttp是怎样分发请求的,那么下一步也就是最重要的是OkHttp是怎么完成请求的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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());

return chain.proceed(originalRequest);
}

OkHttp3对HTTP请求是通过Interceptor链来处理的。拦截器接口中有 intercept(Chain chain) 方法,同时返回Response,主要功能是针对Request和Response的切面处理。
拦截器流程
RealInterceptorChain的初始化中传入了整个拦截器列表,而在RealInterceptorChain中维护了拦截器运行的迭代数,保证每次在拦截器中的intercept(Chain chain)中调用 chain.proceed()都能顺序的执行下一个拦截器。

用户自定义 Interceptor

用户自定义的Interceptor分为应用Interceptor和网络NetworkInterceptor,其实两者并没有什么本质的区别,只不过所处的位置不同。

应用Interceptor位于拦截链的首位,不会担心响应和重定向之间的中间响应,通常只调用一次,即使HTTP响应式通过缓存提供的。
而网络NetworkInterceptor位于 ConnectInterceptorCallServerInterceptor之间,可以在数据传递到网络时即时观测,能够在重定向和重试中操作中间响应。

RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 首先创建了StreamAllocation对象,借由 RealInterceptorChain将其传递给后面执行的Interceptor。之后便是重要的两部分,连接失败重试(Retry)和继续发起请求(Follow up)两部分。

在后面的拦截器的请求失败,会抛出两种异常

  • RouteException
    这个异常发生在 Request 请求还没有发出去前,就是打开 Socket 连接失败。这个异常是 OkHttp 自定义的异常,是一个包裹类,包裹住了建联失败中发生的各种 Exception 主要发生 ConnectInterceptor建立连接环节,比如连接超时抛出的 SocketTimeoutException,包裹在 RouteException

  • IOException
    这个异常发生在 Request 请求发出并且读取 Response 响应的过程中,TCP 已经连接,或者 TLS 已经成功握手后,连接资源准备完毕。主要发生在 CallServerInterceptor 中,通过建立好的通道,发送请求并且读取响应的环节。比如读取超时抛出的 SocketTimeoutException

那么针对以上的异常,OkHttp 使用 recover 方法判断是否可以重试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Report and attempt to recover from a failure to communicate with a server. Returns true if
* {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
* be recovered if the body is buffered or if the failure occurred before the request has been
* sent.
*/

private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);

// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;

// We can't send the request body again.
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;

// No more routes to attempt.
if (!streamAllocation.hasMoreRoutes()) return false;

// For failure recovery, use the same route selector with a new connection.
return true;
}

简单的描述一下四个条件:

  • 在 OkHttpClient是否配置了retryOnConnectionFailure(true)
  • 不是建立连接阶段发生的异常,同时请求的内容不是可重复发送
  • 使用 isRecoverable 过滤不可恢复异常,如 ProtocolException,InterruptedIOException,CertificateExceptionSSLPeerUnverifiedException
  • 已经没有其他的路由可以使用。RouteSelector 封装了选择可用路由进行连接的策略。

如果连接成功,即获得了 Response 响应,但不一定是 200 OK,还有一些其他情况。比如 3xx 重定向,401 未授权等,这些响应码是允许我们再次发起请求的。比如重定向,获取目标地址后,再次发起请求。又比如 401 未授权,可以在 Request 中新增头部 “Authorization” 授权信息再次发起请求等。这时候就需要 followUpRequest对response的响应码进行判断,来确定是否支持再次发起请求。

  • 未授权(407的代理为授权,401的未授权)—— 在请求中添加 “Proxy-Authorization” 和 “Authorization”
  • 重定向 (307和308 —— 要求为get和head请求方式)(300,301,302,303) —— 拿到重定向的URL重建Request
  • 超时 (408 客户端超时) —— 部分服务器会因为客户端请求时间太长而返回 408,此时如果请求体没有实现标记接口 UnrepeatableRequestBody, OkHttp 会再把之前的请求没有修改重新发出

BridgeInterceptor

BridgeInterceptor 用来添加 http 的header,主要修改目标为 request。主要内容:

  • 在header中添加一些常用的字段,如 Content-Type,Content-Length或Transfer-Encoding,Host,Connection,Accept_Encoding,Cookie,User-Agent等
  • 通过 CookieJar 读取 request 上的cookie
  • gzip压缩,同时对response响应数据进行gzip解压。

CacheInterceptor

CacheInterceptor 用于处理http缓存,若缓存中有所需请求的响应则后续Interceptor不再执行。
1、如果在初始化OkhttpClient的时候配置缓存,则从缓存中取caceResponse
2、将当前请求request和caceResponse 构建一个CacheStrategy对象
3、CacheStrategy 这个策略对象将根据相关规则来决定caceResponse和Request是否有效,如果无效则分别将caceResponse和request设置为null
4、经过 CacheStrategy 的处理(步骤3),如果request和caceResponse都置空,直接返回一个状态码为504,且body为Util.EMPTY_RESPONSE的空Respone对象
5、经过CacheStrategy的处理(步骤3),resquest 为null而cacheResponse不为null,则直接返回cacheResponse对象
6、执行下一个拦截器发起网路请求,
7、如果服务器资源没有过期(状态码304)且存在缓存,则返回缓存
8、将网络返回的最新的资源(networkResponse)缓存到本地,然后返回networkResponse.

ConnectInterceptor

ConnectInterceptor 的主要职责是建立与服务器之间的连接,但这个事情它主要是委托给 StreamAllocation 来完成的。如我们前面看到的,StreamAllocation对象是在 RetryAndFollowUpInterceptor中分配的。
ConnectInterceptor 通过 StreamAllocation 创建了 HttpStream对象和RealConnection对象,随后便调用了realChain.proceed(),向连接中写入HTTP请求,并从服务器读回响应。

CallServerInterceptor

CallServerInterceptor 首先将http请求头部发给服务器,如果http请求有body的话,会再将body发送给服务器,继而通过httpStream.finishRequest()结束http请求的发送。随后便是从连接中读取服务器返回的http响应,并构造Response。如果请求的header或服务器响应的header中,Connection值为close,CallServerInterceptor还会关闭连接。

最后便是返回Response。

代理路由与网络请求

在理顺了OkHttp的请求框架之后,剩下的就是最基础也是最重要的部分——okhttp是如何处理代理和路由的以及怎么建立TCP连接。这一块可能需要从计算机网络部分开始讲起,所以暂且放着等日后复习完计算机网络基础再来补充(其实就是觉得细节太多自己把捏不住)

可以先参考一下几篇文章:

OkHttp3中的代理与路由
OKHttp全解析系列(七) – OKHttp中的Route和RouteSelector