浅析 OkHttp 拦截器之 RetryAndFollowUpInterceptor
来源:互联网 发布:苹果x卖得怎么样知乎 编辑:程序博客网 时间:2024/06/10 11:22
1 概述
除了用户自定义的拦截器(比如打打日志),该拦截器处于链条的头部
1.1 核心功能
连接失败重试(Retry)
在发生 RouteException 或者 IOException 后,会捕获建联或者读取的一些异常,根据一定的策略判断是否是可恢复的,如果可恢复会重新创建 StreamAllocation 开始新的一轮请求
继续发起请求(Follow up)
主要有这几种类型
- 3xx 重定向
- 401,407 未授权,调用 Authenticator 进行授权后继续发起新的请求
- 408 客户端请求超时,如果 Request 的请求体没有被
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;
1.2 活动图
2 失败重试
后面的环节会进行路由选择、建立连接、发起请求并得到响应等等。而如果建立连接失败,比如未知主机,三次握手超时,读取超时等等都会导致请求失败。有一些失败是可以恢复的,而有些失败是不可以恢复的
失败重试核心代码:
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(); } } ...}
2.1 异常捕获
在后面的拦截器的请求失败,会抛出两种异常
RouteException
这个异常发生在 Request 请求还没有发出去前,就是打开 Socket 连接失败。这个异常是 OkHttp 自定义的异常,是一个包裹类,包裹住了建联失败中发生的各种 Exception
主要发生 ConnectInterceptor 建立连接环节
比如连接超时抛出的 SocketTimeoutException,包裹在 RouteException 中
IOException
这个异常发生在 Request 请求发出并且读取 Response 响应的过程中,TCP 已经连接,或者 TLS 已经成功握手后,连接资源准备完毕
主要发生在 CallServerInterceptor 中,通过建立好的通道,发送请求并且读取响应的环节
比如读取超时抛出的 SocketTimeoutException
2.2 重试判断
在捕获到这两种异常后,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 的资源了
- StreamAllocation 的 route 为空
- RouteSelector 没有可选择的路由了
- 没有下一个 IP
- 没有下一个代理
- 没有下一个延迟使用的 Route(之前有失败过的路由,会在这个列表中延迟使用)
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 置为空有两种情况
- Http/2 的 StreamResetException,如果是第一次出现 ErrorCode.REFUSED_STREAM 不会置空,其它都会置空
- 非 SPDY 协议且 connection 的 successCount 为 0,即该 Route 对应的 Connection 一次都没成功过
除了这两种情况下,route 在 streamFailed
都不会置空,所以下一次连接前,还会继续使用该 route。目前还未确定,是否真有满足重试的条件且 route 始终不为 null,不停地重试的现象
2.3 死循环问题
怀疑某些路径会引起不断重试。最终内存泄漏
由于出现异常,进行重试是在一个死循环中,而且处理后不是 continue 就是 throw 出异常,所以必定不受继续请求的最大次数 MAX_FOLLOW_UPS
限制。于是,如果出现某个路径,导致 recover
的判断每次都可以通过,那么就死循环了,该连接就无法释放,积累起来最后触发 OOM
目前仅仅是猜测,尚未发现有这样的路径
3 继续请求
如果连接成功,也获得了 Response 响应,但是不一定是 200 OK,还有一些其他情况。比如 3xx 重定向,401 未授权等,这些响应码是允许我们再次发起请求的。比如重定向,获取目标地址后,再次发起请求。又比如 401 未授权,可以在 Request 中新增头部 “Authorization” 授权信息再次发起请求等。OkHttp 帮我们实现了这些功能
Request followUp = followUpRequest(response);
在方法 followUpRequest 对 response 的响应码进行判断,来确定是否支持再次发起请求,具体有这几种类型支持再次请求
3.1 未授权
会使用 Authenticator 来添加授权信息后重新发起请求
407 代理未授权
在请求中添加 “Proxy-Authorization”
401 未授权
在请求中添加 “Authorization”
3.2 重定向
并不是所有 3xx 都支持,有做区分
307 和 308
如果不是 GET 或者 HEAD 请求不进行重定向
300,301,302,303
均允许重定向
具体的流程如下:
- 从响应中获取 “Location”
- 跳转到不支持协议不能重定向
- HTTPS 和 HTTP 之间的重定向,需要根据配置 followSslRedirects 来判断
- 去掉请求体,并响应地去掉 “Transfer-Encoding”,“Content-Length”,“Content-Type” 等头部信息
- 如果不是同一个连接(比如 host 不同),去掉授权头部 “Authorization”
3.3 超时
只处理一种情况
408 客户端超时
部分服务器会因为客户端请求时间太长而返回 408,此时如果请求体没有实现标记接口 UnrepeatableRequestBody, OkHttp 会再把之前的请求没有修改重新发出
Q&A
如果一个请求 route 先成功了,后面都失败了,会一直不断地重试吗?
代码上看,route 使用的 Connection 成功后,而且该 Connection 的 successCount 会递增,所以在 streamFailed 的时候,不会置当前 StreamAllocation 的 route 为空,所以可以直接返回。这样子,会不会存在导致之前连接成功过一次,但后面一直失败,从而死循环了呢?该想法尚未得到验证
- 浅析 OkHttp 拦截器之 RetryAndFollowUpInterceptor
- OKhttp源码解析---拦截器之RetryAndFollowUpInterceptor
- OkHttp之拦截器
- 浅析 OkHttp 的拦截器机制
- okhttp之自定义Interceptor:缓存拦截器
- OKhttp源码解析---拦截器之BridgeInterceptor
- OKhttp源码解析---拦截器之CacheInterceptor
- OKhttp源码解析---拦截器之ConnectInterceptor
- OKhttp源码解析---拦截器之CallServerInterceptor
- Okhttp-wiki 之 Interceptors 拦截器
- Okhttp源码解析之Interceptor(拦截器)
- OkHttp:拦截器之网络请求Log
- Struts2之拦截器浅析
- OkHttp深入理解(2)RetryAndFollowUpInterceptor
- okhttp源码分析(二)-RetryAndFollowUpInterceptor过滤器
- Okhttp 拦截器
- okhttp 拦截器调用。
- OkHttp-Interceptors拦截器
- 如何设置普通用户的ulimit值
- 图片生成失败, Can't create output stream!
- SpringMVC整合Ajax传JSON对象
- oc 视图转换
- Android NDK 编译PjSip 2.6 之 PjSip编译 (二)
- 浅析 OkHttp 拦截器之 RetryAndFollowUpInterceptor
- HashMap--如何线程安全的使用
- .net 实现form表单数据及图片上传
- 升级SSH版本
- 新零售是生鲜电商的最好出路
- hdu5695
- 关于SQL查询效率,100w数据,查询只要1秒
- java通过SVNkit操作SVN
- MySql函数之根据时间取数据