本文目录
除了用户自定义的拦截器(比如打打日志),该拦截器处于链条的头部
连接失败重试(Retry)
在发生 RouteException 或者 IOException 后,会捕获建联或者读取的一些异常,根据一定的策略判断是否是可恢复的,如果可恢复会重新创建 StreamAllocation 开始新的一轮请求
继续发起请求(Follow up)
主要有这几种类型
UnrepeatableRequestBody
标记,会继续发起新的请求 其中 Follow up 的次数受到MAX_FOLLOW_UP
约束,在 OkHttp 中为 20 次,这样可以防止重定向死循环
/**
* 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;
后面的环节会进行路由选择、建立连接、发起请求并得到响应等等。而如果建立连接失败,比如未知主机,三次握手超时,读取超时等等都会导致请求失败。有一些失败是可以恢复的,而有些失败是不可以恢复的
失败重试核心代码:
public Response intercept(Chain chain) throws IOException {
...
while (true) {
...
try {
response = ((RealInterceptorChain) chain).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(), true, request)) throw e.getLastConnectException();
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
if (!recover(e, false, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
...
}
在后面的拦截器的请求失败,会抛出两种异常
RouteException
这个异常发生在 Request 请求还没有发出去前,就是打开 Socket 连接失败。这个异常是 OkHttp 自定义的异常,是一个包裹类,包裹住了建联失败中发生的各种 Exception
主要发生 ConnectInterceptor 建立连接环节
比如连接超时抛出的 SocketTimeoutException,包裹在 RouteException 中
IOException
这个异常发生在 Request 请求发出并且读取 Response 响应的过程中,TCP 已经连接,或者 TLS 已经成功握手后,连接资源准备完毕
主要发生在 CallServerInterceptor 中,通过建立好的通道,发送请求并且读取响应的环节
比如读取超时抛出的 SocketTimeoutException
在捕获到这两种异常后,OkHttp 会使用 recover 方法来判断是否是不可以重试的。然后有两种处理方式:
不可重试的
会把继续把异常抛出,调用 StreamAllocation 的 streamFailed
和 release
方法释放资源,结束请求。OkHttp 有个黑名单机制,用来记录发起失败的 Route,从而在连接发起前将之前失败的 Route 延迟到最后再使用,streamFailed 方法可以将这个出问题的 route 记录下来,放到黑名单(RouteDatabase)。所以下一次发起新请求的时候,上次失败的 Route 会延迟到最后再使用,提高了响应成功率
可以重试的
则继续使用 StreamAllocation 开始新的 proceed
。是不是可以无限重试下去?并不是,每一次重试,都会调用 RouteSelector 的 next 方法获取新的 Route,当没有可用的 Route 后就不会再重试了
什么情况下的失败是不可以恢复的呢?
/**
* 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.
*/
private boolean recover(IOException e, boolean routeException, 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 (!routeException && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// This exception is fatal.
if (!isRecoverable(e, routeException)) 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;
}
简单分析 recover
的代码,首先会调用 StreamAllocation 的 streamFailed
方法释放资源。然后通过以下策略来判断这些类型是否可以重试:
1. 没有配置允许连接失败的重试
需要进行配置
OkHttpClient okHttpClient = new OkHttpClient.Builder()
...
.retryOnConnectionFailure(true)
...
.build();
2. 不是被 RouteException 包裹的异常,并且请求的内容被 UnrepeatableRequestBody 标记
也就是不是建立连接阶段发生的异常,比如发起请求和获取响应的时候发生的 IOException,同时请求的内容不可重复发起,就不能重试
3. 使用 isRecoverable 方法过滤掉不可恢复异常
通过的话会继续进入第 4 步。不可重试的异常有:
ProtocolException
协议异常,主要发生在 RealConnection 中创建 HTTPS 通过 HTTP 代理进行连接重试超过 21 次。不可以重试
private void buildTunneledConnection(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
...
int attemptedConnections = 0;
int maxAttempts = 21;
while (true) {
if (++attemptedConnections > maxAttempts) {
throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
}
...
}
...
}
InterruptedIOException
如果是建立连接时 SocketTimeoutException,即创建 TCP 连接超时,会走到第4步
如果是连接已经建立,在读取响应的超时的 SocketTimeoutException,不可恢复
CertificateException 引起的 SSLHandshakeException
证书错误导致的异常,比如证书制作错误
SSLPeerUnverifiedException
访问网站的证书不在你可以信任的证书列表中
4. 已经没有其他的路由可以使用
前面三步条件都通过的,还需要最后一步检验,就是获取可用的 Route。
public boolean hasMoreRoutes() {
return route != null || routeSelector.hasNext();
}
所以满足下面两个条件就可以结束请求,释放 StreamAllocation 的资源了
RouteSelector 封装了选择可用路由进行连接的策略。重试一个重要作用,就是这个请求存在多个代理,多个 IP 情况下,OkHttp 帮我们在连接失败后换了个代理和 IP,而不是同一个代理和 IP 反复重试。所以,理所当然的,如果没有其他 IP 了,那么就会停止。比如 DNS 对域名解析后会返回多个 IP。比如有三个 IP,IP1,IP2 和 IP3,第一个连接超时了,会换成第二个;第二个又超时了,换成第三个;第三个还是不给力,那么请求就结束了
但是 OkHttp 在执行以上策略前,也就是 RouteSelector 内部的策略前,还有一个判断,就是该 StreamAllocation 的当前 route 是否为空,如果不为空话会继续使用该 route 而没有走入到 RouteSelector 的策略中。
这样子,是否会有这样一个场景,如果每次失败过来,前面3个条件都通过了,而且又满足 route 不为空,然后死循环,然后失败的连接无法释放,应用内存泄漏,最终 OOM?所以 StreamAllocation 的 route 为空,是一个非常重要退出重试条件,如果 route 一直不会空,且前面的几个条件又都满足,真的会发生严重的内存泄漏。那什么时候 route 为空?在 recover
方法的第一行,调用 streamAllocation.streamFailed
方法
public void streamFailed(IOException e) {
boolean noNewStreams = false;
synchronized (connectionPool) {
if (e instanceof StreamResetException) {
StreamResetException streamResetException = (StreamResetException) e;
if (streamResetException.errorCode == ErrorCode.REFUSED_STREAM) {
refusedStreamCount++;
}
// On HTTP/2 stream errors, retry REFUSED_STREAM errors once on the same connection. All
// other errors must be retried on a new connection.
if (streamResetException.errorCode != ErrorCode.REFUSED_STREAM || refusedStreamCount > 1) {
noNewStreams = true;
route = null;
}
} else if (connection != null && !connection.isMultiplexed()) {
noNewStreams = true;
// If this route hasn't completed a call, avoid it for new connections.
if (connection.successCount == 0) {
if (route != null && e != null) {
routeSelector.connectFailed(route, e);
}
route = null;
}
}
}
deallocate(noNewStreams, false, true);
}
所以 route 置为空有两种情况
除了这两种情况下,route 在 streamFailed
都不会置空,所以下一次连接前,还会继续使用该 route。目前还未确定,是否真有满足重试的条件且 route 始终不为 null,不停地重试的现象
怀疑某些路径会引起不断重试。最终内存泄漏
由于出现异常,进行重试是在一个死循环中,而且处理后不是 continue 就是 throw 出异常,所以必定不受继续请求的最大次数 MAX_FOLLOW_UPS
限制。于是,如果出现某个路径,导致 recover
的判断每次都可以通过,那么就死循环了,该连接就无法释放,积累起来最后触发 OOM
目前仅仅是猜测,尚未发现有这样的路径
如果连接成功,也获得了 Response 响应,但是不一定是 200 OK,还有一些其他情况。比如 3xx 重定向,401 未授权等,这些响应码是允许我们再次发起请求的。比如重定向,获取目标地址后,再次发起请求。又比如 401 未授权,可以在 Request 中新增头部 “Authorization” 授权信息再次发起请求等。OkHttp 帮我们实现了这些功能
Request followUp = followUpRequest(response);
在方法 followUpRequest 对 response 的响应码进行判断,来确定是否支持再次发起请求,具体有这几种类型支持再次请求
会使用 Authenticator 来添加授权信息后重新发起请求
407 代理未授权
在请求中添加 “Proxy-Authorization”
401 未授权
在请求中添加 “Authorization”
并不是所有 3xx 都支持,有做区分
307 和 308
如果不是 GET 或者 HEAD 请求不进行重定向
300,301,302,303
均允许重定向
具体的流程如下:
只处理一种情况
408 客户端超时
部分服务器会因为客户端请求时间太长而返回 408,此时如果请求体没有实现标记接口 UnrepeatableRequestBody, OkHttp 会再把之前的请求没有修改重新发出