第一节基础部分我们了解到,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());
}
}