欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

04.OkHttp拦截器

程序员文章站 2022-07-13 10:58:48
...

目录介绍

  • 01.interceptor调用链的入口
  • 02.interceptor接口和RealInterceptorChain类
  • 03.Address类详解
  • 04.Route类详解
  • 05.RouteDatabase类详解
  • 06.RouteSelector类详解
  • 07.RetryAndFollowUpInterceptor类详解
  • 08.BridgeInterceptor类详解

01.interceptor调用链的入口

  • OKHttp有两种调用方式,一种是阻塞的同步请求,一种是异步的非阻塞的请求。但是无论同步还是异步都会调用下RealCall的 getResponseWithInterceptorChain方法来完成请求,同时将返回数据或者状态通过Callback来完成。
  • 源代码如下:
    Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //添加 在配置 OkHttpClient 时设置的 interceptors
        interceptors.addAll(client.interceptors());
        //添加 负责失败重试以及重定向的 RetryAndFollowUpInterceptor;
        interceptors.add(retryAndFollowUpInterceptor);
        //添加 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的 响应转换为用户友好的响应的 BridgeInterceptor;
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        //添加 负责读取缓存直接返回、更新缓存的 CacheInterceptor;
        interceptors.add(new CacheInterceptor(client.internalCache()));
         //添加 负责和服务器建立连接的 ConnectInterceptor;
        interceptors.add(new ConnectInterceptor(client));
        //如果不是webSocket
        if (!forWebSocket) {
          //添加 OkHttpClient 时设置的 networkInterceptors;
          interceptors.addAll(client.networkInterceptors());
        }
        //最后 添加 负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor。
        interceptors.add(new CallServerInterceptor(forWebSocket));
    
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);
    }
    
  • 从上面可知,无论同步还是异步,他们的入口都是从RealCall的getResponseWithInterceptorChain进来的。

02.interceptor接口和RealInterceptorChain类

2.1 interceptor接口详解

  • Interceptor 负责拦截和分发。先来看看Intercepor本身文档的含义:观察,修改以及可能短路的请求输出和响应请求的回来。通常情况下拦截器用来添加,移除或者转换请求或者回应的头部信息。
  • 拦截器,就像水管一样,把一节一节的水管(拦截器)连起来,形成一个回路,实际上client到server也是如此,通过一个又一个的interceptor串起来,然后把数据发送到服务器,又能接受返回的数据,每一个拦截器(水管)都有自己的作用,分别处理不同东西,比如消毒,净化,去杂质,就像一层层过滤网一样。
    /**
     * Observes, modifies, and potentially short-circuits requests going out and the corresponding
     * responses coming back in. Typically interceptors add, remove, or transform headers on the request
     * or response.
     */
    public interface Interceptor {
       //负责拦截
      Response intercept(Chain chain) throws IOException;
    
      interface Chain {
        Request request();
         //负责分发、前行
        Response proceed(Request request) throws IOException;
    
        Connection connection();
      }
    }
    
    • Interceptor是一个接口,主要是对请求和相应的过滤处理,其中有一个抽象方法即Response intercept(Chain chain) throws IOException负责具体的过滤。
    • 在他的子类里面又调用了Chain,从而实现拦截器调用链(chain),所以真正实现拦截作用的是其内部接口Chain
  • Interceptor.Chain的实现类都是RealInterceptorChain,也就说说处理调用过程的实现是RealInterceptorChain。所以RealInterceptorChain持有一个List的Interceptor,通过对这个List的Interceptor进行迭代和递归推进。让我们看看源码实现。
    /**
     * A concrete interceptor chain that carries the entire interceptor chain: all application
     * interceptors, the OkHttp core, all network interceptors, and finally the network caller.
     */
    public final class RealInterceptorChain implements Interceptor.Chain {
      private final List<Interceptor> interceptors;
      private final Request request;   
     // 下面属性会在执行各个拦截器的过程中一步一步赋值
      private final StreamAllocation streamAllocation;//在RetryAndFollowUpInterceptor中new的
      private final HttpCodec httpCodec; //在ConnectInterceptor中new的
      private final RealConnection connection; //在ConnectInterceptor中new的
      private final int index;  //通过index + 1
      private int calls;  //通过call++
    
      public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
          HttpCodec httpCodec, RealConnection connection, int index, Request request) {
        this.interceptors = interceptors;
        this.connection = connection;
        this.streamAllocation = streamAllocation;
        this.httpCodec = httpCodec;
        this.index = index;
        this.request = request;
      }
    
      @Override public Connection connection() {
        return connection;
      }
    
      public StreamAllocation streamAllocation() {
        return streamAllocation;
      }
    
      public HttpCodec httpStream() {
        return httpCodec;
      }
    
      @Override public Request request() {
        return request;
      }
       // 实现了父类proceed方法
      @Override public Response proceed(Request request) throws IOException {
        return proceed(request, streamAllocation, httpCodec, connection);
      }
    
      //处理调用
      public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
          RealConnection connection) throws IOException {
        // 1、迭代拦截器集合
        if (index >= interceptors.size()) throw new AssertionError();
         //2、创建一次实例,call+1
        calls++;
        
        // If we already have a stream, confirm that the incoming request will use it.
        if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
          throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
              + " must retain the same host and port");
        }
    
        // If we already have a stream, confirm that this is the only call to chain.proceed().
        if (this.httpCodec != null && calls > 1) {
          throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
              + " must call proceed() exactly once");
        }
    
        // Call the next interceptor in the chain.
        // 3、创建一个RealInterceptorChain实例
        RealInterceptorChain next = new RealInterceptorChain(
            interceptors, streamAllocation, httpCodec, connection, index + 1, request);
        //4、取出下一个 interceptor
        Interceptor interceptor = interceptors.get(index);
        //5、执行intercept方法,拦截器又会调用proceed()方法
        Response response = interceptor.intercept(next);
    
        // Confirm that the next interceptor made its required call to chain.proceed().
        if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
          throw new IllegalStateException("network interceptor " + interceptor
              + " must call proceed() exactly once");
        }
    
        // Confirm that the intercepted response isn't null.
        if (response == null) {
          throw new NullPointerException("interceptor " + interceptor + " returned null");
        }
    
        return response;
      }
    }
    
    • 看到了上述源码,给大家分析一下就是:
    • 第一步,先判断是否超过list的size,如果超过则遍历结束,如果没有超过则继续执行
    • 第二步calls+1
    • 第三步new了一个RealInterceptorChain,其中然后下标index+1
    • 第四步 从list取出下一个interceptor对象
    • 第五步 执行interceptor的intercept方法
  • 总结一下就是每一个RealInterceptorChain对应一个interceptor,然后每一个interceptor再产生下一个RealInterceptorChain,直到List迭代完成。
  • 所以上面基本上就是迭代+递归,找了一些图片有助于大家理解如下图
    • 04.OkHttp拦截器
    • 04.OkHttp拦截器
    • 04.OkHttp拦截器
  • 这里的拦截器有点像安卓里面的触控反馈的Interceptor。既一个网络请求,按一定的顺序,经由多个拦截器进行处理,该拦截器可以决定自己处理并且返回我的结果,也可以选择向下继续传递,让后面的拦截器处理返回它的结果。这个设计模式叫做责任链模式。
  • 与Android中的触控反馈interceptor的设计略有不同的是,后者通过返回true 或者 false 来决定是否已经拦截。
  • 而OkHttp这里的拦截器通过函数调用的方式,讲参数传递给后面的拦截器的方式进行传递。这样做的好处是拦截器的逻辑比较灵活,可以在后面的拦截器处理完并返回结果后仍然执行自己的逻辑;缺点是逻辑没有前者清晰。

03.Address类详解

  • 这个类的作用
    • 翻译类的注释如下:与服务器连接的格式,对于简单的链接,这里是服务器的主机名和端口号。如果是通过代理(Proxy)的链接,则包含代理信息(Proxy)。如果是安全链接,则还包括SSL socket Factory、hostname验证器,证书等。
  • 通过翻译大家可以理解为一个地址的包装类,封装了地址的所有可能,说白了address描述了建立连接的所有配置信息。再来看下它的字段构造函数
    final HttpUrl url;
    final Dns dns;
    final SocketFactory socketFactory;
    final Authenticator proxyAuthenticator;
    final List<Protocol> protocols;
    final List<ConnectionSpec> connectionSpecs;
    final ProxySelector proxySelector;
    final Proxy proxy;
    final SSLSocketFactory sslSocketFactory;
    final HostnameVerifier hostnameVerifier;
    final CertificatePinner certificatePinner;
    
    public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
          SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
          CertificatePinner certificatePinner, Authenticator proxyAuthenticator, Proxy proxy,
          List<Protocol> protocols, List<ConnectionSpec> connectionSpecs, ProxySelector proxySelector) {
        this.url = new HttpUrl.Builder()
            .scheme(sslSocketFactory != null ? "https" : "http")
            .host(uriHost)
            .port(uriPort)
            .build();
    
        if (dns == null) throw new NullPointerException("dns == null");
        this.dns = dns;
    
        if (socketFactory == null) throw new NullPointerException("socketFactory == null");
        this.socketFactory = socketFactory;
    
        if (proxyAuthenticator == null) {
          throw new NullPointerException("proxyAuthenticator == null");
        }
        this.proxyAuthenticator = proxyAuthenticator;
    
        if (protocols == null) throw new NullPointerException("protocols == null");
        this.protocols = Util.immutableList(protocols);
    
        if (connectionSpecs == null) throw new NullPointerException("connectionSpecs == null");
        this.connectionSpecs = Util.immutableList(connectionSpecs);
    
        if (proxySelector == null) throw new NullPointerException("proxySelector == null");
        this.proxySelector = proxySelector;
    
        this.proxy = proxy;
        this.sslSocketFactory = sslSocketFactory;
        this.hostnameVerifier = hostnameVerifier;
        this.certificatePinner = certificatePinner;
    }
    
  • 果然和咱们想的一样,就是一个地址的包装类,包含了三种请求类型的封装1直连,2走代理,3ssl 。
  • 这里先简单的说下Address的构造函数,咱们回想一下什么时候new的Address对象,记忆好的同学可能想起来了,在RetryAndFollowUpInterceptor类里面,我们曾经创建过StreamAllocation类,在构造这个StreamAllocation的对象的时候,需要传入一个Addres对象,而在RetryAndFollowUpInterceptor类中则是通过createAddress来创建的Address对象的
    private Address createAddress(HttpUrl url) {
        SSLSocketFactory sslSocketFactory = null;
        HostnameVerifier hostnameVerifier = null;
        CertificatePinner certificatePinner = null;
        if (url.isHttps()) {
          sslSocketFactory = client.sslSocketFactory();
          hostnameVerifier = client.hostnameVerifier();
          certificatePinner = client.certificatePinner();
        }
    
        return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
            sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
            client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
    }
    
  • 通过构createAddress方法,我们发现除了uriHost和uriPort外的所有构造函数的参数均来自OkHttpClient,而Address的url字段正式根据这两个字段构造的,由此可见,Address的url字段仅仅包含HTTP请求的url的schema+host+port三部分的信息,而不包含path和query等信息。
  • 让我们再来看下它的一个重要方法equalsNonHost
    boolean equalsNonHost(Address that) {
        return this.dns.equals(that.dns)
            && this.proxyAuthenticator.equals(that.proxyAuthenticator)
            && this.protocols.equals(that.protocols)
            && this.connectionSpecs.equals(that.connectionSpecs)
            && this.proxySelector.equals(that.proxySelector)
            && equal(this.proxy, that.proxy)
            && equal(this.sslSocketFactory, that.sslSocketFactory)
            && equal(this.hostnameVerifier, that.hostnameVerifier)
            && equal(this.certificatePinner, that.certificatePinner)
            && this.url().port() == that.url().port();
    }
    
  • 用来返回两个Address是否是同一个地址,这个方法什么时候会被调用那,会在连接池复用的时候调用,因为只有两个Address相同才能说明这两个连接的配置信息是一直的,才能使用RealConnection的复用。看到方法内部大家发现,这个"相同"的要求还是很严格的,必须所有配置信息都一直才可以。
  • 至此这个类基本已经讲解完毕,后续流程涉及到再提及。

04.Route类详解

  • 该类的作用是什么
    • 连接使用的路由到抽象服务器。创建连接时,客户端有很多选择
    • 1、HTTP proxy(http代理):已经为客户端配置了一个专门的代理服务器,否则会通过net.ProxySelector proxy selector尝试多个代理
    • 2、IP address(ip地址):无论是通过直连还是通过代理,DNS服务器可能会尝试多个ip地址。
    • 每一个路由都是上述路由的一种格式
    • 所以我的理解就是OkHttp3中抽象出来的Route是描述网络数据包传输的路径,最主要还是描述直接与其建立TCP连接的目标端点。
  • 现在看下他的字段和构造函数
    final Address address;
    final Proxy proxy;
    final InetSocketAddress inetSocketAddress;
    
    public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
        if (address == null) {
          throw new NullPointerException("address == null");
        }
        if (proxy == null) {
          throw new NullPointerException("proxy == null");
        }
        if (inetSocketAddress == null) {
          throw new NullPointerException("inetSocketAddress == null");
        }
        this.address = address;
        this.proxy = proxy;
        this.inetSocketAddress = inetSocketAddress;
    }
    
  • 所以咱咱们知道Route通过代理服务器的信息proxy,及链接的目标地址Address来描述路由即Route,连接的目标地址inetSocketAddress根据代理类型的不同而有着不同的含义,这主要是通过不同代理协议的差异而造成的。对于无需代理的情况,连接的目标地址inetSocketAddress中包含HTTP服务器经过DNS域名解析的IP地址以及协议端口号;对于SOCKET代理其中包含HTTP服务器的域名及协议端口号;对于HTTP代理,其中则包含代理服务器经过域名解析的IP地址及端口号。
  • 这里面说一个后面能用到的requiresTunnel()方法
    /**
    * Returns true if this route tunnels HTTPS through an HTTP proxy. See <a
    * href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.
    */
    public boolean requiresTunnel() {
        //是HTTP请求,但是还有SSL
        return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
    }
    
  • 即对于设置了HTTP代理,且安全的连接(SSL)需要请求代理服务器,建立一个到目标HTTP服务器的隧道连接,客户端与HTTP代理建立TCP连接,以此请求HTTP代理服务器在客户端与HTTP服务器之间进行数据的盲目转发
  • 即对于设置了HTTP代理,且安全的连接 (SSL) 需要请求代理服务器建立一个到目标HTTP服务器的隧道连接,客户端与HTTP代理建立TCP连接,以此请求HTTP代理服务在客户端与HTTP服务器之间进行数据的盲转发。

05.RouteDatabase类详解

  • 该类的作用是什么?
    • 简单翻译一下就是:当在创建与目标地址的链接时,为了避免重复出现路由故障而创建的黑名单,如果尝试链接特定的IP或者代理服务器最后失败了,将记住这些故障。
  • 这个类很简单, 大家看下
      private final Set<Route> failedRoutes = new LinkedHashSet<>();
    
      /** Records a failure connecting to {@code failedRoute}. */
      public synchronized void failed(Route failedRoute) {
        failedRoutes.add(failedRoute);
      }
    
      /** Records success connecting to {@code failedRoute}. */
      public synchronized void connected(Route route) {
        failedRoutes.remove(route);
      }
    
      /** Returns true if {@code route} has failed recently and should be avoided. */
      public synchronized boolean shouldPostpone(Route route) {
        return failedRoutes.contains(route);
      }
    
  • 看了代码,相信大家都知道了,这个类,内部维护了一个LinkHashSet().如果链接失败了,就放进去,如果成功就删除,还提供了一个判断是否包含路由的方法,用来判断该rount是否存在于LinkedHashSet()里面

06.RouteSelector类详解

6.1 该类的作用

  • 翻译一下就是:这个类主要是选择连接到服务器的路由,每个连接应该是代理服务器/IP地址/TLS模式 三者中的一种。连接月可以被回收,所以可以把RouteSelector理解为路由选择器。

6.2 那为什么需要RouteSelector那?

  • 因为HTTP请求处理过程中所需的TCP连接建立过程,主要是找到一个Route,然后依据代理协议规则与特定目标建立TCP连接。对于无代理的情况,是与HTTP服务器建立TCP连接,对于SOCKS代理和http代理,是与代理服务器建立tcp连接,虽然都是与代理服务器建立tcp连接,但是SOCKS代理协议和http代理协议又有一定的区别。
  • 而且借助于域名做负均衡已经是网络中非常常见的手法了,因而,常常会有域名对应不同IP地址的情况。同时相同系统也可以设置多个代理,这使Route的选择变得非常复杂。
  • 在OKHTTP中,对Route连接有一定的错误处理机制。OKHTTP会逐个尝试找到Route建立TCP连接,直到找到可用的哪一个。这样对Route信息有良好的管理。OKHTTP中借助RouteSelector类管理所有路由信息,并帮助选择路由。

6.3 RouteSelector构造函数

  • 看一下它的字段和构造函数
      private final Address address;
      private final RouteDatabase routeDatabase;
    
      /* The most recently attempted route. */
      private Proxy lastProxy;
      private InetSocketAddress lastInetSocketAddress;
    
      /* State for negotiating the next proxy to use. */
      private List<Proxy> proxies = Collections.emptyList();
      private int nextProxyIndex;
    
      /* State for negotiating the next socket address to use. */
      private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
      private int nextInetSocketAddressIndex;
    
      /* State for negotiating failed routes */
      private final List<Route> postponedRoutes = new ArrayList<>();
    
      public RouteSelector(Address address, RouteDatabase routeDatabase) {
        this.address = address;
        this.routeDatabase = routeDatabase;
    
        resetNextProxy(address.url(), address.proxy());
      }
    
      /** Prepares the proxy servers to try. */
      private void resetNextProxy(HttpUrl url, Proxy proxy) {
        if (proxy != null) {
         //第一种方式
          // If the user specifies a proxy, try that and only that.
          proxies = Collections.singletonList(proxy);
        } else {
         //第二种方式
          // Try each of the ProxySelector choices until one connection succeeds.
          List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
          proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
              ? Util.immutableList(proxiesOrNull)
              : Util.immutableList(Proxy.NO_PROXY);
        }
        nextProxyIndex = 0;
      }
    
  • RouteSelector这个类的字段和构造函数比较简单,但是在构造函数里面调用了resetNextProxy()方法
  • 收集路由主要分为两个步骤:第一步收集所有的代理;第二步则是收集特定的代理服务器选择所有的连接目标的地址。
  • 收集代理的过程正如上面的这段代码所示,有两种方式:
    • 一是外部通过address传入代理,此时代理集合将包含这唯一的代理。address的代理最终来源于OkHttpClient,我们可以在构造OkHttpClient时设置代理,来指定该client执行的所有请求特定的代理。
    • 二是,借助于ProxySelectory获得多个代理。ProxySelector最终也来源于OkHttpClient用户当然也可以对此进行配置。但通常情况下,使用系统默认收集的所有代理保存在列表proxies中
  • 为OkHttpClient配置Proxy或ProxySelector的场景大概是,需要让连接使用代理,但不使用系统的代理配置情况。
  • PS:proxies是在这时候被初始化的。inetSocketAddresses也是在这里被初始化,并且添加的第一个元素

6.4 hasNext()方法

  • hasNext()方法代码
      public boolean hasNext() {
        return hasNextInetSocketAddress()
            || hasNextProxy()
            || hasNextPostponed();
      }
    
      /** Returns true if there's another proxy to try. */
     //是否还有代理
      private boolean hasNextProxy() {
        return nextProxyIndex < proxies.size();
      }
    
      /** Returns true if there's another socket address to try. */ 
      //是否还有socket地址
      private boolean hasNextInetSocketAddress() {
        return nextInetSocketAddressIndex < inetSocketAddresses.size();
      }
    
      /** Returns true if there is another postponed route to try. */
      //是否还有延迟路由
      private boolean hasNextPostponed() {
        return !postponedRoutes.isEmpty();
      }
    
  • hasNext()表明是否有可以使用的路由。里面做了三个判断,如果满足一条就可以表明有可以使用的路由 :1是否还有代理、2是否还有Socket、3是否还有延迟路由,如果三者都没有,则认为没有了。

6.4 next()方法

  • 下面介绍一下他的一个重要方法next()方法。
  • 收集 一个特定代理服务器选择下的连接目标地址,因代理类型的不同而不同,这里主要分3种情况:
    • 1、对于没有配置代理的情况,会对HTTP服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址
    • 2、对于SOCKS代理,直接以HTTP的服务器的域名以及协议端口创建 连接目标地址
    • 3、对于HTTP代理,则会对HTTP代理服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址。
  • 这里其实就是OkHttp发生DNS域名解析的场所。对于使用代理的场景,没有对HTTP服务器的域名做DNS域名解析,也就意味着HTTP服务器的域名解析要由代理服务器完成。
  • 代理服务器的收集是在创建RouteSelector完成的;而一个特定的代理服务器选择下,连接目标地址 收集则是在选择Route时根据需要完成的。
    public Route next() throws IOException {
        // Compute the next route to attempt.
        if (!hasNextInetSocketAddress()) {
          if (!hasNextProxy()) {
            if (!hasNextPostponed()) {
              throw new NoSuchElementException();
            }
            return nextPostponed();
          }
          lastProxy = nextProxy();
        }
        lastInetSocketAddress = nextInetSocketAddress();
    
        Route route = new Route(address, lastProxy, lastInetSocketAddress);
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes.add(route);
          // We will only recurse in order to skip previously failed routes. They will be tried last.
          return next();
        }
    
        return route;
    }
    
  • 这个方法主要是通过收集路由来选择路由。里面分了三种情况
    • 1、如果hasNextPostponed(),则return nextPostponed()。
    • 2、如果hasNextProxy(),则通过nextProxy()获取上一个代理,并用他去构造一个route,如果在失败链接的数据库里面有这个route,则最后通过递归调用next(),否则返回route
    • 3、如果hasNextInetSocketAddress(),则通过nextInetSocketAddress()获取上一个InetSocketAddress,并用他去构造一个route,如果在这个失败里面数据中有这个路由,然后继续通过递归调用next()方法,或者直接返回route。
  • 那么首先我们来看下nextPostponed()这个方法
    private Route nextPostponed() {
        return postponedRoutes.remove(0);
    }
    
  • postponedRoutes是一个list,里面存放的是之前失败链接的路由,目的是在前所有不符合的情况,把之前失败的路由再试一次。再来看一下nextProxy()方法
    /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
    private Proxy nextProxy() throws IOException {
        if (!hasNextProxy()) {
          throw new SocketException("No route to " + address.url().host()
              + "; exhausted proxy configurations: " + proxies);
        }
        Proxy result = proxies.get(nextProxyIndex++);
        resetNextInetSocketAddress(result);
        return result;
    }
    
  • 这个就是从proxies里面去一个出来,proxies是在构造函数里面方法resetNextProxy()来赋值的。咱们再来看下nextInetSocketAddress()方法
    /** Returns the next socket address to try. */
    private InetSocketAddress nextInetSocketAddress() throws IOException {
        if (!hasNextInetSocketAddress()) {
          throw new SocketException("No route to " + address.url().host()
              + "; exhausted inet socket addresses: " + inetSocketAddresses);
        }
        return inetSocketAddresses.get(nextInetSocketAddressIndex++);
    }
    
  • 这个就是从inetSocketAddresses里面取一个出来,proxies是在构造函数里面方法resetNextProxy()来赋值的。

6.6 connectFailed()方法

  • 通过维护失败的路由信息,以避免浪费时间去连接一切不可用的路由。RouteSelector借助于RouteDatabase维护失败的路由信息。
  • 综上所述RouteSelector在OkHttp里面主要负责三件事,1收集路由信息,2选择路由,3维护失败路由。

07.RetryAndFollowUpInterceptor类详解

  • RetryAndFollowUpInterceptor 负责失败重连以及重定向
    public final class RetryAndFollowUpInterceptor implements Interceptor {
      /**
       * 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;
    
      public RetryAndFollowUpInterceptor(OkHttpClient client, boolean forWebSocket) {
        this.client = client;
        this.forWebSocket = forWebSocket;
      }
    
    @Override public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        // 三个参数分别对应:(1)全局的连接池,(2)连接线路Address, (3)堆栈对象
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(request.url()), callStackTrace);
    
        int followUpCount = 0;
        Response priorResponse = null;
        while (true) {
          if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    
          Response response = null;
          boolean releaseConnection = true;
          try {
            //  执行下一个拦截器,即BridgeInterceptor
           // 这里有个很重的信息,即会将初始化好的连接对象传递给下一个拦截器,也是贯穿整个请求的连击对象,上面我们说过,在拦截器执行过程中,RealInterceptorChain的几个属性字段会一步一步赋值
            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;
          } finally {
            // We're throwing an unchecked exception. Release any resources.
            if (releaseConnection) {
              streamAllocation.streamFailed(null);
              streamAllocation.release();
            }
          }
    
          // Attach the prior response if it exists. Such responses never have a body.
          if (priorResponse != null) {
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                        .body(null)
                        .build())
                .build();
          }
           // 检查是否符合要求
          Request followUp = followUpRequest(response);
    
          if (followUp == null) {
            if (!forWebSocket) {
              streamAllocation.release();
            }
            // 返回结果
            return response;
          }
           //不符合,关闭响应流
          closeQuietly(response.body());
           // 是否超过最大限制
          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());
          }
           // 是否有相同的连接
          if (!sameConnection(response, followUp.url())) {
            streamAllocation.release();
            streamAllocation = new StreamAllocation(
                client.connectionPool(), createAddress(followUp.url()), callStackTrace);
          } else if (streamAllocation.codec() != null) {
            throw new IllegalStateException("Closing the body of " + response
                + " didn't close its backing stream. Bad interceptor?");
          }
    
          request = followUp;
          priorResponse = response;
        }
    }
    
  • 我们知道每个拦截器都实现了接口interceptor,interceptor.intercept()方法就是子类用来处理自己的业务逻辑,所以我们仅仅需要分析这个方法即可。看源码我们得出了如下流程
    • 1 根据url创建一个Address对象,初始化一个Socket连接对象,基于Okio
    private Address createAddress(HttpUrl url) {
        SSLSocketFactory sslSocketFactory = null;
        HostnameVerifier hostnameVerifier = null;
        CertificatePinner certificatePinner = null;
        if (url.isHttps()) {
          sslSocketFactory = client.sslSocketFactory();
          hostnameVerifier = client.hostnameVerifier();
          certificatePinner = client.certificatePinner();
        }
    
        return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
            sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
            client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
    }
    
    • 2 用前面创建的address作为参数去实例化StreamAllocation。PS:此处还没有真正的去建立连接,只是初始化一个连接对象
    • 3 开启一个while(true)循环
    • 4 如果取消,释放资源并抛出异常,结束流程
    • 5 执行下一个拦截器,一般是BridgeInterceptor
    • 6 如果发生异常,走到catch里面,判断是否继续请求,不继续请求则退出
    • 7 如果priorResponse不为空,则说明前面已经获取到了响应,这里会结合当前获取的Response和先前的Response
    • 8 调用followUpRequest查看响应是否需要重定向,如果不需要重定向则返回当前请求
    • 9 重定向次数+1,同时判断是否达到最大限制数量。是:退出
    • 10 检查是否有相同的链接,是:释放,重建创建
    • 11 重新设置request,并把当前的Response保存到priorResponse,继续while循环
  • 我们来看下重定向的判断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) {
          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);
    
          case HTTP_UNAUTHORIZED:
            return client.authenticator().authenticate(route, userResponse);
    
          case HTTP_PERM_REDIRECT:
          case HTTP_TEMP_REDIRECT:
            // "If the 307 or 308 status code is received in response to a request other than GET
            // or HEAD, the user agent MUST NOT automatically redirect the request"
            if (!method.equals("GET") && !method.equals("HEAD")) {
              return null;
            }
            // fall-through
          case HTTP_MULT_CHOICE:
          case HTTP_MOVED_PERM:
          case HTTP_MOVED_TEMP:
          case HTTP_SEE_OTHER:
            // Does the client allow redirects?
            if (!client.followRedirects()) return null;
    
            String location = userResponse.header("Location");
            if (location == null) return null;
            HttpUrl url = userResponse.request().url().resolve(location);
    
            // Don't follow redirects to unsupported protocols.
            if (url == null) return null;
    
            // If configured, don't follow redirects between SSL and non-SSL.
            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();
            if (HttpMethod.permitsRequestBody(method)) {
              final boolean maintainBody = HttpMethod.redirectsWithBody(method);
              if (HttpMethod.redirectsToGet(method)) {
                requestBuilder.method("GET", null);
              } else {
                RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                requestBuilder.method(method, requestBody);
              }
              if (!maintainBody) {
                requestBuilder.removeHeader("Transfer-Encoding");
                requestBuilder.removeHeader("Content-Length");
                requestBuilder.removeHeader("Content-Type");
              }
            }
    
            // When redirecting across hosts, drop all authentication headers. This
            // is potentially annoying to the application layer since they have no
            // way to retain them.
            if (!sameConnection(userResponse, url)) {
              requestBuilder.removeHeader("Authorization");
            }
    
            return requestBuilder.url(url).build();
    
          case HTTP_CLIENT_TIMEOUT:
            // 408's are rare in practice, but some servers like HAProxy use this response code. The
            // spec says that we may repeat the request without modifications. Modern browsers also
            // repeat the request (even non-idempotent ones.)
            if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
              return null;
            }
    
            return userResponse.request();
    
          default:
            return null;
        }
    }
    
  • 这里主要是根据响应码(code)和响应头(header),查看是否需要重定向,并重新设置请求,当然,如果是正常响应则直接返回Response停止循环
    private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);
         // 1. 应用层配置不在连接,默认为true
        // The application layer has forbidden retries.
        if (!client.retryOnConnectionFailure()) return false;
         // 2. 请求Request出错不能继续使用
        // We can't send the request body again.
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
        //  是否可以恢复的
        // This exception is fatal.
        if (!isRecoverable(e, requestSendStarted)) return false;
        // 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;
      }
    
      private boolean isRecoverable(IOException e, boolean requestSendStarted) {
        // If there was a protocol problem, don't recover.
        if (e instanceof ProtocolException) {
          return false;
        }
    
        // If there was an interruption don't recover, but if there was a timeout connecting to a route
        // we should try the next route (if there is one).
        if (e instanceof InterruptedIOException) {
          return e instanceof SocketTimeoutException && !requestSendStarted;
        }
    
        // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
        // again with a different route.
        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;
        }
    
        // An example of one we might want to retry with a different route is a problem connecting to a
        // proxy and would manifest as a standard IOException. Unless it is one we know we should not
        // retry, we return true and try a new route.
        return true;
    }
    
  • 看上面代码可以这样理解:判断是否可以恢复如果下面几种条件符合,则返回true,代表可以恢复,如果返回false,代表不可恢复。
    • 1、应用层配置不在连接(默认为true),则不可恢复
    • 2、请求Request是不可重复使用的Request,则不可恢复
    • 3、根据Exception的类型判断是否可以恢复的 (isRecoverable()方法)
      • 3.1、如果是协议错误(ProtocolException)则不可恢复
      • 3.2、如果是中断异常(InterruptedIOException)则不可恢复
      • 3.3、如果是SSL握手错误(SSLHandshakeException && CertificateException)则不可恢复
      • 3.4、certificate pinning错误(SSLPeerUnverifiedException)则不可恢复
    • 4、没用更多线路可供选择 则不可恢复
      • 如果上述条件都不满足,则这个request可以恢复
  • 综上所述:
    • 一个循环来不停的获取response。每循环一次都会获取下一个request,如果没有,则返回response,退出循环。而获取下一个request的逻辑,是根据上一个response返回的状态码,分别作处理。

08.BridgeInterceptor类详解

  • BridgeInterceptor :负责对Request和Response报文进行加工,具体如下:
    • 1、请求从应用层数据类型类型转化为网络调用层的数据类型。
    • 2、将网络层返回的数据类型 转化为 应用层数据类型。
    • 3、补充:Keep-Alive 连接:
  • 区别如下图:
    • 04.OkHttp拦截器
  • 看下源码:
    @Override
    // 主要方法,其他略
    // 此拦截器较为简单,其中有两点比较重要,1、cookie的处理 2Gzip
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request userRequest = chain.request();
        Request.Builder requestBuilder = userRequest.newBuilder();
        RequestBody body = userRequest.body();
        if (body != null) {
            MediaType contentType = body.contentType();
            if (contentType != null) {
                requestBuilder.header("Content-Type", contentType.toString());
            }
            long contentLength = body.contentLength();
            if (contentLength != -1) {
                requestBuilder.header("Content-Length", Long.toString(contentLength));
                requestBuilder.removeHeader("Transfer-Encoding");
            } else {
                requestBuilder.header("Transfer-Encoding", "chunked");
                requestBuilder.removeHeader("Content-Length");
            }
        }
        if (userRequest.header("Host") == null) {
            requestBuilder.header("Host", hostHeader(userRequest.url(), false));
        }
        if (userRequest.header("Connection") == null) {
            requestBuilder.header("Connection", "Keep-Alive");
        }
        // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
        // the transfer stream.
        boolean transparentGzip = false;
        if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
            transparentGzip = true;
            requestBuilder.header("Accept-Encoding", "gzip");
        }
         // 所以返回的cookies不能为空,否则这里会报空指针
        List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
        if (!cookies.isEmpty()) {
            // 创建Okhpptclitent时候配置的cookieJar,
            requestBuilder.header("Cookie", cookieHeader(cookies));
        }
        if (userRequest.header("User-Agent") == null) {
            requestBuilder.header("User-Agent", Version.userAgent());
        }
    
        //  以上为请求前的头处理
        Response networkResponse = chain.proceed(requestBuilder.build());
         // 以下是请求完成,拿到返回后的头处理
         // 响应header, 如果没有自定义配置cookie不会解析
        HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
        Response.Builder responseBuilder = networkResponse.newBuilder()
                .request(userRequest);
         // 前面解析完header后,判断服务器是否支持gzip压缩格式,如果支持将交给Okio处理
        if (transparentGzip
                && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
                && HttpHeaders.hasBody(networkResponse)) {
            GzipSource responseBody = new GzipSource(networkResponse.body().source());
            Headers strippedHeaders = networkResponse.headers().newBuilder()
                    .removeAll("Content-Encoding")
                    .removeAll("Content-Length")
                    .build();
            responseBuilder.headers(strippedHeaders);
            // 处理完成后,重新生成一个response
            responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
        }
        return responseBuilder.build();
    }
    
  • 读了源码发现这个interceptor比较简单,可以分为发送请求和响应两个阶段来看:
    • 1.在发送阶段BridgeInterceptor补全了一些header包括Content-Type、Content-Length、Transfer-Encoding、Host、Connection、Accept-Encoding、User-Agent。
    • 2.如果需要gzip压缩则进行gzip压缩
    • 3.加载Cookie
    • 4.随后创建新的request并交付给后续的interceptor来处理,以获取响应。
    • 5.首先保存Cookie
    • 6.如果服务器返回的响应content是以gzip压缩过的,则会先进行解压缩,移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。
    • 7 否则直接返回response
  • 其中* CookieJar来自 OkHttpClient*,他是OKHttp的Cookie管理类,负责Cookie的存取。
    public interface CookieJar {
      /** A cookie jar that never accepts any cookies. */
      CookieJar NO_COOKIES = new CookieJar() {
        @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        }
    
        @Override public List<Cookie> loadForRequest(HttpUrl url) {
          return Collections.emptyList();
        }
      };
    
      /**
       * Saves {@code cookies} from an HTTP response to this store according to this jar's policy.
       *
       * <p>Note that this method may be called a second time for a single HTTP response if the response
       * includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's
       * cookies.
       */
      void saveFromResponse(HttpUrl url, List<Cookie> cookies);
    
      /**
       * Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly
       * empty list of cookies for the network request.
       *
       * <p>Simple implementations will return the accepted cookies that have not yet expired and that
       * {@linkplain Cookie#matches match} {@code url}.
       */
      List<Cookie> loadForRequest(HttpUrl url);
    }
    
  • 由于OKHttpClient默认的构造过程可以看到,OKHttp默认是没有提供Cookie管理功能的,所以如果想增加Cookie管理需要重写里面的方法,PS:如果重写CookieJar()需要注意loadForRequest()方法的返回值不能为null
    public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
          // 没有配置,不解析
        if (cookieJar == CookieJar.NO_COOKIES) return;
        // 此处遍历,解析Set-Cookie的值,比如max-age
        List<Cookie> cookies = Cookie.parseAll(url, headers);
        if (cookies.isEmpty()) return;
         // 然后保存,即自定义
        cookieJar.saveFromResponse(url, cookies);
    }
    
相关标签: OkHttp