okhttp5

okhttp分享5:ConnectInterceptor(1)

按顺序我们现在走到了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);
  }
}

可以看到总共没几行代码,核心代码就两句

HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

虽然只有两句,但是却涉及okhttp连接池,路由选择策略等多种逻辑,我们一步步看,这两步核心逻辑都是在streamAllocation中实现的。我们回忆一下,这个streamAllocation最早是在RetryAndFollowUpInterceptor中实例化的,我们看一下RetryAndFollowUpInterceptor中代码

StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);

在看下其构造方法

  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
                          EventListener eventListener, Object callStackTrace) {
    this.connectionPool = connectionPool;//连接池
    this.address = address;//地址相关信息
    this.call = call;//用户请求
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);//路由选择策略
    this.callStackTrace = callStackTrace;
  }

其中我们主要关注routeSelector和connectionPool

一、路由选择

之前我们说到,okhttp会自动选择最优的路由,并以此建立或复用连接。连接的建立是要基于路由选择的,我们先看一下okhttp的路由相关类

1.1、Address

final HttpUrl url;
  final Dns dns;
  final SocketFactory socketFactory;
  final Authenticator proxyAuthenticator;
  final List protocols;
  final List connectionSpecs;
  final ProxySelector proxySelector;
  final @Nullable
  Proxy proxy;
  final @Nullable SSLSocketFactory sslSocketFactory;
  final @Nullable HostnameVerifier hostnameVerifier;
  final @Nullable
  CertificatePinner certificatePinner;

  public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
                 @Nullable SSLSocketFactory sslSocketFactory, @Nullable HostnameVerifier hostnameVerifier,
                 @Nullable CertificatePinner certificatePinner, Authenticator proxyAuthenticator,
                 @Nullable Proxy proxy, List protocols, List 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;
  }

可以看出这是一个地址封装类,是在RetryAndFollowUpInterceptor实例化StreamAllocation时作为参数实例化的。对于简单的链接,这里是服务器的主机名和端口号。如果是通过代理(Proxy)的链接,则包含代理信息(Proxy)。如果是安全链接,则还包括SSL socket Factory、hostname验证器,证书等。
注意下这个类注释的最后一句

HTTP requests that share the same {@code Address} may also share the same {@link Connection}.,拥有相同address的http请求可以共用同一个连接。配合在看下这个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();
  }

1.2、Route

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来描述的路由,连接的目标地址inetSocketAddress根据代理类型的不同而有着不同的含义,这主要是通过不同代理协议的差异而造成的。对于无需代理的情况,连接的目标地址inetSocketAddress中包含HTTP服务器经过DNS域名解析的IP地址以及协议端口号;对于SOCKET代理其中包含HTTP服务器的域名及协议端口号;对于HTTP代理,其中则包含代理服务器经过域名解析的IP地址及端口号。

1.3、RouteDatabase

这个是用于记录路由历史情况的类,看下源码

/**
 * A blacklist of failed routes to avoid when creating a new connection to a target address. This is
 * used so that OkHttp can learn from its mistakes: if there was a failure attempting to connect to
 * a specific IP address or proxy server, that failure is remembered and alternate routes are
 * preferred.
 */
public final class RouteDatabase {
  private final Set failedRoutes = new LinkedHashSet<>();

  /** Records a failure connecting to {@code failedRoute}. */
  public synchronized void failed(Route failedRoute) {
    failedRoutes.add(failedRoute);
  }

  /** Records success connecting to {@code route}. */
  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,并提供一个方法(shouldPostpone)用于判断传入路由是否在失败列表中。

1.4、RouteSelector

下面我们就要看下okhttp路由选择的核心类RouteSelector,大家可以把它理解为路由选择器。android客户端与服务端网络通信的过程,可以细分成很多块,大体分的话其实就是两步首先建立连接(tcp/udp),连接建立后基于当前连接及请求构建信息传输流(http/socket),通过流进行io操作,与服务器通信。做个简单的比喻,连接就是桥梁,流就是桥上跑的货车,信息就是货车装载的货物。而tcp连接建立过程所需要的关键元素就是Route,现在借助于域名做负载均衡也十分常见,所以路由选择也变得较为复杂,okhttp中通过RouteSelector对路由信息进行管理,使建立tcp连接时可以使用最优的路由。下面我们看下源码

private final Address address;
  private final RouteDatabase routeDatabase;
  private final Call call;
  private final EventListener eventListener;

  /* State for negotiating the next proxy to use. */
  private List proxies = Collections.emptyList();
  private int nextProxyIndex;

  /* State for negotiating the next socket address to use. */
  private List inetSocketAddresses = Collections.emptyList();

  /* State for negotiating failed routes */
  private final List postponedRoutes = new ArrayList<>();

  public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
                       EventListener eventListener) {
    this.address = address;
    this.routeDatabase = routeDatabase;
    this.call = call;
    this.eventListener = eventListener;

    resetNextProxy(address.url(), address.proxy());
  }

成员变量都是之前提过的内容,直接看构造器中调用的resetNextProxy方法

/** 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 proxiesOrNull = address.proxySelector().select(url.uri());
      proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
          ? Util.immutableList(proxiesOrNull)
          : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
  }

这个方法主要是选择可用代理,如果用户传如proxy则就指定使用该proxy;若用户没有指定proxy则通过proxySelector根据请求uri产生proxy,若产生的proxy为null则使用Proxy.NO_PROXY。
这边我们注意下proxySelector(),这个是在okhttpclient传入的代理选择策略,用户若不传入则使用系统默认的,我们简单看下系统默认的ProxySelectorImpl,在java.net包中

final class ProxySelectorImpl extends ProxySelector {

    @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        if (uri == null || sa == null || ioe == null) {
            throw new IllegalArgumentException();
        }
    }

    @Override public List select(URI uri) {
        return Collections.singletonList(selectOneProxy(uri));
    }

    private Proxy selectOneProxy(URI uri) {
        if (uri == null) {
            throw new IllegalArgumentException("uri == null");
        }
        String scheme = uri.getScheme();
        if (scheme == null) {
            throw new IllegalArgumentException("scheme == null");
        }

        int port = -1;
        Proxy proxy = null;
        String nonProxyHostsKey = null;
        boolean httpProxyOkay = true;
        if ("http".equalsIgnoreCase(scheme)) {
            port = 80;
            nonProxyHostsKey = "http.nonProxyHosts";
            proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
        } else if ("https".equalsIgnoreCase(scheme)) {
            port = 443;
            nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
            proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
        } else if ("ftp".equalsIgnoreCase(scheme)) {
            port = 80; // not 21 as you might guess
            nonProxyHostsKey = "ftp.nonProxyHosts";
            proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
        } else if ("socket".equalsIgnoreCase(scheme)) {
            httpProxyOkay = false;
        } else {
            return Proxy.NO_PROXY;
        }

        if (nonProxyHostsKey != null
                && isNonProxyHost(uri.getHost(), System.getProperty(nonProxyHostsKey))) {
            return Proxy.NO_PROXY;
        }

        if (proxy != null) {
            return proxy;
        }

        if (httpProxyOkay) {
            proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
            if (proxy != null) {
                return proxy;
            }
        }

        proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
        if (proxy != null) {
            return proxy;
        }

        return Proxy.NO_PROXY;
    }
 ····
}

主要看selectOneProxy方法,其实就是根据url的scheme不同生成不同的proxy并返回。我们接着看RouteSelector

/**
   * Returns true if there's another set of routes to attempt. Every address has at least one route.
   */
  public boolean hasNext() {
    return hasNextProxy() || !postponedRoutes.isEmpty();
  }
  /** Returns true if there's another proxy to try. */
 //是否还有代理
  private boolean hasNextProxy() {
    return nextProxyIndex < proxies.size();
  }

public Selection next() throws IOException {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    // Compute the next set of routes to attempt.
    List routes = new ArrayList<>();
    while (hasNextProxy()) {
      // Postponed routes are always tried last. For example, if we have 2 proxies and all the
      // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted
      // all the good routes will we attempt the postponed routes.
      Proxy proxy = nextProxy();
      for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
        Route route = new Route(address, proxy, inetSocketAddresses.get(i));
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes.add(route);
        } else {
          routes.add(route);
        }
      }

      if (!routes.isEmpty()) {
        break;
      }
    }

    if (routes.isEmpty()) {
      // We've exhausted all Proxies so fallback to the postponed routes.
      routes.addAll(postponedRoutes);
      postponedRoutes.clear();
    }

    return new Selection(routes);
  }

/** 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;
  }

  /** Prepares the socket addresses to attempt for the current proxy or host. */
  private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    inetSocketAddresses = new ArrayList<>();

    String socketHost;
    int socketPort;
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
      SocketAddress proxyAddress = proxy.address();
      if (!(proxyAddress instanceof InetSocketAddress)) {
        throw new IllegalArgumentException(
            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
      }
      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
      socketHost = getHostString(proxySocketAddress);
      socketPort = proxySocketAddress.getPort();
    }

    if (socketPort < 1 || socketPort > 65535) {
      throw new SocketException("No route to " + socketHost + ":" + socketPort
          + "; port is out of range");
    }

    if (proxy.type() == Proxy.Type.SOCKS) {
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
      eventListener.dnsStart(call, socketHost);

      // Try each address for best behavior in mixed IPv4/IPv6 environments.
      List addresses = address.dns().lookup(socketHost);
      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));
      }
    }
  }

这块是RouteSelector的核心逻辑,外部进行路由选择时会调用next方法,返回一个Selection对象,该对象就是一个可选路由的list。其逻辑如下

  • 1、判断是否还有路由(包括延迟路由)
  • 2、进入while循环,当还存在可尝试的proxy时进行以下逻辑
    • 2.1、通过nextProxy()方法获取下一个proxy
    • 2.2、根据proxy,address,dns解析后得到的inetSocketAddresses生成route
    • 2.3、根据routeDatabase判断生成route是否为延迟路由,并加入相应的list
    • 2.4、若可用路由列表不为空则跳出循环,返回Selection;若可用路由列表为空则将延迟路由列表全部加如可用路由列表,清空延迟路由列表,执行下一轮循环

我们继续看next中调用的nextProxy方法,就是从proxy列表中获取下一个proxy,作为参数传入resetNextInetSocketAddress方法,而resetNextInetSocketAddress逻辑如下

  • 1、对于没有配置代理的情况,会对HTTP服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址
  • 2、对于SOCKS代理,直接以HTTP的服务器的域名以及协议端口创建 连接目标地址
  • 3、对于HTTP代理,则会对HTTP代理服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址

从代码中我们可以看出这里就是okhttp网络请求解析dns的地方List addresses = address.dns().lookup(socketHost);该行就是根据根据host解析出其对应的ip地址。
最后再看一下连接失败的情况

/**
   * Clients should invoke this method when they encounter a connectivity failure on a connection
   * returned by this route selector.
   */
  public void connectFailed(Route failedRoute, IOException failure) {
    if (failedRoute.proxy().type() != Proxy.Type.DIRECT && address.proxySelector() != null) {
      // Tell the proxy selector when we fail to connect on a fresh connection.
      address.proxySelector().connectFailed(
          address.url().uri(), failedRoute.proxy().address(), failure);
    }
    routeDatabase.failed(failedRoute);
  }

当连接失败,外部会调用这个方法,该方法会维护RouteDatabase中的失败路由信息。到这里okhttp的路由选择就讲完了,通过RouteSelector收集、选择路由以及维护失败路由列表,使连接时不会优先使用之前已出过错的路由,节省时间,提高效率。

二、连接:Connection类

上面我们介绍了路由选择策略,下面我们看一下okhttp具体连接相关逻辑。okhttp连接相关逻辑都由Connection负责,Connection是一个接口,有四个抽象方法

Route route(); //返回一个路由
Socket socket();  //返回一个socket
Handshake handshake();  //如果是一个https,则返回一个TLS握手协议
Protocol protocol(); //返回一个协议类型 比如 http1.1 等或者自定义类型 

其实现类为RealConnection,该对象会在需要构建tcp连接时实例化,具体在StreamAllocation中,我们后面会讲到。如果拥有了一个RealConnection就代表了我们已经跟服务器有了一条通信链路,也就说明此时tcp三次握手已经完成。先看下这个类的源码

private final ConnectionPool connectionPool;
  private final Route route;

  // The fields below are initialized by connect() and never reassigned.
  //下面这些字段,通过connect()方法开始初始化,并且绝对不会再次赋值
  /** The low-level TCP socket. */
  private Socket rawSocket; //底层socket
  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  private Socket socket;  //应用层socket
  //握手
  private Handshake handshake;
   //协议
  private Protocol protocol;
   // http2的链接
  private Http2Connection http2Connection;
  //通过source和sink,大家可以猜到是与服务器交互的输入输出流
  private BufferedSource source;
  private BufferedSink sink;

  // The fields below track connection state and are guarded by connectionPool.
  //下面这个字段是 属于表示链接状态的字段,并且有connectPool统一管理
  /** If true, no new streams can be created on this connection. Once true this is always true. */
  //如果noNewStreams被设为true,则noNewStreams一直为true,不会被改变,并且表示这个链接不会再创新的stream流
  public boolean noNewStreams;
  
  //成功的次数
  public int successCount;

  /**
   * The maximum number of concurrent streams that can be carried by this connection. If {@code
   * allocations.size() < allocationLimit} then new streams can be created on this connection.
   */
  //此链接可以承载最大并发流的限制,如果不超过限制,可以随意增加
  public int allocationLimit = 1;
  /** Current streams carried by this connection. */
  public final List> allocations = new ArrayList<>();

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;

简单解释几个变量:

  • 1、noNewStream可以简单理解为它表示该连接不可用。这个值一旦被设为true,则这个conncetion则不会再创建stream。
  • 2、allocationLimit是分配流的数量上限,http2支持一个连接上并发多个流,其他的都只支持一个连接对应一个流。
  • 3、allocations是关联StreamAllocation,它用来统计在一个连接上建立了哪些流,通过StreamAllocation的acquire方法和release方法可以将一个allcation对象添加到链表或者移除链表。

看下其核心方法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 connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    while (true) {
      try {
        if (route.requiresTunnel()) {
           // 如果要求隧道模式,建立通道连接
          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的建立
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;

        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);

        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }

        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(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) {
         // http2,修改每个连接上可承载的流的数量
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

整理下逻辑

  • 1、检查连接是否已经建立,若已经建立,则抛出异常,否则继续。连接是否建立由protocol标示,它表示在整个连接建立、协商过程中选择所有要用到的协议
  • 2、根据ConnectionSpec的集合connnectionspecs构造ConnectionSpecSelector。这边说一下,ConnectionSpec用于描述传输HTTP流量的socket连接的配置。若是https连接,这些配置主要包括协商安全连接时要使用的TLS版本号和密码套间,是否支持TLS扩展等;http连接则包含是否支持明文传输等,用户可以自定义ConnectionSpec集合,但一般使用okhttp默认的
  • 3、若不是https请求,则根据ConnectionSpec进行相关逻辑检测
  • 4、根据请求判断是否需要建立隧道连接,如果建立隧道连接则调用
    connectTunnel(connectTimeout, readTimeout, writeTimeout);
  • 5、如果不是隧道连接则调用connectSocket(connectTimeout, readTimeout);建立普通连接。
  • 6、建立协议
  • 7、若是http2,则修改allocationLimit值

先看普通的非隧道连接的建立connectSocket(connectTimeout, readTimeout),这个方法后tcp连接就会建立,三次握手也会完成。

/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    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 {
      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;
    }
    // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
    try {
      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);
      }
    }
  }

有3种情况需要建立普通连接:无代理(直连),http代理,socks代理。方法逻辑如下

  • 1、创建Socket,设置超时。非SOCKS代理的情况下,通过SocketFactory创建;在SOCKS代理则传入proxy手动new一个出来。
  • 2、基于第一步创建的socket,调用其connectSocket方法,完成三次握手构建tcp连接。最后会调用java.net.Socket类中的connect方法,具体细节有兴趣的同学可以继续深挖。
  • 3、创建用于I/O的source和sink

接着我们着重看下隧道连接的建立。首先先解释下什么是隧道连接,我们这里说的隧道连接是指https ssl隧道协议。现在客户端请求大部分都已经是https请求,我们知道https连接需要建立在tls握手成功的基础上,但是网络连接过程中会存在代理,如果我们想在复用现有的HTTP proxy的传输方式来代理HTTPS流量,那么就会变成浏览器和代理握手跑TLS,代理拿到明文的请求报文,代理和网站握手跑TLS。但是代理没有,也不可能有网站的私钥证书,所以这么做会导致浏览器和代理之间的TLS无法建立,证书校验根本通不过。
HTTP tunnel以及CONNECT报文解决了这个问题,代理服务器不再作为中间人,不再改写浏览器的请求,而是把浏览器和远端服务器之间通信的数据原样透传,这样浏览器就可以直接和远端服务器进行TLS握手并传输加密的数据。


http隧道.png

隧道连接建立的过程如下

  • 1、由于是https请求,代理服务器无法解析header信息,所以需要额外添加明文Header信息,告诉代理使用CONNECT扩展建立与服务器的隧道连接。CONNECT 方法就是一条单行的文本命令,它提供了由冒号分隔的安全原始服务器的主机名和端口号。host:port 后面跟着一个空格和 HTTP 版本字符串,再后面是 CRLF。

CONNECT home.netscape.com:443 HTTP/1.0
User-agent: Mozilla/1.1N

  • 2、服务器收到信令后首先进行身份验证,通过后便与远程主机建立tcp连接,连接成功后代理会返回给客户端HTTP/1.0 200 Connection Established(与普通 HTTP 响应不同,这个响应并不需要包含 Content-Type 首部。此时连接只是对原始字节进行转接,不再是报文的承载者,所以不需要使用内容类型了。)
  • 3、建立成功后,代理将不会解析客户端报文,只会作为一个通道对报文进行盲转发。


    隧道连接建立过程.jpeg

了解了http隧道的原理,我们再来看下okhttp中的实现


/**
   * Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a
   * proxy server can issue an auth challenge and then close the connection.
   */
  private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
      EventListener eventListener) throws IOException {
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
      connectSocket(connectTimeout, readTimeout, call, eventListener);
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

      if (tunnelRequest == null) break; // Tunnel successfully created.

      // The proxy decided to close the connection after an auth challenge. We need to create a new
      // connection, but this time with the auth credentials.
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
      eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
    }
  }
/**
   * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
   * the proxy connection. This may need to be retried if the proxy requires authorization.
   */
  private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
                               HttpUrl url) throws IOException {
    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    while (true) {
      Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
      source.timeout().timeout(readTimeout, MILLISECONDS);
      sink.timeout().timeout(writeTimeout, MILLISECONDS);
      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
      tunnelConnection.finishRequest();
      Response response = tunnelConnection.readResponseHeaders(false)
          .request(tunnelRequest)
          .build();
      // The response body from a CONNECT should be empty, but if it is not then we should consume
      // it before proceeding.
      long contentLength = HttpHeaders.contentLength(response);
      if (contentLength == -1L) {
        contentLength = 0L;
      }
      Source body = tunnelConnection.newFixedLengthSource(contentLength);
      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
      body.close();

      switch (response.code()) {
        case HTTP_OK:
          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
          // that it will almost certainly fail because the proxy has sent unexpected data.
          if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
            throw new IOException("TLS tunnel buffered too many bytes!");
          }
          return null;

        case HTTP_PROXY_AUTH:
          tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
          if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");

          if ("close".equalsIgnoreCase(response.header("Connection"))) {
            return tunnelRequest;
          }
          break;

        default:
          throw new IOException(
              "Unexpected response code for CONNECT: " + response.code());
      }
    }
  }

  /**
   * Returns a request that creates a TLS tunnel via an HTTP proxy. Everything in the tunnel request
   * is sent unencrypted to the proxy server, so tunnels include only the minimum set of headers.
   * This avoids sending potentially sensitive data like HTTP cookies to the proxy unencrypted.
   */
  private Request createTunnelRequest() {
    return new Request.Builder()
        .url(route.address().url())
        .header("Host", Util.hostHeader(route.address().url(), true))
        .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
        .header("User-Agent", Version.userAgent())
        .build();
  }
  • 1、创建tunnelRequest,并建立与proxy的连接
  • 2、创建一个while循环,调用createTunnel方法创建隧道连接,该方法会返回一个Request,当返回Request为null时,表示连接建立成功,跳出循环。
    • 2.1、createTunnel方法首先生成CONNECT头部的文本requestLine
    • 2.2、生成数据读写的管理类HttpCodec,将requestLine写入tunnelRequest头部,并开始发送数据
    • 2.3、当返回的reponse满足要求时return null,否则return tunnelRequest

tcp连接建立成功后,我们继续看建立协议establishProtocol

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
                                 int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        startHttp2(pingIntervalMillis);
        return;
      }

      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

整理下逻辑

  • 1、若不是https请求,首先判断是否为h2_prior_knowledge类型的请求(这种类型的请求属于http2但是需要服务器支持明文http2请求,因此不能使用https)则给相关值赋值并调用startHttp2方法建立http2连接(http2后续会专门分享,此处不展开)并return;若不是h2_prior_knowledge,则给相关值赋值后返回。
  • 2、若是https请求,则调用connectTls进行tls握手,tls握手成功后判断当前连接是否为http2,若是则调用startHttp2方法建立http2连接。

看下connectTls方法

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake();
      // block for session establishment
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

TLS连接是对原始TCP连接(Socket)的一个封装,完成TLS握手、收发过程中的加解密等功能。并最后构建好一个SSLSocket。

  • 1、基于原始的socket构建SSLSocket
  • 2、根据用户传入connectionSpecSelector与生成的SSLSocket取交集获得连接配置connectionSpec
  • 3、判断connectionSpec是否需要tls扩展,若需要则进行tls扩展
  • 4、开始tls握手
  • 5、TLS握手完成之后,获取证书信息,对TLS握手过程中传回来的证书进行验证。
  • 6、在前面选择的ConnectionSpec支持TLS扩展参数时,获取TLS握手过程中顺便完成的协议协商过程所选择的协议。这个过程主要用于HTTP/2的ALPN扩展。
  • 7、基于之前建立的SSLSocket创建I/O用的source,sink。

至此连接建立分析结束。

三、HttpCodec

上面在构建连接时出现了Http1Codec,Http2Codec,这里就简单介绍下这几个类。在okHttp中,HttpCodec是网络读写的管理类,也可以理解为解码器。当tcp连接建立后,信息在连接上传输是以流的形式,所以服务端和客户端都需要负责I/O操作的管理类。它有对应的两个子类,Http1Codec和Http2Codec,分别对应HTTP/1.1以及HTTP/2.0协议。我们看下HttpCodec接口

/** Encodes HTTP requests and decodes HTTP responses. */
public interface HttpCodec {
  /**
   * The timeout to use while discarding a stream of input data. Since this is used for connection
   * reuse, this timeout should be significantly less than the time it takes to establish a new
   * connection.
   */
  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;

  /** Returns an output stream where the request body can be streamed. */
  Sink createRequestBody(Request request, long contentLength);

  /** This should update the HTTP engine's sentRequestMillis field. */
  void writeRequestHeaders(Request request) throws IOException;

  /** Flush the request to the underlying socket. */
  void flushRequest() throws IOException;

  /** Flush the request to the underlying socket and signal no more bytes will be transmitted. */
  void finishRequest() throws IOException;

  /**
   * Parses bytes of a response header from an HTTP transport.
   *
   * @param expectContinue true to return null if this is an intermediate response with a "100"
   *     response code. Otherwise this method never returns null.
   */
  Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;

  /** Returns a stream that reads the response body. */
  ResponseBody openResponseBody(Response response) throws IOException;

  /**
   * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
   * That may happen later by the connection pool thread.
   */
  void cancel();
}

writeRequestHeaders(Request request) :写入请求头
createRequestBody(Request request, long contentLength) :写入请求体
flushRequest() 相当于flush,把请求刷入底层socket
finishRequest() throws IOException : 相当于flush,把请求输入底层socket并不在发出请求
readResponseHeaders(boolean expectContinue) //读取响应头
openResponseBody(Response response) //读取响应体
void cancel() :取消请求

由于HTTP/2和HTTP/1.x在支持流数量以及传输格式上有本质的区别,因此需要Http1Codec,Http2Codec两个来满足不同协议的需求,而volley之所以无法支持http2就是缺少了类似Http2Codec这样针对HTTP/2的解码器。关于Http1Codec具体实现这边就不做展开,关于http2在okhttp中的使用及逻辑,后续我们会专门做一期分享,这边也不展开。

你可能感兴趣的:(okhttp5)