OkHttp连接拦截器篇

缓存篇

流程图

在这里插入图片描述

ConnectInterceptor详解

ExchangeFinder

  • 该类主要通过findHealthyConnection函数生成一个RealConnection,然后构造一个ExchangeCodec对象用于网络请求。
  • 生成RealConnection基于如下规则:
  1. 如果已经有connection连接满足要求,那么直接服用当前链路。
  2. 如果没有满足要求的connection连接,那么在ConnectionPool连接池中寻找可用连接,进行连接服用。
  3. 如果连接池中也没有找到可用connection连接,那么根据路由表(可能会DNS请求),尝试建立一个可用连接。
/**
 * Attempts to find the connections for a sequence of exchanges. This uses the following strategies:
 *
 * <ol>
 *   <li>If the current call already has a connection that can satisfy the request it is used.
 *       Using the same connection for an initial exchange and its follow-ups may improve locality.
 *
 *   <li>If there is a connection in the pool that can satisfy the request it is used. Note that
 *       it is possible for shared exchanges to make requests to different host names! See {@link
 *       RealConnection#isEligible} for details.
 *
 *   <li>If there's no existing connection, make a list of routes (which may require blocking DNS
 *       lookups) and attempt a new connection them. When failures occur, retries iterate the list
 *       of available routes.
 * </ol>
 *
 * <p>If the pool gains an eligible connection while DNS, TCP, or TLS work is in flight, this finder
 * will prefer pooled connections. Only pooled HTTP/2 connections are used for such de-duplication.
 */

ExchangeFinder#findConnection源码分析

  1. 注释1处transmitter中的connection不为空,而且是可用的直接返回当前连接。
  2. 注释2处说明当前transmitter中的connection为空,尝试通过transmitterAcquirePooledConnection获取ConnectionPool连接池中的可用连接。
  3. 注释3,nextRouteToTry是在注释13处赋值的,它并不是记录的routes列表中的下一个路由地址。它只是记录一种异常情况:在ConnectionPool获取到可用连接,但是这个连接又马上unhealthy
  4. 重试当前的Route路由地址。
  5. 返回可用连接,有可能是transmitter本身自带的,也有可能是ConnectionPool中获取的
  6. 获取下一个Proxy的路由表,此处会进行DNS。返回RouteSelector.Selection对象。每个Selection包含一个路由表。
  7. 根据获取的路由表,再次尝试获取ConnectionPool连接池中的可用连接。
  8. ConnectionPool没有找到可用连接,获取一个Route路由对象,每个。
  9. 创建一个RealConnection对象,并且传入Route路由对象。
  10. 如果注释7出找到了可用连接,直接返回。
  11. 真正的想服务器发起socket连接。可能进行TCP + TLS handshakes + Http/2.0协商 等步骤
  12. 在注释11建立一条新的RealConnection后,在尝试在ConnectionPool获取是否有可用连接,如果找到了可用连接,优先复用在连接池中找到的连接。并且关闭注释11中建立的RealConnection,给nextRouteToTry赋值当前的Route路由对象(因为这个route对象是可用的)。注意传入的最后一个参数是true,表明是要求http2.0多路复用。
  13. 参考注释3 和 注释12
  14. 注释12中没有在ConnectionPool中找到可用连接,将本次新建立的连接放到连接池中。
  15. transmitter对象中的RealConnection变量赋值。
/**
 * Returns a connection to host a new stream. This prefers the existing connection if it exists,
 * then the pool, finally building a new connection.
 */
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  RealConnection releasedConnection;
  Socket toClose;
  synchronized (connectionPool) {
    if (transmitter.isCanceled()) throw new IOException("Canceled");
    hasStreamFailure = false; // This is a fresh attempt.

    // Attempt to use an already-allocated connection. We need to be careful here because our
    // already-allocated connection may have been restricted from creating new exchanges.
    releasedConnection = transmitter.connection;
    toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
        ? transmitter.releaseConnectionNoEvents() : null; // 注释1

    if (transmitter.connection != null) { // 注释1
      // We had an already-allocated connection and it's good.
      result = transmitter.connection;
      releasedConnection = null;
    }

    if (result == null) { // 注释2
      // Attempt to get a connection from the pool.
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
        foundPooledConnection = true;
        result = transmitter.connection;
      } else if (nextRouteToTry != null) {  // 注释3
        selectedRoute = nextRouteToTry;
        nextRouteToTry = null;
      } else if (retryCurrentRoute()) {  // 注释4
        selectedRoute = transmitter.connection.route();
      }
    }
  }
  // 省略代码...
  if (result != null) {
    // If we found an already-allocated or pooled connection, we're done.
    return result; // 注释5
  }

  // 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;
    routeSelection = routeSelector.next(); // 注释6
  }

  List<Route> routes = null;
  synchronized (connectionPool) {
    if (transmitter.isCanceled()) throw new IOException("Canceled");

    if (newRouteSelection) {
      // Now that we have a set of IP addresses, make another attempt at getting a connection from
      // the pool. This could match due to connection coalescing.
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(
          address, transmitter, routes, false)) { // 注释7
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

    if (!foundPooledConnection) {
      if (selectedRoute == null) {
        selectedRoute = routeSelection.next(); // 注释8
      }

      // Create a connection and assign it to this allocation immediately. This makes it possible
      // for an asynchronous cancel() to interrupt the handshake we're about to do.
      result = new RealConnection(connectionPool, selectedRoute); // 注释9
      connectingConnection = result;
    }
  }

  // If we found a pooled connection on the 2nd time around, we're done.
  if (foundPooledConnection) {  // 注释10
    eventListener.connectionAcquired(call, result);
    return result;
  }

  // Do TCP + TLS handshakes. This is a blocking operation.
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);  // 注释11
  connectionPool.routeDatabase.connected(result.route());

  Socket socket = null;
  synchronized (connectionPool) {
    connectingConnection = null;
    // Last attempt at connection coalescing, which only occurs if we attempted multiple
    // concurrent connections to the same host.
    // 注释12
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // We lost the race! Close the connection we created and return the pooled connection.
      result.noNewExchanges = true;
      socket = result.socket();
      result = transmitter.connection;

      // It's possible for us to obtain a coalesced connection that is immediately unhealthy. In
      // that case we will retry the route we just successfully connected with.
      nextRouteToTry = selectedRoute; // 注释13
    } else {
      connectionPool.put(result); // 注释14
      transmitter.acquireConnectionNoEvents(result); // 注释15
    }
  }
  closeQuietly(socket);

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

RealConnection

  • 表示一个真实的socket连接。
  • RealConnection#connect是一个重要的函数,这个函数建立了TCP socket、sslSocket连接、进行https TLS安全协商过程、HTTP 2.0协议协商
  • socket的连接动作都是在这个函数完成的。几个关键函数RealConnection#connectconnectSocketcreateTunnelestablishProtocolconnectTlsstartHttp2
  • createTunnel中发送的CONNECT消息,参考资料https://www.jianshu.com/p/54357cdd4736
http协议版本连接流程
http/1.11. DNS
2. TCP Socket(未加密)
https/1.11. DNS
2. TCP Socket(未加密)
3. TLS安全隧道。"CONNECT"消息、TLS协商
http/2.01. DNS
2. TCP Socket(未加密)
3. TLS安全隧道。"CONNECT"消息、TLS协商
4. Http2协商。sendConnectionPreface、settings

连接池ConnectionPool

  • transmitterAcquirePooledConnection获取缓存池中的可用连接。
  • put函数,向缓存池中增加可用连接。

Transmitter

在这里插入图片描述

代理和DNS

在这里插入图片描述

Java定义的代理类型三种DIRECTHTTPSOCKS

  • 需要注意的是HTTP代理HTTP协议是两个不同的概念。
  • SOCKS代理是不需要在本地DNS解析的,可以直接将域名发送到SOCKS代理服务器上。DIRECTHTTP代理需要进行DNS解析。参考方法RouteSelector#resetNextInetSocketAddress
  • HTTP代理分为普通代理和隧道代理。普通代理是明文传输,即http请求;隧道代理是加密传输,仅仅转发TCP包,即https和http2.0。隧道代理在建立时,会要求先发送connect请求建立隧道,进行SSL握手,因为数据是加密的,代理服务器没有办法解析数据,只能转发数据。
  • 默认的ProxySelector类是sun.net.spi.DefaultProxySelector,在OkHttpClient.Builder#Builder()初始化默认值。参考方法java.net.ProxySelector#getDefault
  • 代理和路由相关的参考资料:https://www.jianshu.com/p/63ba15d8877a

DNS解析

  • 提供一个域名,返回一个IP地址列表。
  • URL通过DNS解析成为具体的服务器IP地址。java提供的DNS解析方法java.net.InetAddress#getAllByName。可以解析IPV4 和 IPV6地址。
  • Dns接口定义了lookup方法寻找IP地址。
  • Dns#SYSTEM变量实现了该接口。匿名内部类。
  • DNS寻址动作在 RouteSelector#resetNextInetSocketAddress方法中实现
//okhttp3.internal.connection.RouteSelector#resetNextInetSocketAddress
if (proxy.type() == Proxy.Type.SOCKS) { // 如果是socks代理,是不需要DNS解析的,可以直接将域名发送到服务器进行解析。
  inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else { // DIRECT、HTTP代理需要进行DNS解析
  eventListener.dnsStart(call, socketHost);

  // Try each address for best behavior in mixed IPv4/IPv6 environments.
  List<InetAddress> addresses = address.dns().lookup(socketHost); // DNS解析
  if (addresses.isEmpty()) {
    throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost);
  }

  eventListener.dnsEnd(call, socketHost, addresses);

  for (int i = 0, size = addresses.size(); i < size; i++) {
    InetAddress inetAddress = addresses.get(i);
    inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
  }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值