你必需学会的OkHttp(第二篇)

作者 : 开心源码 本文共9106个字,预计阅读时间需要23分钟 发布时间: 2022-05-13 共265人阅读

引子

OkHttp 知名第三方网络框架SDK,使用简单,性能优秀,但是内核并不简单,此系列文章,专挑硬核知识点详细讲解。何为硬核,就是要想深入研究,你绝对绕不过去的知识点。

TIPS:公告:阻拦器种细节太多,要逐个讲解不太现实,所以我挑了其中最实用的少量要点加以总结。

详细讲解 OKHttp的核心内容,阻拦器。不过阻拦器众多,有系统自带的,也有我们可以自己去自己设置的。

大家可以先看首篇-你必需学会的OKHttp
顺手留下GitHub链接,需要获取相关面试或者者面试宝典核心笔记PDF等内容的可以自己去找
xiangjiana/Android-MS

这是网络请求执行的核心方法的起点,这里涉及了众多阻拦器。

正文大纲

系统自带阻拦器

1 重试与重定向阻拦器 RetryAndFollowUpInterceptor
2 桥接阻拦器
3 缓存阻拦器 CacheInterceptor
4 连接阻拦器 ConnectInterceptor
5 服务调用阻拦器 CallServerInterceptor

正文

在详解阻拦器之前,有必要先将 RealCallgetResponseWithInterceptorChain() 方法最后两行开展说明:

  Interceptor.Chain chain = newRealInterceptorChain( interceptors, null, null, null, 0, originalRequest);  return chain.proceed(originalRequest);

这里最终返回 一个 Response,进入 chain.proceed方法,最终索引到 RealInterceptorChain的 proceed方法:


之后,我们追踪这个 interceptor.intercept(next); ,发现是一个接口,找到实现类,有多个,进入其中的 RetryAndFollowUpInterceptor,发现:

它这里又执行了 chain.proceed,于是又回到了 RealInterceptorChain.proceed()方法,但是此时,刚才链条中的阻拦器已经不再是原来的阻拦器了,而是变成了第二个,由于每一次都 index+1了(这里比较绕,相似递归,需要反复仔细体会),依次类推,直到所有阻拦器的intercept方法都执行完毕,直到链条中没有阻拦器。就返回最后的 Response

这一段是 okhttp责任链模式的核心,应该好了解

系统自带阻拦器

1. 重试与重定向阻拦器 RetryAndFollowUpInterceptor

先说结论吧:

顾名思义,retry 重试,FollowUp 重定向 。这个阻拦器处在所有阻拦器的第一个,它是用来判定要不要对当前请求进行重试和重定向的,
那么我们应该关心的是: 什么时候重试, 什么时候重定向。并且,它会判断客户有没有取消请求,由于RealCall中有一个cancel方法,可以支持客户 取消请求(不过这里有两种情况,在请求发出之 前取消,和 在之 后取消。假如是在请求之 前取消,那就直接不执行之后的过程,假如是在请求发出去之 后取消,那么用户端就会丢弃这一次的 response

重试
RetryAndFollowUpInterceptor的核心方法 interceptor() :

  @Override 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(), 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, requestSendStarted, request)) throw e;        releaseConnection = false;        continue;      }      ...省略      if (followUp == null) {        if (!forWebSocket) {          streamAllocation.release();        }        return response;      }      ...省略    }  }

上面的代码中,我只保留了关键部分。其中有两个continue,一个return.
当请求到达了这个阻拦器,它会进入一个 while(true)循环,

当发生了 RouteException 异常(这是因为请求尚未发出去,路由异常,连接未成功),就会去判断 recover方法的返回值,根据返回值决定要不要 continue.
当发生 IOException(请求已经发出去,但是和服务器通信失败了)之后,同样去判断 recover方法的返回值,根据返回值决定要不要 continue.
假如这两个 continue都没有执行,就有可能走到最后的 returnresponse结束本次请求. 那么 是不是要 重试,其判断逻辑就在 recover()方法内部:

  private boolean recover(IOException e, StreamAllocation streamAllocation,                            boolean requestSendStarted, Request userRequest) {        streamAllocation.streamFailed(e);        //todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试        //The application layer has forbidden retries.        if (!client.retryOnConnectionFailure()) return false;        //todo 2、因为requestSendStarted只在http2的io异常中为false,http1则是 true,        //在http1的情况下,需要判定 body有没有实现UnrepeatableRequestBody接口,而body默认是没有实现,所以后续instanceOf不成立,不会走return false.        //We can't send the request body again.        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)            return false;        //todo 3、判断是不是属于重试的异常        //This exception is fatal.        if (!isRecoverable(e, requestSendStarted)) return false;        //todo 4、有没有可以用来连接的路由路线        //No more routes to attempt.        if (!streamAllocation.hasMoreRoutes()) return false;        // For failure recovery, use the same route selector with a new connection.        return true;    }

简单解读一下这个方法:

  • 假如okhttpClient已经set了不允许重试,那么这里就返回false,不再重试。
  • 假如requestSendStarted 只在http2.0的IO异常中是true,不过HTTP2.0还没普及,先不论他,这里默认通过。
  • 判断能否是重试的异常,也就是说,是不是之前重试之后发生了异常。这里解读一下,之前重试发生过异常,抛出了Exception,这个 isRecoverable方法会根据这个异常去判定,能否还有必要去重试。
  • 协议异常,假如发生了协议异常,那么没必要重试了,你的请求或者者服务器本身可能就存在问题,再重试也是白瞎。
  • 超时异常,只是超时而已,直接判定重试(这里requestSendStartedhttp2才会为true,所以这里默认就是false)
  • SSL异常,HTTPS证书出现问题,没必要重试。
  • SSL握手未受权异常,也不必重试
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {    // 出现协议异常,不能重试    if (e instanceof ProtocolException) {      return false;    }    // requestSendStarted认为它一直为false(不论http2),异常属于socket超时异常,直接判定可以重试    if (e instanceof InterruptedIOException) {      return e instanceof SocketTimeoutException && !requestSendStarted;        }    // SSL握手异常中,证书出现问题,不能重试    if (e instanceof SSLHandshakeException) {      if (e.getCause() instanceof CertificateException) {        return false;      }    }    // SSL握手未受权异常 不能重试    if (e instanceof SSLPeerUnverifiedException) {      return false;    }    return true;}

有没有可以用来连接的路由路线,也就是说,假如当DNS解析域名的时候,返回了多个IP,那么这里可能一个一个去尝试重试,直到没有更多ip可用。

重定向
仍然是 RetryAndFollowUpInterceptor的核心方法 interceptor() 方法,这次我截取后半段:

  public Response intercept(Chain chain) throws IOException {     while (true) {            ...省略前面的重试判定            //todo 解决3和4xx的少量状态码,如301 302重定向            Request followUp = followUpRequest(response, streamAllocation.route());            if (followUp == null) {                if (!forWebSocket) {                    streamAllocation.release();                }                return response;            }            closeQuietly(response.body());            //todo 限制最大 followup 次数为20次            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());            }            //todo 判断是不是可以复用同一份连接            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?");            }     } }

上面源码中, followUpRequest() 方法中规定了哪些响应码可以重定向:

  private Request followUpRequest(Response userResponse) throws IOException {    if (userResponse == null) throw new IllegalStateException();    Connection connection = streamAllocation.connection();    Route route = connection != null        ? connection.route()        : null;    int responseCode = userResponse.code();    final String method = userResponse.request().method();    switch (responseCode) {      // 407 用户端使用了HTTP代理商服务器,在请求头中增加 “Proxy-Authorization”,让代理商服务器受权      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”      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 配置新的请求 url        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;        Request.Builder requestBuilder = userResponse.request().newBuilder();        /**         *  重定向请求中 只需不是 PROPFIND 请求,无论是POST还是其余的方法都要改为GET请求方式,         *  即只有 PROPFIND 请求才能有请求体         */        //请求不是get与head        if (HttpMethod.permitsRequestBody(method)) {          final boolean maintainBody = HttpMethod.redirectsWithBody(method);           // 除了 PROPFIND 请求之外都改成GET请求          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()) {            return null;        }        // UnrepeatableRequestBody实际并没发现有其余地方用到        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {            return null;        }        // 假如是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是由于408,那我们这次不再重请求了        if (userResponse.priorResponse() != null                       &&userResponse.priorResponse().code()==HTTP_CLIENT_TIMEOUT) {            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) {             return null;         }         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {             return userResponse.request();         }         return null;      default:        return null;    }}

解读一下这个方法,它根据拿到的response的内容,判断他的响应码,决定要不要返回一个新的request,假如返回了新的request,那么外围( 看RetryAndFollowUpInterceptorintercept方法)的 while(true)无限循环就会 使用新的request再次请求,完成重定向。细节上请查看上面代码的注释,来自一位高手,写的很详细。大概做个结论:

  • 响应码 3XX 一般都会返回一个 新的Request,而另外的 return null就是不允许重定向。
  • followup最大发生20次

不过还是那句话,我们不是专门做网络架构或者者优化,理解到 这一个阻拦器的基本作用,重要节点就可,真要抠细节,谁也记不了那么清楚。

2. 桥接阻拦器 BridgeInterceptor

这个可能是这5个当中最简单的一个阻拦器了,它从上一层RetryAndFollowUpInterceptor拿到 request之后,只做了一件事: 补全请求头我们使用OkHttp发送网络请求,一般只会 addHeader中写上我们业务相关的少量参数,而 真正的请求头远远没有那么简单。服务器不只是要识别 业务参数,还要识别 请求类型,请求体的解析方式等,具体列举如下:


它在补全了请求头之后,交给下一个阻拦器解决。在它得到响应之后,还会干两件事:
1、保存cookie,下一次同样域名的请求就会带上cookie到请求头中,但是这个要求我们自己在okHttpClientCookieJar中实现具体过程。

假如使用gzip返回的数据,则使用GzipSource包装便于解析。

3. 缓存阻拦器 CacheInterceptor

本文只详情他的作用,由于内部逻辑太过复杂,必需单独成文讲解。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 你必需学会的OkHttp(第二篇)

发表回复