第一节基础部分我们了解到,HTTP 请求信息、响应信息中有很多是重复的,通信过程中会很多种异常情况,根据响应码的不同,也需要做不同的处理。
那么多情况要处理,放到一两个类里想必会十分冗余,这个时候就是表演技术的时候了, OkHttp
采用了一种清晰、低耦合的分层责任链模式。
上一小节我们了解到, OkHttp
内置了 5 个拦截器,在每一个拦截器里,分别对请求信息和响应值做了处理,每一层只做当前相关的操作,这五个拦截器分别是:
他们的作用分别如下:
RetryAndFollowUpInterceptor
:取消、失败重试、重定向 BridgeInterceptor
:把用户请求转换为 HTTP 请求;把 HTTP 响应转换为用户友好的响应 CacheInterceptor
:读写缓存、根据策略决定是否使用 ConnectInterceptor
:实现和服务器建立连接 CallServerInterceptor
:实现读写数据 掌握了这五个拦截器,我们就熟悉了 OkHttp
的核心,来挨个了解一下吧!
RetryAndFollowUpInterceptor
是内置拦截器中的第一个,也是我们接触最早的一个。在前面的 AsyncCall.execute()
方法中,通过拦截器链拿到响应值后,首先调用了 RetryAndFollowUpInterceptor.isCanceled()
方法判断当前请求是否取消:
//AsyncCall.execute() @Override protected void execute() { boolean signalledCallback = false; try { Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { //判断是否取消请求 signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } //... }
拦截器最核心的就是拦截方法 intecept(Chain)
,我们直接看 RetryAndFollowUpInterceptor
的拦截方法吧:
//RetryAndFollowUpInterceptor.intercept() @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Call call = realChain.call(); EventListener eventListener = realChain.eventListener(); //首先创建了流引用管理的类 StreamAllocation StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; int followUpCount = 0; Response priorResponse = null; while (true) { //有一个循环 if (canceled) { //检查当前请求是否被取消,如果这时请求被取消了,则会通过StreamAllocation释放连接 streamAllocation.release(); throw new IOException("Canceled"); } Response response; boolean releaseConnection = true; try { response = realChain.proceed(request, streamAllocation, null, null); releaseConnection = false; //请求过程中,只要发生未处理的异常,releaseConnection 就会为true,一旦变为true,就会将StreamAllocation释放掉 } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. if (!recover(e.getLastConnectException(), streamAllocation, false, request)) { throw e.getLastConnectException(); } releaseConnection = false; continue; } catch (IOException e) { // An attempt to communicate with a server failed. The request may have been sent. boolean requestSendStarted = !(e instanceof ConnectionShutdownException); if (!recover(e, streamAllocation, requestSendStarted, request)) throw e; releaseConnection = false; continue; } finally { if (releaseConnection) { streamAllocation.streamFailed(null); streamAllocation.release(); } } //关联前一个响应 if (priorResponse != null) { response = response.newBuilder() .priorResponse(priorResponse.newBuilder() .body(null) .build()) .build(); } //根据 code 和 method 判断是否需要重定向请求 Request followUp = followUpRequest(response, streamAllocation.route()); if (followUp == null) { //不需要重定向时直接返回结果 if (!forWebSocket) { streamAllocation.release(); } return response; } closeQuietly(response.body()); if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } if (followUp.body() instanceof UnrepeatableRequestBody) { streamAllocation.release(); throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); } //通常发生请求重定向时,url 地址将会有所不同 if (!sameConnection(response, followUp.url())) { streamAllocation.release(); //释放原来的 streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; } else if (streamAllocation.codec() != null) { throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?"); } request = followUp; priorResponse = response; } }
可以看到, RetryAndFollowUpInterceptor
的拦截操作中做了这么几件事:
proceed()
方法 followUpRequest()
方法中判断是否需要重定向,是的话就再请求 当不需要重定向时, followUpRequest()
会返回空 。我们来看下 followUpRequest()
方法如何决定是否需要重定向:
private Request followUpRequest(Response userResponse, Route route) throws IOException { if (userResponse == null) throw new IllegalStateException(); int responseCode = userResponse.code(); final String method = userResponse.request().method(); switch (responseCode) { //根据响应码做相关操作 case HTTP_PROXY_AUTH: //407,代理服务器验证 Proxy selectedProxy = route != null ? route.proxy() : client.proxy(); if (selectedProxy.type() != Proxy.Type.HTTP) { throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); } return client.proxyAuthenticator().authenticate(route, userResponse); case HTTP_UNAUTHORIZED: //401,未验证 return client.authenticator().authenticate(route, userResponse); case HTTP_PERM_REDIRECT: case HTTP_TEMP_REDIRECT: // "If the 307 or 308 status code is received in response to a request other than GET // or HEAD, the user agent MUST NOT automatically redirect the request" if (!method.equals("GET") && !method.equals("HEAD")) { return null; } // fall-through case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: // Does the client allow redirects? if (!client.followRedirects()) return null; String location = userResponse.header("Location"); if (location == null) return null; HttpUrl url = userResponse.request().url().resolve(location); // Don't follow redirects to unsupported protocols. if (url == null) return null; // If configured, don't follow redirects between SSL and non-SSL. boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme()); if (!sameScheme && !client.followSslRedirects()) return null; // Most redirects don't include a request body. Request.Builder requestBuilder = userResponse.request().newBuilder(); if (HttpMethod.permitsRequestBody(method)) { final boolean maintainBody = HttpMethod.redirectsWithBody(method); if (HttpMethod.redirectsToGet(method)) { requestBuilder.method("GET", null); } else { RequestBody requestBody = maintainBody ? userResponse.request().body() : null; requestBuilder.method(method, requestBody); } if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding"); requestBuilder.removeHeader("Content-Length"); requestBuilder.removeHeader("Content-Type"); } } //... return requestBuilder.url(url).build(); case HTTP_CLIENT_TIMEOUT: //... if (userResponse.priorResponse() != null && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) { // We attempted to retry and got another timeout. Give up. return null; } if (retryAfter(userResponse, 0) > 0) { return null; } return userResponse.request(); case HTTP_UNAVAILABLE: if (userResponse.priorResponse() != null && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) { // We attempted to retry and got another timeout. Give up. return null; } if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) { // specifically received an instruction to retry without delay return userResponse.request(); } return null; default: return null; } }
在第一章我们介绍了一些比较冷门的响应码,这里就见到用处了。
followUpRequest()
根据响应码做了这些事:
OkHttpClient
时传入的 Authenticator
,做鉴权处理操作,返回处理后的结果 OkHttpClient
时设置允许重定向,就从当前响应头中取出 Location
即新地址,然后构造一个新的 Request 再请求一次 如果要处理 401 或者 407,我们就要在构造 OkHttpClient 时传入自定义的 Authenticator
,比如这样:
private OkHttpClient getOkHttpClient() { return new OkHttpClient.Builder() .authenticator(new Authenticator() { @Nullable @Override public Request authenticate(final Route route, final Response response) throws IOException { //这里根据响应做一些鉴权、新请求构造操作 return null; } }) .build(); }
在 followUpRequest()
方法结束之后,如果返回不为 null,说明要重新请求,就会把和这次请求地址不同的连接释放掉,创建新连接。
在这里我们频繁看到 StreamAllocation
,它主要用于管理客户端与服务器之间的连接,同时管理连接池,以及请求成功后的连接释放等操作,我们讲连接拦截器时介绍。
OK,至此我们了解了第一个拦截器 RetryAndFollowUpInterceptor
,小结一下它做的事:
StreamAllocation
内置的拦截器中第二个是 BridgeInterceptor
。Bridge,桥,什么桥?连接用户请求信息和 HTTP 请求的桥梁。
BridgeInterceptor
负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应。
来看下它的拦截方法:
// BridgeInterceptor.intercept() @Override public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); RequestBody body = userRequest.body(); if (body != null) { //根据请求体添加 header MediaType contentType = body.contentType(); if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); if (contentLength != -1) { requestBuilder.header("Content-Length", Long.toString(contentLength)); requestBuilder.removeHeader("Transfer-Encoding"); } else { requestBuilder.header("Transfer-Encoding", "chunked"); requestBuilder.removeHeader("Content-Length"); } } if (userRequest.header("Host") == null) { //添加 Host header requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } if (userRequest.header("Connection") == null) { //添加连接信息 header requestBuilder.header("Connection", "Keep-Alive"); } boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { //从本地加载 cookie 信息 requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } Response networkResponse = chain.proceed(requestBuilder.build()); HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); responseBuilder.headers(strippedHeaders); String contentType = networkResponse.header("Content-Type"); responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody))); } return responseBuilder.build(); }
可以看到就是在请求前添加一下 header,请求后做完解压缩处理等,然后移除一些 header。
我们记录一下都具体添加了哪些信息吧,请求前:
Content-Type
, Content-Length
等 Host
,就通过 url 来获取 Host 值添加到 Header 中 Accept-Encoding
,且没指定接收的数据范围,就添加默认接受格式为 gzip CookieJar
中根据 url 查询 Cookie 添加到 Header User-Agent
信息 发起请求后:
GzipSource
进行解压,同时移除 Content-Encoding
和 Content-Length
这个拦截器做的很简单,用一个图就可以描述它的功能:
第三个拦截器是缓存处理拦截器 CacheInterceptor
,它的重要性用一句话来描述: 最快的请求就是不请求,直接用缓存。
缓存用得好,响应快的不得了,但是如果一不小心用错了缓存,会导致在对的时间遇到错的数据,遗憾终生。
这一节我们来看看 OkHttp
的缓存处理是如何做的,不过在这之前先再补补一些基础知识。
在 HTTP 协议中,定义了一些缓存相关的 Header:
Cache-Control
Etag
, If-None_match
LastModified
, If-Modified-Since
Expired
首先看下 Cache-Control
,即缓存策略,它的值关系到客户端是否使用缓存,请求和响应分别有这些值:
不同于拦截器设置缓存,CacheControl 是针对 Request 的,所以它可以针对每个请求设置不同的缓存策略。
剩下的那些缓存相关 Header 使用规则如下图所示:
HTTP 定义的规范,首先会根据 CacheControl
来判断是否使用缓存,如果使用缓存,就去判断当前缓存是否新鲜,是否新鲜这样判断:
Etag
,就向服务器发送带 If-None-Match
的请求,服务器进行决策 Etag
就看有没有 Last-Modified
,有的话向服务器发送带 If-Modified-Since
的请求,由服务器进行决策 服务器验证缓存有效性后,如果缓存仍可以使用,就返回 304;如果 code 不是 304,客户端就需要从响应里拿数据,同时更新缓存。
服务器返回的响应头里可能有 Expired
,这个值表示当前响应将在什么时候过期,对于过期了的对象,只有在跟服务器验证了其有效性后,才能用来响应客户请求。例如: Expires:Sat, 23 May 2009 10:02:12 GMT
。
在基本了解 HTTP 协议中定义的缓存策略后,我们来看看 OkHttp
的缓存拦截器是如何实现的:
public final class CacheInterceptor implements Interceptor { final InternalCache cache; public CacheInterceptor(InternalCache cache) { this.cache = cache; } @Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; //先去缓存拿,拿到了用不用得看情况 //... return response; }
首先可以看到的是,缓存拦截器会先去 InternalCache
里找有没有缓存。
这个 InternalCache
唯一的实现是在 OkHttp.Cache
中,它就是一个包装类,还是调用的 OkHttp.Cache
的方法,我们直接看 Cache.get()
方法:
public final class Cache implements Closeable, Flushable { private static final int VERSION = 201105; private static final int ENTRY_METADATA = 0; private static final int ENTRY_BODY = 1; private static final int ENTRY_COUNT = 2; final InternalCache internalCache = new InternalCache() { @Override public Response get(Request request) throws IOException { return Cache.this.get(request); } //... }; final DiskLruCache cache; public Cache(File directory, long maxSize) { this(directory, maxSize, FileSystem.SYSTEM); } Cache(File directory, long maxSize, FileSystem fileSystem) { this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize); } public static String key(HttpUrl url) { return ByteString.encodeUtf8(url.toString()).md5().hex(); } @Nullable Response get(Request request) { String key = key(request.url()); DiskLruCache.Snapshot snapshot; Entry entry; snapshot = cache.get(key); if (snapshot == null) { return null; } //... entry = new Entry(snapshot.getSource(ENTRY_METADATA)); Response response = entry.response(snapshot); //... return response; }
从上面的代码可以看到的是,OkHttp 中的缓存使用的是基于文件系统的磁盘缓存,缓存的 key 是 url 的 md 值。
由于调用方可以 针对某个请求是否要使用缓存进行配置 (通过给 Request
配置 CacheControl
属性),比如这样:
Request request = new Request.Builder() .cacheControl(new CacheControl.Builder().noCache().build()) .url("http://publicobject.com/helloworld.txt") .build();
因此我们从 cache 拿到缓存响应后,还需要做这几件事:
request.cacheControl()
的值) 在 CacheInterceptor
中,是通过 CacheStrategy
来判断缓存能否使用的。
在 CacheInterceptor.intercept()
中我们可以看到,去 cache 里拿到缓存响应后,接着又调用了 CacheStrategy
:
//CacheInterceptor.intercept() @Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; //去缓存拿缓存响应 //判断缓存能否使用 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); / Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; //... }
这个 CacheStrategy
如何判断缓存能否使用的呢?我们通过一张图来解释:
如图所示, CacheStrategy
的工厂方法构造需要两个参数:请求信息和拿到的缓存响应。
//两个参数:请求信息和拿到的缓存响应 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
在其内部,它会根据用户对当前请求设置的 CacheControl
和缓存响应的时间、ETag 、 LastModified 或者 ServedDate 等 Header 进行判断,最后输出两个值 :
//加工后拿到两个值,根据这两个值得情况决定是请求网络还是直接返回缓存 Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse;
根据这两个值是否为空的四种情况,有不同的处理,分别如下:
networkRequest
和 cacheResponse
都是空,表示调用端要求只用缓存,但缓存不可用了,只好返回 504 响应 // If we're forbidden from using the network and the cache is insufficient, fail. if (networkRequest == null && cacheResponse == null) { return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); }
504 Gateway Timeout
作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。
networkRequest
为空,但 cacheResponse
不为空,就直接返回缓存响应 // If we don't need the network, we're done. if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); }
networkRequest
不为空,表示不用缓存或者缓存有效性需要验证,这时就需要请求网络了 cacheResponse
不为空,且请求的响应码是 304,表示缓存还可以用,就直接返回 cacheResponse
** 下面的代码就是这种情况的逻辑:
```
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}