Android Okhttp3《四》CacheInterceptor之DiskLruCache

前言

本章我们介绍一下ConnectInterceptor,这个类主要是与服务端建立连接操作,例如HTTP1,HTTP2,HTTPS以及HTTP代理与Socktet代理。在 OKHTTP 底层是通过 SOCKET 的方式于服务端进行连接的,并且在连接建立之后会通过 OKIO 获取通向 server 端的输入流 Source 和输出流 Sink。

这里我们仅仅分析HTTP1 的相关部分,同时本篇不会像之前几篇写的那么细致入微,这里仅仅大致分析一下其实现方式。

一、ConnectInterceptor

1.1 获取链接

  @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");
    //创建新的stream
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

streamAllocation 返回的StreamAllocation 对象实际创建与RetryAndFollowUpInterceptor 中

在这里插入图片描述

streamAllocation.newStream 通过这个方法得到一个 HttpStream 这个接口有两个实现类分别是 Http1xStream 和 Http2xStream 现在只分析 Http1xStream ,这个 Http1xStream 流是通过 SOCKET 与服务端建立连接之后,通向服务端的输入和输出流的封装。

  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
      // 这些时间设置都是从OKHttpClinet里面读取过来的
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
    //创建连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
          //创建HttpCodec 
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

findHealthyConnection 在连接池中寻找或者在新创建一个连接 ,findHealthyConnection 放到后面再说,我们这里先看一下newCodec

  public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
    // 这是HTT2部分,这里不做分析
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
    //配置超时时间
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

HttpCodec 可以看成是一个sokcet连接HttpCodec 通过这个链接发送HTTP请求并且读取返返回数据
在这里插入图片描述

现在我们看看上面遗漏的findHealthyConnection 方法

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
    int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
    boolean doExtensiveHealthChecks) throws IOException {
  while (true) {
    RealConnection candidate = findConnection(connectTimeout, readTimeout, 	
writeTimeout,
        pingIntervalMillis, connectionRetryEnabled);

  //新创建的RealConnection,此时直接返回就可以了,省略后面的检查
    synchronized (connectionPool) {
      if (candidate.successCount == 0) {
        return candidate;
      }
    }

//检查是否健康,若是健康就可以复用。
// 内部主要是实际是检查socket 是否已经关闭,
    if (!candidate.isHealthy(doExtensiveHealthChecks)) {
      noNewStreams();
      continue;
    }

    return candidate;
  }
}

这里还没有接触到核心代码,我们继续进入findConnection 看看

//代码有删减

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  Connection releasedConnection;
  Socket toClose;
  synchronized (connectionPool) {
    releasedConnection = this.connection;
    toClose = releaseIfNoNewStreams();
// 例如重定向的时候,地址又没有改变,这是与第一次访问使用同一个StreatAloocation
//,其内部已经创建的connection ,这个时候直接复用。
//或者是重试的时候使用的也是同一个StreatAloocation。
//具体的参见RetryAndFollpwUpInterceptor,
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      result = this.connection;
      releasedConnection = null;
    }
    if (!reportedAcquired) {
      // If the connection was never reported acquired, don't report it as released!
      releasedConnection = null;
    }
//还没有分配连接,一般是初始访问,没有经历重定向,与重试
    if (result == null) {
      // 在连接池寻找可用的链接,注意最后一个参数是null
      //关于如何复用的在后面介绍
      Internal.instance.get(connectionPool, address, this, null);
       //connection != null 表示在连接池找到了可用的链接
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
      } else {
       //没有找到可用的
        selectedRoute = route;
      }
    }
  }
  closeQuietly(toClose);


  if (result != null) {
    // If we found an already-allocated or pooled connection, we're done.
    return result;
  }

  // If we need a route selection, make one. This is a blocking operation.
  boolean newRouteSelection = false;
  if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
    newRouteSelection = true;
   //这里一般是设置了代理之后会走到这里,前面根据要访问的url没有找到可以复用的
   //链接,此时根据用户配置的代理查找可用的链接 
    routeSelection = routeSelector.next();
  }

  synchronized (connectionPool) {
    if (canceled) throw new IOException("Canceled");

    if (newRouteSelection) {
  //遍历当前url 对应的所有ip地址
  //一个代理可能对用多个ip地址。
      List<Route> routes = routeSelection.getAll();
      for (int i = 0, size = routes.size(); i < size; i++) {
        Route route = routes.get(i);
        //查询是否存在可以使用的链接,注意最后的参数非空
        Internal.instance.get(connectionPool, address, this, route);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
          this.route = route;
          break;
        }
      }
    }
//
//没有在Connection里面找到可用的链接
    if (!foundPooledConnection) {
      if (selectedRoute == null) {
//更换路径
        selectedRoute = routeSelection.next();
      }
      route = selectedRoute;
      refusedStreamCount = 0;
// 还是没有找到可以复用的链接,
//此时只可以建立一个新的连接
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result, false);
    }
  }

  // If we found a pooled connection on the 2nd time around, we're done.
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
    return result;
  }
  //开始三次握手,与TLS握手
  // Do TCP + TLS handshakes. This is a blocking operation.
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  routeDatabase().connected(result.route());

  Socket socket = null;
  synchronized (connectionPool) {
    reportedAcquired = true;

//保存新建立的链接
    Internal.instance.put(connectionPool, result);

    if (result.isMultiplexed()) {
      socket = Internal.instance.deduplicate(connectionPool, address, this);
      result = connection;
    }
  }
  closeQuietly(socket);

  eventListener.connectionAcquired(call, result);
  return result;
}

findConnection() 返回一个用于流执行底层IO的连接。这个方法优先复用已经创建的连接;在没有可复用连接的情况下新建一个。

1.1 查询连接

前面我们看到查询复用链接的方法为Internal.instance.get(connectionPool, address, this, route);其中Internal.instance 的实现类是OKHttpClient 的一个内部类

            public RealConnection get(ConnectionPool pool, Address address, StreamAllocation streamAllocation, Route route) {
                return pool.get(address, streamAllocation, route);
            }

Internal 的get 方法调了ConnectionPool 的get 方法

  @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

这里就是遍历连接池里面的链接然后调用connection 的isEligible 判断这个链接是不是可以复用在address 上。

  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;

    // this.route.address() 返回的是创建这个Connection 的时候使用的address
    //实际最终调用的额Address 的equalsNonHost方法判断是否相同。
    //
    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.
    }

    if (http2Connection == null) return false;


    if (route == null) return false;
      // 当router参数不为空的的时候,下面就是根据route来寻找可服用的链接
     //到了这里一般是使用了HTTP代理
    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;
    //这里面比较的是X509的SAN扩展
    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) {
    //这个错误会抛到RetryAndFollowUp那里去处理,重试
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

如果不考虑代理的情况下一般查询复用就只会走前面两步

    // 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 (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

Address.java

  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();
  }

proxyAuthenticator ,connectionSpecs,proxySelector ,dns 等等都是从从client里面获取到的变量,因此正常情况下两个相同的主机地址这里比较都会返回true,个人觉得这里到更像是比较这两个Address 是不是由同一个OKHttpcliet 对象创建。

equalsNonHost 比较成功之后就比较主机名,如果主机一样就代表这个链接可以复用,例如
https://blog.csdn.net/guolin_blog/article/details/28863651 与https://blog.csdn.net/freak_csh/article/details/95009057
这两个url 的主机是一致的,因此链接也就可以复用。

创建连接’

在创建完RealConnection 之后会调用connect 方法建立网络连接,这里就不在分析这一块了,感兴趣的朋友可以自己研究一下。

  public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

   //代码有删减,
    while (true) {
      try {
        if (route.requiresTunnel()) {
        //建立http隧道,也就是https 的代理
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
        //打开socket
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        /如果是HTTPS或者是HTTP2 的话会进行对应的协议的处理,例如tsl握手
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
			//代码有删减
	  if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }
        
        throw routeException;
      }
    }

    if (route.requiresTunnel() && rawSocket == null) {
      // 建立失败
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

这里主要是调用connectSocket 或connectTunnel方法建立socket 连,如果是普通的http 协议,此时就可以通过这个sokcet 发送HTTP 数据。如果使用的是HTTPS 或者是HTTP2 协议会接着调用establishProtocol 建相应的协议。这里还想说一下异常的处理,
可以看到所有的有的io异常都封装成了RouteException,关于RouteException 我们在RetryAndFollowUpInterceptor 介绍过,RetryAndFollowUpInterceptor 会捕获所有的RouteException,然后判断如果该地址还对应这其他的Ip地址的话,会自动尝试与连一个ip建立连接。
下面我们先看一下connectSocket。

  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
	//根据代理类型创建不同的socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
    //根据平台建立连接,放到andorid 平台就是一句代码
    //socket.connect(address, connectTimeout);
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }

    try {
    //okio 是OKHTTP团队封装的一套io工具类。
    //获取输入流
      source = Okio.buffer(Okio.source(rawSocket));
      //获取输出流
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
  }

不考虑代理的话,这里就是直接new 一个Socket 然后调用socket的connect 方法,进而与服务器建立了连接,建立连接之后获取到对应的输入输出流。

  private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {

	//代码有删减
    eventListener.secureConnectStart(call);
    //tsl握手
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
      //http2 协议处理
      http2Connection = new Http2Connection.Builder(true)
          .socket(socket, route.address().url().host(), source, sink)
          .listener(this)
          .pingIntervalMillis(pingIntervalMillis)
          .build();
      http2Connection.start();
    }
  }

关于tsl 握手与http2 的内部,这里也不做多余的介绍了,干兴趣的朋友可以自己查点资料看看。至此ConnectInterceptor 也就是介绍完成了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值