一、前言
之前分析了okhttp3的基本工作流程,其中重点说明了分发器、高并发线程池设计、任务的分发和转换原理,后面还有一个比较重要的5大拦截器还没有具体深入研究,其实okhttp中核心工作基本上都是在拦截器中执行的,接下来这篇文章就带大家分析五大拦截器究竟是怎么协同工作的,每个拦截器究竟干了什么事情。
二、基于责任链模式的拦截器的工作流程
其实拦截器的工作流程就像是我们去办理业务时的流程,下面举一个简单的例子
新进社畜小王出差回来,带了一堆发票要公司报销,然后就出现了下面的事情
小王进入财务室。。。
小王:钱掌柜,我之前出差,住宿,吃饭,交通都产生了一笔费用,这些是发票,麻烦报销下
小王把一堆发票双手捧住递给了财务钱掌柜。。。
财务:你这不行啊,你得把这个申请单填好,而且你来我这报销必须得要人事部门同意啊,不然怎么知道你是不是真的去出差了
财务打出了一份报销申请单,递给了小王。。。
小王拿着申请单和一堆发票进入了人事部。。。
小王:人事小姐姐你好,这是我的报销申请单,需要你在上面签字,我才能找财务报销
人事:你这不行啊,没你的部门老大签字,鬼知道你去出差干嘛了
小王很烦躁地从人事部走出来,拿着申请单到了技术部经理的办公室。。。
小王:秃经理,我这里有份报销申请单,需要你签字后,人事才能签字,人事签字后,财务才能签字,然后报销才能到账,就问你签不签!
技术部经理:不要激动!你这几天出差帮公司解决了难题,我这不会卡你的
秃经理在报销申请单上签上了自己的名字。。。
人事在报销申请单上签上了自己的名字。。。
财务在报销申请单上签上了自己的名字。。。
小王银行卡到账1000元
上面的例子很简单地展示了一整个的报销流程,大概意思就是每个环节都必须要满足上一个环节,否则就不执行,下面是一个简单的流程图
image.png
这个流程图也类比了okhttp的五大拦截器之间的流转。
五大拦截器的流转原理是基于责任链模式的,一个请求过来首先会依次流经每个拦截器,但是每个拦截器都是要求下一个拦截器返回结果后再去往下走,接下来直接看源码
我们直接从RealCall的getResponseWithInterceptorChain方法开始看
final class RealCall implements Call {
...
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();//代码1
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));//代码2
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());//代码3
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);//代码4
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);
}
}
}
...
}
代码流向:->
,这段代码执行的过程就是将拦截器加入到列表中的过程,执行完后,会得到这样一个拦截器列表:
| 拦截器列表:interceptors | 释义 |
|---|---|
| client.interceptors | 用户自定义的拦截器 |
| RetryAndFollowUpInterceptor | 重试和重定向拦截器 |
| BridgeInterceptor | 桥接拦截器 |
| CacheInterceptor | 缓存拦截器 |
| ConnectInterceptor | 连接拦截器 |
| client.networkInterceptors | 当网络请求回来后,用户自定义的拦截器 |
| CallServerInterceptor | 和服务器通信的拦截器 |
上面的顺序十分重要,关系到拦截器的流转流程,在这里假设用户没有自定义拦截器列表即client.interceptors.size == 0
中创建了一个chain,翻译过来就是链条,这个chain的左右就是将5大拦截器串联起来
开始就是拦截器流转的开端,接下来我们具体看看,okhttp是如何将从一个拦截器流转到另一个拦截器的
public interface Interceptor {
...
interface Chain {
...
Response proceed(Request request) throws IOException;//代码5
...
}
...
}
public final class RealInterceptorChain implements Interceptor.Chain {
...
@Override public Response proceed(Request request) throws IOException {
return proceed(request, transmitter, exchange);//代码6
}
public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
throws IOException {
...
RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
index + 1, request, call, connectTimeout, readTimeout, writeTimeout);//代码7
Interceptor interceptor = interceptors.get(index);//代码8
Response response = interceptor.intercept(next);//代码9
...
return response;
}
}
Chain这个接口的唯一实现只有RealInterceptorChain所以这里的代码流向应该是:
先看下代码7这里生成了一个Chain的实例,其中第一个参数interceptors不变,还是原来的拦截器列表,变化的是index,这个成员变量是用来指定当前链条指向哪个拦截器,所以可以推测实际上就是生成指向下一个拦截器的链条。走到
可以发现取出的是当前的(第index个)拦截器,而
则是将下一个拦截器的链条做为参数传递到了拦截器的intercept()方法中执行。继续跟代码,看看interceptor.intercept(next)这里做了什么。在这之前记住当前index == client.interceptors.size而我们之前假设用户没有自定义拦截器所以index == 0,所以
取出的拦截器是RetryAndFollowUpInterceptor。
public interface Interceptor {
Response intercept(Chain chain) throws IOException;//代码10
...
}
public final class RetryAndFollowUpInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
...
RealInterceptorChain realChain = (RealInterceptorChain) chain;//代码11
...
while (true) {
...
Response response;
...
try {
response = realChain.proceed(request, transmitter, null);//代码12
success = true;
} catch (RouteException e) {
...
}
...
}
}
代码流向:
可以很清楚地知道中获得的realChain实际上就是
中传递过来的下一个链表。我们再看看
好熟悉,这不就又回到了这个代码流向了吗?,但是不同的是之前就已经在代码9中将index+1,所以当前index == 1,所以代码8取到的拦截器应该是BridgeInterceptor,简单看一下BridgeInterceptor
public final class BridgeInterceptor implements Interceptor {
...
@Override public Response intercept(Chain chain) throws IOException {
...
Response networkResponse = chain.proceed(requestBuilder.build());//代码13
...
}
...
}
果然BridgeInterceptor也会通过链表chain来执行下一个拦截器,这样就形成了一个驱动型的链表,上一个链表会驱动下一个链表去执行拦截器中的intercept方法,而intercept()方法又会驱动下一个链表...由此拦截器列表就能执行起来了。
值得注意的是Interceptor接口中有个返回值Response,为啥要有这个?
还记得开篇讲过社畜小王报销流程吗,他先找财务财务要求人事签名才继续下面的工作,不然不给钱,这个签名就类比Response,在okhttp拦截器中的含义就是,我RetryAndFollowUpInterceptor拦截器得先有一个Response才能执行下面的操作,而这个Response得由下一个拦截器(BridgeInterceptor)给我,下一个拦截器也要一个Response,而这个拦截器得由下下一个(CacheInterceptor)给...所以一旦执行了只有当没有下一个拦截器时,拦截器才会终止往下传,一个个拦截器收到了Response后才会继续下面的工作。
到这里流程基本上就通了,接下来会围绕上面的列表,详细讲讲5大拦截器里每个拦截器做了哪些事情。
三、拦截器详解
3.1 RetryAndFollowUpInterceptor重试和重定向拦截器
RetryAndFollowUpInterceptor是什么
从名字上就能看出,这个拦截器的作用是用来重试和重定向的,上期分析出拦截器中主要用来执行的方法是intercept()方法,那就废话不多说直接看RetryAndFollowUpInterceptor在intercept()中做了什么。
看源码:
public final class RetryAndFollowUpInterceptor implements Interceptor {
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_FOLLOW_UPS = 20;
@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 = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
while (true) {
......
boolean releaseConnection = true;
//重试
try {
//如果抛出异常就说明可能需要重试,如果没有必要重试,拦截器会直接抛出异常
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} 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.getFirstConnectException();
}
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 {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp;
try {
//重定向
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
......
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
......
}
}
}
上述就是主要代码,其实也就只做了两件事情
1.重试
2.重定向
重试
首先明确一点,为什么重试?首先肯定是程序员要求你这么做,如果说不需要重试那肯定就不用去重试了。其次肯定是因为超时等原因,我们的网络连接失败而抛出异常。那每个异常都要重试吗?并不是,如果说你压根连host的格式都不对,这样的重试肯定毫无意义。基于以上两点RetryAndFollowUpInterceptor拦截器为我们做了一些处理。
response = realChain.proceed(request, streamAllocation, null, null);执行后,如果抛出异常就说明可能需要重试,如果没有必要重试,拦截器会直接抛出异常,如果有必要重试,就会继续往下走。
整个重试和重定向的逻辑都是在while循环里的,所以满足条件的重试肯定不会一直循环下去,代码中有个MAX_FOLLOW_UPS,这个的意思是允许最大重试的次数,这个次数是20,如果重试次数大于了20次,也会报错ProtocolException,定为20的原因如下:
意思是,我们这个20次重试不是瞎搞的,这是参考了Chrome、Firefox、Safari这几个大佬开发的浏览器的重试次数综合考虑后得来的。
那怎么样的重试才是值得去重试的呢?我们每次catch异常时,在Catch后都会执行一个recover()方法,这个方法的返回值直接决定了是不是要直接报错(跳出循环,不再重试),接下来看看这个方法究竟干了什么:
/**
* 简单来说这个方法的意义就是判断当前是否值得去重试,如果值得重试就返回true,如果不值得重试就返回false
*/
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
//意思是如果程序员不让我重试,那我就不重试
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
//意思是同一个请求(request)不能被调用两次,如果请求是不重复的并且已经发送过了(requestSendStarted==true),那就不允许重试了
// 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;
}
看下isRecoverable方法:
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
//意思是如果是协议错误,那不能重试
// If there was a protocol problem, don't recover.
if (e instanceof ProtocolException) {
return false;
}
//如果不是超时异常那也不能重试
// If there was an interruption don't recover, but if there was a timeout connecting to a route
// we should try the next route (if there is one).
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
//ssl握手异常不能重试
// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
// again with a different route.
if (e instanceof SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//ssl握手未授权异常,不能重试
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
// An example of one we might want to retry with a different route is a problem connecting to a
// proxy and would manifest as a standard IOException. Unless it is one we know we should not
// retry, we return true and try a new route.
return true;
}
简而言之,就是那种很严重的错误,你重试100遍都没结果的,索性就不要重试。
重试的判断基本上就是这些逻辑,接下来我们回过去看看调用followUpRequest重定向的代码。
重定向
try {
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
followUpRequest方法:
/**
* 代码走到这里后其实服务端已经返回的数据,这段代码的目的就是看看是否还
* 需要重定向,如果不需要重定向就返回null。
*/
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://看有没有代理,并返回
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://需要身份验证的
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT://意思是如果返回的状态码为永久或临时重定向的,而重定向的方法不是GET或者HEAD的就不用管
// "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;
//是取出重定向的地址,如果是null则不管,如果不为null,
//就直接通过HttpUrl url = userResponse.request().url().resolve(location);拼接成一个httpUrl
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;
//判断当前重定向是否是http和https之间的重定向
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
//表示如果确实是http和https之间的重定向,那就得看用户是否让这种重定向,如果不让那就返回null不用管
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
//redirectsWithBody的方法是判断这个方法既不是GET也不是HEAD
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
//判断改方法是不是不等于PROPFIND
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");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
return null;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
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;
}
}
重定向的判断大致就是这么多
再回过去看看,这个拦截器其实主要的工作都集中在了判断上面,判断是否能重试,判断是否能重定向,而每个判断又分为是否是用户(程序员)不让重试或者重定向,还是因为外在条件导致报错。
3.2 BridgeInterceptor桥连接拦截器
BridgeInterceptor做的事情就比较简单了,从字面意思上讲Bridge是桥的意思,这个桥连接着两边,一边是用户端的请求一边是服务端的响应,所以这个拦截器的作用大概就像下面这个样子
image.png
public final class BridgeInterceptor implements Interceptor {
private final CookieJar cookieJar;
public BridgeInterceptor(CookieJar cookieJar) {
this.cookieJar = cookieJar;
}
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
//------------------请求数据加工开始------------------
RequestBody body = userRequest.body();
if (body != null) {
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) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
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()) {
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 | 请求体类型,如: application/x-www-form-urlencoded |
| Content-Length 或 Transfer-Encoding | 请求体解析方式 |
| Host | 请求的主机站点 |
| Connection: Keep-Alive | 保持长连接 |
| Accept-Encoding: gzip | 接受响应支持gzip压缩 |
| Cookie | cookie身份辨别 |
| User-Agent | 请求的用户信息,如:操作系统、浏览器等 |
其中Accept-Encoding: gzip这个请求头的意思是服务端可以返回被压缩后的数据,如果数据量过大,只要服务端配合好,使用这个请求头会大幅提升传输的效率
User-Agent这个请求头是系统信息,这个信息是可以随意修改的,比如我们的okhttp就做了这样的事情
public final class Version {
public static String userAgent() {
return "okhttp/3.14.9";//如果你不去设置,就默认给你okhttp
}
private Version() {
}
}
Connection: Keep-Alive这个是保持长连接的意思,为啥要保持长连接?首先一个完整的tcp可靠连接一定会经历下面的步骤
三次握手->传输数据->四次挥手
这就产生了一个问题,我一个页面多图,文字,多视频,gif等,每下载一张图就来一轮握手挥手,岂不是很浪费时间和资源,所以这个头的意思是,我跟你握手一次建立连接后,你图片文字啥的该传传,我先等会儿再挥手。
数据返回时做的工作有两个:
1.保存cookie(默认不实现)
2.如果是gzip返回并且客户端这里是支持的,那就得包装一层GzipSource方便后面解压缩用












网友评论