okhttp5

作者: 天马呵呵拳 | 来源:发表于2019-08-12 18:58 被阅读0次

    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<Protocol> protocols;
      final List<ConnectionSpec> 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<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;
      }
    

    可以看出这是一个地址封装类,是在RetryAndFollowUpInterceptor实例化StreamAllocation时作为参数实例化的。对于简单的链接,这里是服务器的主机名和端口号。如果是通过代理(Proxy)的链接,则包含代理信息(Proxy)。如果是安全链接,则还包括SSL socket Factory、hostname验证器,证书等。
    注意下这个类注释的最后一句<p>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<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 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<Proxy> proxies = Collections.emptyList();
      private int nextProxyIndex;
    
      /* State for negotiating the next socket address to use. */
      private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
    
      /* State for negotiating failed routes */
      private final List<Route> 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<Proxy> 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<Proxy> 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<Route> 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<InetAddress> 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<InetAddress> 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<Reference<StreamAllocation>> 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<ConnectionSpec> 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

          本文链接:https://www.haomeiwen.com/subject/rjxnqctx.html