OKHttp五大拦截器

Posted 高、远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OKHttp五大拦截器相关的知识,希望对你有一定的参考价值。

文章目录

【1】五大拦截器总体概述

OkHttp最核心的工作是在 getResponseWithInterceptorChain() 中进行,在进入这个方法分析之前,我们先来了 解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线


一、责任链设计模式

①定义:

它为请求创建了一个接收者对象的链。为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(责任链模式也叫职责链模式)

在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象 决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的 情况下动态的重 新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象 所接受。

②为什么要使用责任链模式

我觉得原因如下:

  • 解耦
  • 实现单依职责原则

【2】拦截器的工作流程


(借助别人的图了,我太懒了)

一、默认的5大拦截器有哪些?

  • RetryAndFollowUpInterceptor(重试和重定向拦截器)
    第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求
  • BridgeInterceptor(桥接拦截器)
    补全请求,并对响应进行额外处理
  • CacheInterceptor(缓存拦截器)
    请求前查询缓存,获得响应并判断是否需要缓存
  • ConnectInterceptor(链接拦截器)
    与服务器完成TCP连接 (Socket)
  • CallServerInterceptor(请求服务拦截器)
    与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)

【3】RetryAndFollowUpInterceptor拦截器

第一个拦截器: RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向


一、重试

请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。

  • RouteException
catch (RouteException e) 
//todo 路由异常,连接未成功,请求还没发出去
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) 
        throw e.getLastConnectException();
    
    releaseConnection = false;
continue; 
  • IOException
catch (IOException e) 
//todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
// HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
continue; 

两个异常都是根据 recover 方法判断是否能够进行重试,如果返回 true ,则表示允许重试

private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) 
streamAllocation.streamFailed(e);
//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
if (!client.retryOnConnectionFailure()) return false;
//todo 2、如果是RouteException,不用管这个条件,
// 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件 if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//todo 3、判断是不是属于重试的异常
if (!isRecoverable(e, requestSendStarted)) return false; //todo 4、有没有可以用来连接的路由路线
    if (!streamAllocation.hasMoreRoutes()) return false;
    // For failure recovery, use the same route selector with a new connection.
    return true;

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行 请求的重试。其中某些异常是在 isRecoverable 中进行判断:

private boolean isRecoverable(IOException e, boolean requestSendStarted)  // 出现协议异常,不能重试
if (e instanceof ProtocolException) 
      return false;
    
// 如果不是超时异常,不能重试
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;

1、协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来 定义数据,再重试也没用)
2、超时异常,可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。
3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确, 那还怎么重试?
经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比 如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。


二、重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重 定向的判断。重定向的判断位于 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:
		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:
	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;
    

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

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

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

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


【4】BridgeInterceptor 桥接拦截器

连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。
补全请求头:

请求头说明
Content-Type请求体类型,如: application/x-www-form-urlencoded 请求体解析方式
Content-Length / Transfer-Encoding Host请求体解析方式
Host请求的主机站点
Connection: Keep-Alive保持长连接
Accept-Encoding: gzip接受响应支持gzip压缩
Cookiecookie身份辨别
User-Agent请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

1、 保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的 CookieJar 不提供实现
2、如果使用gzip返回的数据,则使用GzipSource包装便于解析。

总结
桥接拦截器的执行逻辑主要就是以下几点

对用户构建的 Request 进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的 Request 将符合网络 请求规范的Request交给下一个拦截器处理,并获取 Response 如果响应体经过了GZIP压缩,那就需要解压,再构 建成用户可用的 Response 并返回


【5】CacheInterceptor缓存拦截器

在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存 在Get请求的缓存)

步骤为:
1、 从缓存中获得对应请求的响应缓存
2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest 与 cacheResponse 。他们的组合如下:

networkRequestcacheResponse说明
NullNot Null直接使用缓存
Not NullNull向服务器发起请求
NullNullokhttp直接返回504
Not NullNot Null发起请求,若得到响应为304(无修改),则更新缓存响应并返回

3、交给下一个责任链继续处理
4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓 存,或是请求服务器都是通过 CacheStrategy 判断。

想讲清楚CacheInterceptor有些困难,也比较复杂,所以这里我只是简单介绍一下。


【6】ConnectInterceptor连接拦截器

打开与目标服务器的连接,并执行下一个拦截器。它简短的可以直接完整贴在这里:

public final class ConnectInterceptor implements Interceptor 
  public final OkHttpClient client;
  public ConnectInterceptor(OkHttpClient client) 
    this.client = client;

  @Override public Response intercept(Chain chain) throws IOException 
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  

虽然代码量很少,实际上大部分功能都封装到其它类去了,这里只是调用而已。

首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在
这里。
"当一个请求发出,需要建立连接,连接建立后需要使用流用来读取数据 ";而这个StreamAllocation就是协调请 求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。

这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了 输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。
StreamAllocation 中简单来说就是维护连接:RealConnection——封装了Socket与一个Socket连接池。可复用 的 RealConnection 需要:

public boolean isEligible(Address address, @Nullable Route route) 
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;
    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) 
    return true; // This connection is a perfect match.
    
    // At this point we don't have a hostname match. But we still be able to carry the request
	if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;
    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;
    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;
    // 4. Certificate pinning must match the host.
    try 
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
     catch (SSLPeerUnverifiedException e) 
      return false;
	
    return true; // The caller's address can be carried by this connection.

1、 if (allocations.size() >= allocationLimit || noNewStreams) return false;
连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者
连接被关闭;那就不允许复用;

2、

if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) 
      return true; // This connection is a perfect match.

DNS、代理、SSL证书、服务器域名、端口完全相同则可复用; 如果上述条件都不满足,在HTTP/2的某些场景下可能仍可以复用(http2先不管)。 所以综上,如果在连接池中找到个连接参数一致并且未被关闭没被占用的连接,则可以复用。

总结
这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。


【7】CallServerInterceptor请求服务连接器

利用 HttpCodec 发出请求到服务器并且解析生成 Response
首先调用 httpCodec.writeRequestHeaders(request); 将请求头写入到缓存中(直到调用 flushRequest() 才真正发
送给服务器)。然后马上进行第一个逻辑判断

 以上是关于OKHttp五大拦截器的主要内容,如果未能解决你的问题,请参考以下文章

OKHttp五大拦截器

OkHttp源码分析:五大拦截器详解

42. OkHttp总结

改造 OKHTTP 离线缓存不起作用

Android网络请求框架—OKHttp 源码解析

使用 kotlin-coroutine 时 OkHttp 调度程序崩溃