OkHttp 3.10.0版本源码之重试重定向拦截器原理分析

分发器的逻辑执行完成就会进入拦截器了,OkHttp使用了拦截器模式来处理一个请求从发起到响应的过程。

代码还是从我们上一篇提到的getResponseWithInterceptorChain开始

java 复制代码
    @Override
    public Response execute() throws IOException {
        ...
        try {
            ...
            // 发起请求
            Response result = getResponseWithInterceptorChain();
            ...
            return result;
        } catch (IOException e) {
            
        } finally {
            
        }
    }
csharp 复制代码
    Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //自定义拦截器加入到集合
        interceptors.addAll(client.interceptors()); 
        interceptors.add(retryAndFollowUpInterceptor);
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        interceptors.add(new CacheInterceptor(client.internalCache()));
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
            //自定义拦截器加入到集合,(和上边client.interceptors()的区别仅在于添加的顺序)
            //但是不同的顺序也会产生不同的效果,具体可参考下
            //https://segmentfault.com/a/1190000013164260
            interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));

        Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
                originalRequest, this, eventListener, client.connectTimeoutMillis(),
                client.readTimeoutMillis(), client.writeTimeoutMillis());

        return chain.proceed(originalRequest);
    }
addInterceptor 与 addNetworkInterceptor 的区别?

1.添加顺序不同

应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,第一个被添加的,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据

2.可能执行的次数不同

网络拦截器(addNetworkInterceptor)可能执行多次(如果发生了错误重试或者网络重定向),也可能一次都没有执行(如果在CacheInterceptor中命中了缓存);而addInterceptor只会执行一次,因为它是在请求发起之前最先执行的(在RetryAndFollowUpInterceptor之前)

3.应用场景不同

应用拦截器因为只调用一次,可用于统计客户端发起的次数,而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

可以看到OkHttp内部默认存在五大拦截器,而今天这篇要讲的就是retryAndFollowUpInterceptor,这个拦截器在RealCall被new出来时已经创建了,从他的名字就可以看出来,他负责的是失败重试和重定向的逻辑处理。

失败重试

从这个拦截器的intercept方法中可以看出,虽然这个拦截器是第一个被执行的,但是其实他真正的重试和重定向操作是在请求被响应之后才做的处理.

java 复制代码
    @Override
    public Response intercept(Chain chain) throws IOException {
        ...
        while (true) {
            ...
            try {
                //请求出现了异常,那么releaseConnection依旧为true。
                response = realChain.proceed(request, streamAllocation, null, null);
                releaseConnection = false;
            } catch (RouteException e) {
                //路由异常,连接未成功,请求还没发出去
                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                    throw e.getLastConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                //请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
                // HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true
                boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
                if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
                releaseConnection = false;
                continue;
            } finally {
                ...
            }
            ...
        }
    }

可以看到被处理的exception只有RouteException和IOException,RouteException是路由异常,连接未成功,请求还没发出去,所以recover方法中第三个参数直接传的false,表示请求还没有开始;而IOException是请求发出去了,但是和服务器通信失败了,所以所以recover方法中第三个参数值取决于

ini 复制代码
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);

HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true。

从上面的代码可以看出,realChain.proceed是后续的责任链执行的逻辑,如果这些执行发生了异常,在RetryAndFollowUpInterceptor会被捕获,然后通过recover方法判断当前异常是否满足重试的条件(并不是所有失败都会被重试),如果满足,则continue,再进行一次,这个操作是在while循环中进行的,也就是只要满足重试的条件,可以进行无数次的重试,但事实上,由于重试的条件比较苛刻,一般也不会被多次重试。那么这个重试的条件究竟有哪些呢?

重试条件

进入recover方法

kotlin 复制代码
    private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);

        //调用方在OkhttpClient初始化时设置了不允许重试(默认允许)
        if (!client.retryOnConnectionFailure()) return false;

        //RouteException不用判断这个条件,
        //当是IOException时,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件,body是UnrepeatableRequestBody则不必重试
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
            return false;

        //对异常类型进行判断
        if (!isRecoverable(e, requestSendStarted)) return false;

        //不存在更多的路由也没办法重试
        if (!streamAllocation.hasMoreRoutes()) return false;
        //以上条件都允许了,才能重试
        return true;
    }

进入isRecoverable方法

typescript 复制代码
    private boolean isRecoverable(IOException e, boolean requestSendStarted) {
        // 协议异常,那么重试几次都是一样的
        if (e instanceof ProtocolException) {
            return false;
        }

        // 请求超时导致的中断,可以重试
        if (e instanceof InterruptedIOException) {
            return e instanceof SocketTimeoutException && !requestSendStarted;
        }
        //证书不正确  可能证书格式损坏 有问题
        if (e instanceof SSLHandshakeException) {
            // If the problem was a CertificateException from the X509TrustManager,
            // do not retry.
            if (e.getCause() instanceof CertificateException) {
                return false;
            }
        }
        //证书校验失败 不匹配
        if (e instanceof SSLPeerUnverifiedException) {
            // e.g. a certificate pinning error.
            return false;
        }
        return true;
    }

总结一下:

1、协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来 定义数据,再重试也没用)

2、超时异常,可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。

3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书

所以说要满足重试的条件还是比较苛刻的。

重定向

OkHttp支持重定向请求,见followUpRequest方法,主要是对响应头的一些判断

scss 复制代码
    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) {
            //407 客户端使用了HTTP代理服务器,在请求头中添加 "Proxy-Authorization",让代理服务器授权 @a
            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);
             //401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 "Authorization" @b
            case HTTP_UNAUTHORIZED:
                return client.authenticator().authenticate(route, userResponse);
            // 308 永久重定向
            // 307 临时重定向
            case HTTP_PERM_REDIRECT:
            case HTTP_TEMP_REDIRECT:
                // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
                if (!method.equals("GET") && !method.equals("HEAD")) {
                    return null;
                }
            // 300 301 302 303
            case HTTP_MULT_CHOICE:
            case HTTP_MOVED_PERM:
            case HTTP_MOVED_TEMP:
            case HTTP_SEE_OTHER:
                // 如果设置了不允许重定向,那就返回null
                if (!client.followRedirects()) return null;
                // 从响应头取出location
                String location = userResponse.header("Location");
                if (location == null) return null;
                // 根据location 配置新的请求
                HttpUrl url = userResponse.request().url().resolve(location);

                // 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
                if (url == null) return null;

                // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
                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();

                //重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方       
                //法都要改为GET请求方式, * 即只有 PROPFIND 请求才能有请求体
                //请求不是get与head HttpMethod.permitsRequestBody ===> return !(method.equals("GET") || method.equals("HEAD"));
                
                //HttpMethod.permitsRequestBody ===> return method.equals("PROPFIND"); 
                //HttpMethod.permitsRequestBody ===> return !method.equals("PROPFIND");
                if (HttpMethod.permitsRequestBody(method)) {
                    final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                    // 除了 PROPFIND 请求之外都改成GET请求
                    //HttpMethod.redirectsToGet ===> return !method.equals("PROPFIND");
                    if (HttpMethod.redirectsToGet(method)) {
                        requestBuilder.method("GET", null);
                    } else {
                        RequestBody requestBody = maintainBody ? userResponse.request().body() :
                                null;
                        requestBuilder.method(method, requestBody);
                    }
                    // 不是 PROPFIND 的请求(不包含请求体的请求),把请求头中关于请求体的数据删掉
                    if (!maintainBody) {
                        requestBuilder.removeHeader("Transfer-Encoding");
                        requestBuilder.removeHeader("Content-Length");
                        requestBuilder.removeHeader("Content-Type");
                    }
                }

                // 在跨主机重定向时,删除身份验证请求头
                if (!sameConnection(userResponse, url)) {
                    requestBuilder.removeHeader("Authorization");
                }

                return requestBuilder.url(url).build();
            // 408 客户端请求超时
            case HTTP_CLIENT_TIMEOUT:
                // 408 算是连接失败了,所以判断用户是不是允许重试
                if (!client.retryOnConnectionFailure()) {
                    // The application layer has directed us not to retry the request.
                    return null;
                }

                if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
                    return null;
                }
                // 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求 了
                if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
                    // We attempted to retry and got another timeout. Give up.
                    return null;
                }
                // 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
                if (retryAfter(userResponse, 0) > 0) {
                    return null;
                }

                return userResponse.request();
            // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
            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;
        }
    }

@a:在OkHttpClient Builder构建的时候可以设置,对应HTTP_PROXY_AUTH响应头

kotlin 复制代码
    public Builder proxyAuthenticator(Authenticator proxyAuthenticator) {
            if (proxyAuthenticator == null)
                throw new NullPointerException("proxyAuthenticator == null");
            this.proxyAuthenticator = proxyAuthenticator;
            return this;
        }

@b:在OkHttpClient Builder构建的时候可以设置,对应HTTP_UNAUTHORIZED响应头

kotlin 复制代码
    public Builder authenticator(Authenticator authenticator) {
            if (authenticator == null) throw new NullPointerException("authenticator == null");
            this.authenticator = authenticator;
            return this;
        }

整个是否需要重定向的判断内容很多,关键在于理解他们的意思。如果此方法返回空,那就表 示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是, 我们的 followup 在拦截器中定义的最大次数为20次。

总结

RetryAndFollowUpInterceptor拦截器是整个责任链中的第一个,这意味着它会是首次接触到 Request 与最后接收到 Response 的角色,在这个 拦截器中主要功能就是判断是否需要重试与重定向。

重试的前提是出现了 RouteException 或者 IOException 。一但在后续的拦截器执行过程中出现这两个异常,就会 通过 recover 方法进行判断是否进行连接重试。

重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用 followUpRequest 根据 Response 的响 应码(当然,如果直接请求失败, Response 都不存在就会抛出异常)。 followup 最大发生20次。

相关推荐
雾里看山6 分钟前
【MySQL】 表的约束(上)
android·mysql·adb
q567315232 小时前
无法在Django 1.6中导入自定义应用
android·开发语言·数据库·django·sqlite
a3158238062 小时前
Android设置个性化按钮按键的快捷启动应用
android·开发语言·framework·源码·android13
Henry_He2 小时前
SystemUI通知在阿拉伯语下布局方向RTL下appName显示异常
android
XJSFDX_Ali3 小时前
安卓开发,底部导航栏
android·java·开发语言
云罗张晓_za8986685 小时前
抖音“碰一碰”发视频:短视频社交的新玩法
android·c语言·网络·线性代数·矩阵·php
默萧笙故7 小时前
常见的前端框架和库有哪些
前端框架·c#·.net
货拉拉技术8 小时前
记一次无障碍测试引发app崩溃问题的排查与解决
android·前端·程序员
GrimRaider8 小时前
【逆向工程】破解unity的安卓apk包
android·unity·游戏引擎·软件逆向
yzpyzp8 小时前
Jetpack之ViewBinding和DataBinding的区别
android