`

学会这篇OkHttp,花了我一个通宵,也是值了!

阅读更多

引子

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

TIPS:声明:拦截器种细节太多,要一一讲解不太现实,所以我挑了其中最实用的一些要点加以总结。

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

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

 

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

正文大纲

系统自带拦截器

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

正文

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

  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

本文只介绍他的作用,因为内部逻辑太过复杂,必须单独成文讲解。

分享到:
评论

相关推荐

    okhttp3 jar包

    当网络出现问题的时候OkHttp依然坚守自己的职责,它会自动恢复一般的连接问题,如果你的服务有多个IP地址,当第一个IP请求失败时,OkHttp会交替尝试你配置的其他IP,OkHttp使用现代TLS技术(SNI, ALPN)初始化新的连接

    okhttp最新版okhttp-3.9.0.jar下载

    描述中提到的"okio-1.6.0.jar"是OkHttp依赖的一个重要库,Okio是一个用于处理I/O操作的库,特别是在处理大量数据流时,如网络传输和磁盘读写。Okio提供了比Java标准库更高效、更灵活的缓冲机制,能够简化文件读写、...

    OkHttp的一个Demo

    自己写的一个OKHttp的学习的一个Demo,里面有常规的get,post请求;post上传参数,post上传Json;post上传文件,get下载文件;上传下载文件的进度提示;Http请求的缓存的一点知识;Https单向双向认证的Demo。

    okhttp-2.0.0.jar+okhttp-apache-2.0.0.jar+okhttp-urlconnection-2.0.0.jar

    这对于那些已经在项目中使用Apache HttpClient的开发者来说,是一个平滑过渡到OKHttp的好选择。适配器使得两种API之间的切换变得简单,同时保持了原有的代码风格。 3. **URLConnection适配器 (okhttp-urlconnection...

    okhttp多文件上传

    1. 创建OkHttpClient实例:首先,我们需要创建一个OkHttpClient对象,它是OkHttp的核心组件,负责管理网络连接和请求。 ```java OkHttpClient client = new OkHttpClient(); ``` 2. 构建RequestBody:RequestBody...

    okhttp中连接池实现

    1. **连接对象的添加**:当一个请求被发送时,OkHttp会检查连接池中是否存在可用于该请求的连接。如果存在,就直接使用;否则,会创建一个新的TCP连接并将其添加到连接池中。这个过程涉及到对目标主机、端口和协议的...

    学会Retrofit+OkHttp+RxAndroid三剑客的使用,让自己紧跟Android潮流的步伐

    其次,OkHttp是另一个由Square公司开发的网络库,主要负责网络连接和数据传输。OkHttp提供了一个高效的HTTP客户端,其特点包括缓存、连接池、透明压缩和重试策略。OkHttp的使用可以显著减少网络请求的延迟,提高应用...

    Android-基于retrofit2和okhttp3的一个上传和下载库

    本项目结合这两个库,构建了一个专门针对上传和下载功能的库。下面将详细介绍这两个库以及如何利用它们实现上传和下载。 1. **Retrofit2** - **简介**:Retrofit是由Square公司开发的一款强大的Android网络请求库...

    okhttp3 和 okhttp2 jar

    Okio 是一个底层的 I/O 库,为 OkHttp 提供了高效的流处理能力。 OkHttp3 是 OkHttp 的最新版本,相较于 OkHttp2,它引入了许多改进和新特性: 1. **HTTP/2 支持**:OkHttp3 增加了对 HTTP/2 协议的原生支持,该...

    okhttp3jar包及源码

    OkHttp 3.4.1是该库的一个稳定版本,它在3.x系列中提供了许多改进和优化,包括更好的性能、更低的内存消耗以及更简洁的API。而Okio 1.11.0则是OkHttp依赖的数据处理库,它提供了一种高效的方式来处理I/O操作,如读写...

    OKHTTP依赖包jar包下载 OKHTTP和OKIO的JAR包下载

    OKHTTP是由Square公司开发的一个开源项目,它的设计目标是提高网络请求的性能和效率。相比于Android自带的HttpURLConnection,OKHTTP在速度、连接管理和错误处理方面具有显著优势。OKHTTP的特点包括: 1. **连接池*...

    okhttp-4.8.1.jar和 okhttp-4.9.1.jar

    OkHttp 4.8.1 版本是 OkHttp 的一个稳定版本,它包含了先前版本的所有功能,并可能对已知问题进行了修复和性能优化。在这一版本中,开发者可能会关注以下方面: 1. **连接管理**:OkHttp 使用连接池来复用 TCP 连接...

    OKHttp.jar包

    在Android应用开发中,网络通信是一个不可或缺的部分,而OKHttp作为一款高效的网络请求库,被广泛地应用于各种项目之中。本篇文章将深入探讨OKHttp.jar包,以及它如何与Eclipse集成,以助力Android开发者进行更高效...

    Okhttp3+MVP

    本项目结合这两者,旨在提供一个简洁且实用的实战案例,帮助开发者更好地理解和运用它们。 首先,我们来详细了解OkHttp3。OkHttp是Square公司推出的一款高效的HTTP客户端库,它的核心在于减少了网络请求的延迟,...

    okhttp

    7. **Okio**:OkHttp 内部依赖于 Okio,一个强大的 I/O 库,提供了一种更高效的流式处理方式,对于大数据操作有显著优势。 ### Okio 深入理解 Okio 是一个现代的 I/O 库,为处理字节流和字符流提供了简洁、高性能...

    okHttp封装库 Eclipse版本

    在提供的 `OkhttpDemo` 文件中,很可能是包含了一个简单的示例程序,演示如何使用 OkHttp 发送 GET 和 POST 请求,以及处理响应。`okhttputils` 文件可能是一个工具类库,封装了一些常用的网络请求操作,使得使用...

    OKHttp多线程断点下载

    【OKHttp多线程断点下载】是一种在Android或Java应用中实现高效文件下载的方法,它结合了OKHttp网络库的优秀性能与多线程技术,以提高下载速度,并允许在下载过程中中断和恢复,避免因网络问题或其他因素导致的下载...

    okhttp3所有Jar包_12.zip

    【标题】"okhttp3所有Jar包_12.zip" 提供的是OkHttp3的完整库集合,这个压缩包包含了OkHttp3框架及其相关依赖,便于开发者在一个项目中快速集成和使用OkHttp3。 【描述】提到的"okhttp3所有Jar包"暗示了这个压缩包...

    okhttp3.8.0-jdk1.6.zip

    OkHttp3.8.0-jdk1.6.zip是一个专门为Java Web项目设计的网络通信库,它针对JDK1.6进行了优化和重新编译,确保在较低版本的Java环境中也能稳定运行。OkHttp,由Square公司开发,是一款高性能的HTTP客户端库,以其高效...

    okhttp最新版jar

    OkHttp 的最新版本提及的是 2.6.0,这是一个相对早期的版本,尽管如此,它仍然包含了许多重要的功能和优化,旨在提高网络通信的性能和效率。 **OkHttp 的核心特性:** 1. **连接池**:OkHttp 使用连接池来复用已...

Global site tag (gtag.js) - Google Analytics