okhttp5

okhttp分享5:ConnectInterceptor(1)

按順序我們現(xiàn)在走到了ConnectInterceptor,該攔截器主要負(fù)責(zé)建立與服務(wù)器的鏈接。先簡單看一下代碼,代碼量不多

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中實現(xiàn)的。我們回憶一下,這個streamAllocation最早是在RetryAndFollowUpInterceptor中實例化的,我們看一下RetryAndFollowUpInterceptor中代碼

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

在看下其構(gòu)造方法

  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
                          EventListener eventListener, Object callStackTrace) {
    this.connectionPool = connectionPool;//連接池
    this.address = address;//地址相關(guān)信息
    this.call = call;//用戶請求
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);//路由選擇策略
    this.callStackTrace = callStackTrace;
  }

其中我們主要關(guān)注routeSelector和connectionPool

一、路由選擇

之前我們說到,okhttp會自動選擇最優(yōu)的路由,并以此建立或復(fù)用連接。連接的建立是要基于路由選擇的,我們先看一下okhttp的路由相關(guān)類

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時作為參數(shù)實例化的。對于簡單的鏈接,這里是服務(wù)器的主機名和端口號。如果是通過代理(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方法,用于后續(xù)連接池調(diào)用

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表示通過代理服務(wù)器的信息proxy及鏈接的目標(biāo)地址Address來描述的路由,連接的目標(biāo)地址inetSocketAddress根據(jù)代理類型的不同而有著不同的含義,這主要是通過不同代理協(xié)議的差異而造成的。對于無需代理的情況,連接的目標(biāo)地址inetSocketAddress中包含HTTP服務(wù)器經(jīng)過DNS域名解析的IP地址以及協(xié)議端口號;對于SOCKET代理其中包含HTTP服務(wù)器的域名及協(xié)議端口號;對于HTTP代理,其中則包含代理服務(wù)器經(jīng)過域名解析的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客戶端與服務(wù)端網(wǎng)絡(luò)通信的過程,可以細(xì)分成很多塊,大體分的話其實就是兩步首先建立連接(tcp/udp),連接建立后基于當(dāng)前連接及請求構(gòu)建信息傳輸流(http/socket),通過流進行io操作,與服務(wù)器通信。做個簡單的比喻,連接就是橋梁,流就是橋上跑的貨車,信息就是貨車裝載的貨物。而tcp連接建立過程所需要的關(guān)鍵元素就是Route,現(xiàn)在借助于域名做負(fù)載均衡也十分常見,所以路由選擇也變得較為復(fù)雜,okhttp中通過RouteSelector對路由信息進行管理,使建立tcp連接時可以使用最優(yōu)的路由。下面我們看下源碼

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

成員變量都是之前提過的內(nèi)容,直接看構(gòu)造器中調(diào)用的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根據(jù)請求uri產(chǎn)生proxy,若產(chǎn)生的proxy為null則使用Proxy.NO_PROXY。
這邊我們注意下proxySelector(),這個是在okhttpclient傳入的代理選擇策略,用戶若不傳入則使用系統(tǒng)默認(rèn)的,我們簡單看下系統(tǒng)默認(rèn)的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方法,其實就是根據(jù)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的核心邏輯,外部進行路由選擇時會調(diào)用next方法,返回一個Selection對象,該對象就是一個可選路由的list。其邏輯如下

  • 1、判斷是否還有路由(包括延遲路由)
  • 2、進入while循環(huán),當(dāng)還存在可嘗試的proxy時進行以下邏輯
    • 2.1、通過nextProxy()方法獲取下一個proxy
    • 2.2、根據(jù)proxy,address,dns解析后得到的inetSocketAddresses生成route
    • 2.3、根據(jù)routeDatabase判斷生成route是否為延遲路由,并加入相應(yīng)的list
    • 2.4、若可用路由列表不為空則跳出循環(huán),返回Selection;若可用路由列表為空則將延遲路由列表全部加如可用路由列表,清空延遲路由列表,執(zhí)行下一輪循環(huán)

我們繼續(xù)看next中調(diào)用的nextProxy方法,就是從proxy列表中獲取下一個proxy,作為參數(shù)傳入resetNextInetSocketAddress方法,而resetNextInetSocketAddress邏輯如下

  • 1、對于沒有配置代理的情況,會對HTTP服務(wù)器的域名進行DNS域名解析,并為每個解析到的IP地址創(chuàng)建 連接的目標(biāo)地址
  • 2、對于SOCKS代理,直接以HTTP的服務(wù)器的域名以及協(xié)議端口創(chuàng)建 連接目標(biāo)地址
  • 3、對于HTTP代理,則會對HTTP代理服務(wù)器的域名進行DNS域名解析,并為每個解析到的IP地址創(chuàng)建 連接的目標(biāo)地址

從代碼中我們可以看出這里就是okhttp網(wǎng)絡(luò)請求解析dns的地方List<InetAddress> addresses = address.dns().lookup(socketHost);該行就是根據(jù)根據(jù)host解析出其對應(yīng)的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);
  }

當(dāng)連接失敗,外部會調(diào)用這個方法,該方法會維護RouteDatabase中的失敗路由信息。到這里okhttp的路由選擇就講完了,通過RouteSelector收集、選擇路由以及維護失敗路由列表,使連接時不會優(yōu)先使用之前已出過錯的路由,節(jié)省時間,提高效率。

二、連接:Connection類

上面我們介紹了路由選擇策略,下面我們看一下okhttp具體連接相關(guān)邏輯。okhttp連接相關(guān)邏輯都由Connection負(fù)責(zé),Connection是一個接口,有四個抽象方法

Route route(); //返回一個路由
Socket socket();  //返回一個socket
Handshake handshake();  //如果是一個https,則返回一個TLS握手協(xié)議
Protocol protocol(); //返回一個協(xié)議類型 比如 http1.1 等或者自定義類型 

其實現(xiàn)類為RealConnection,該對象會在需要構(gòu)建tcp連接時實例化,具體在StreamAllocation中,我們后面會講到。如果擁有了一個RealConnection就代表了我們已經(jīng)跟服務(wù)器有了一條通信鏈路,也就說明此時tcp三次握手已經(jīng)完成。先看下這個類的源碼

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;  //應(yīng)用層socket
  //握手
  private Handshake handshake;
   //協(xié)議
  private Protocol protocol;
   // http2的鏈接
  private Http2Connection http2Connection;
  //通過source和sink,大家可以猜到是與服務(wù)器交互的輸入輸出流
  private BufferedSource source;
  private BufferedSink sink;

  // The fields below track connection state and are guarded by connectionPool.
  //下面這個字段是 屬于表示鏈接狀態(tài)的字段,并且有connectPool統(tǒng)一管理
  /** If true, no new streams can be created on this connection. Once true this is always true. */
  //如果noNewStreams被設(shè)為true,則noNewStreams一直為true,不會被改變,并且表示這個鏈接不會再創(chuàng)新的stream流
  public boolean noNewStreams;
  
  //成功的次數(shù)
  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.
   */
  //此鏈接可以承載最大并發(fā)流的限制,如果不超過限制,可以隨意增加
  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可以簡單理解為它表示該連接不可用。這個值一旦被設(shè)為true,則這個conncetion則不會再創(chuàng)建stream。
  • 2、allocationLimit是分配流的數(shù)量上限,http2支持一個連接上并發(fā)多個流,其他的都只支持一個連接對應(yīng)一個流。
  • 3、allocations是關(guān)聯(lián)StreamAllocation,它用來統(tǒng)計在一個連接上建立了哪些流,通過StreamAllocation的acquire方法和release方法可以將一個allcation對象添加到鏈表或者移除鏈表。

看下其核心方法connect,也就是在這里面構(gòu)建了連接。

  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,修改每個連接上可承載的流的數(shù)量
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

整理下邏輯

  • 1、檢查連接是否已經(jīng)建立,若已經(jīng)建立,則拋出異常,否則繼續(xù)。連接是否建立由protocol標(biāo)示,它表示在整個連接建立、協(xié)商過程中選擇所有要用到的協(xié)議
  • 2、根據(jù)ConnectionSpec的集合connnectionspecs構(gòu)造ConnectionSpecSelector。這邊說一下,ConnectionSpec用于描述傳輸HTTP流量的socket連接的配置。若是https連接,這些配置主要包括協(xié)商安全連接時要使用的TLS版本號和密碼套間,是否支持TLS擴展等;http連接則包含是否支持明文傳輸?shù)龋脩艨梢宰远xConnectionSpec集合,但一般使用okhttp默認(rèn)的
  • 3、若不是https請求,則根據(jù)ConnectionSpec進行相關(guān)邏輯檢測
  • 4、根據(jù)請求判斷是否需要建立隧道連接,如果建立隧道連接則調(diào)用
    connectTunnel(connectTimeout, readTimeout, writeTimeout);
  • 5、如果不是隧道連接則調(diào)用connectSocket(connectTimeout, readTimeout);建立普通連接。
  • 6、建立協(xié)議
  • 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、創(chuàng)建Socket,設(shè)置超時。非SOCKS代理的情況下,通過SocketFactory創(chuàng)建;在SOCKS代理則傳入proxy手動new一個出來。
  • 2、基于第一步創(chuàng)建的socket,調(diào)用其connectSocket方法,完成三次握手構(gòu)建tcp連接。最后會調(diào)用java.net.Socket類中的connect方法,具體細(xì)節(jié)有興趣的同學(xué)可以繼續(xù)深挖。
  • 3、創(chuàng)建用于I/O的source和sink

接著我們著重看下隧道連接的建立。首先先解釋下什么是隧道連接,我們這里說的隧道連接是指https ssl隧道協(xié)議?,F(xiàn)在客戶端請求大部分都已經(jīng)是https請求,我們知道https連接需要建立在tls握手成功的基礎(chǔ)上,但是網(wǎng)絡(luò)連接過程中會存在代理,如果我們想在復(fù)用現(xiàn)有的HTTP proxy的傳輸方式來代理HTTPS流量,那么就會變成瀏覽器和代理握手跑TLS,代理拿到明文的請求報文,代理和網(wǎng)站握手跑TLS。但是代理沒有,也不可能有網(wǎng)站的私鑰證書,所以這么做會導(dǎo)致瀏覽器和代理之間的TLS無法建立,證書校驗根本通不過。
HTTP tunnel以及CONNECT報文解決了這個問題,代理服務(wù)器不再作為中間人,不再改寫瀏覽器的請求,而是把瀏覽器和遠端服務(wù)器之間通信的數(shù)據(jù)原樣透傳,這樣瀏覽器就可以直接和遠端服務(wù)器進行TLS握手并傳輸加密的數(shù)據(jù)。


http隧道.png

隧道連接建立的過程如下

  • 1、由于是https請求,代理服務(wù)器無法解析header信息,所以需要額外添加明文Header信息,告訴代理使用CONNECT擴展建立與服務(wù)器的隧道連接。CONNECT 方法就是一條單行的文本命令,它提供了由冒號分隔的安全原始服務(wù)器的主機名和端口號。host:port 后面跟著一個空格和 HTTP 版本字符串,再后面是 CRLF。

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

  • 2、服務(wù)器收到信令后首先進行身份驗證,通過后便與遠程主機建立tcp連接,連接成功后代理會返回給客戶端HTTP/1.0 200 Connection Established(與普通 HTTP 響應(yīng)不同,這個響應(yīng)并不需要包含 Content-Type 首部。此時連接只是對原始字節(jié)進行轉(zhuǎn)接,不再是報文的承載者,所以不需要使用內(nèi)容類型了。)
  • 3、建立成功后,代理將不會解析客戶端報文,只會作為一個通道對報文進行盲轉(zhuǎn)發(fā)。


    隧道連接建立過程.jpeg

了解了http隧道的原理,我們再來看下okhttp中的實現(xiàn)


/**
   * 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、創(chuàng)建tunnelRequest,并建立與proxy的連接
  • 2、創(chuàng)建一個while循環(huán),調(diào)用createTunnel方法創(chuàng)建隧道連接,該方法會返回一個Request,當(dāng)返回Request為null時,表示連接建立成功,跳出循環(huán)。
    • 2.1、createTunnel方法首先生成CONNECT頭部的文本requestLine
    • 2.2、生成數(shù)據(jù)讀寫的管理類HttpCodec,將requestLine寫入tunnelRequest頭部,并開始發(fā)送數(shù)據(jù)
    • 2.3、當(dāng)返回的reponse滿足要求時return null,否則return tunnelRequest

tcp連接建立成功后,我們繼續(xù)看建立協(xié)議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但是需要服務(wù)器支持明文http2請求,因此不能使用https)則給相關(guān)值賦值并調(diào)用startHttp2方法建立http2連接(http2后續(xù)會專門分享,此處不展開)并return;若不是h2_prior_knowledge,則給相關(guān)值賦值后返回。
  • 2、若是https請求,則調(diào)用connectTls進行tls握手,tls握手成功后判斷當(dāng)前連接是否為http2,若是則調(diào)用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握手、收發(fā)過程中的加解密等功能。并最后構(gòu)建好一個SSLSocket。

  • 1、基于原始的socket構(gòu)建SSLSocket
  • 2、根據(jù)用戶傳入connectionSpecSelector與生成的SSLSocket取交集獲得連接配置connectionSpec
  • 3、判斷connectionSpec是否需要tls擴展,若需要則進行tls擴展
  • 4、開始tls握手
  • 5、TLS握手完成之后,獲取證書信息,對TLS握手過程中傳回來的證書進行驗證。
  • 6、在前面選擇的ConnectionSpec支持TLS擴展參數(shù)時,獲取TLS握手過程中順便完成的協(xié)議協(xié)商過程所選擇的協(xié)議。這個過程主要用于HTTP/2的ALPN擴展。
  • 7、基于之前建立的SSLSocket創(chuàng)建I/O用的source,sink。

至此連接建立分析結(jié)束。

三、HttpCodec

上面在構(gòu)建連接時出現(xiàn)了Http1Codec,Http2Codec,這里就簡單介紹下這幾個類。在okHttp中,HttpCodec是網(wǎng)絡(luò)讀寫的管理類,也可以理解為解碼器。當(dāng)tcp連接建立后,信息在連接上傳輸是以流的形式,所以服務(wù)端和客戶端都需要負(fù)責(zé)I/O操作的管理類。它有對應(yīng)的兩個子類,Http1Codec和Http2Codec,分別對應(yīng)HTTP/1.1以及HTTP/2.0協(xié)議。我們看下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() 相當(dāng)于flush,把請求刷入底層socket
finishRequest() throws IOException : 相當(dāng)于flush,把請求輸入底層socket并不在發(fā)出請求
readResponseHeaders(boolean expectContinue) //讀取響應(yīng)頭
openResponseBody(Response response) //讀取響應(yīng)體
void cancel() :取消請求

由于HTTP/2和HTTP/1.x在支持流數(shù)量以及傳輸格式上有本質(zhì)的區(qū)別,因此需要Http1Codec,Http2Codec兩個來滿足不同協(xié)議的需求,而volley之所以無法支持http2就是缺少了類似Http2Codec這樣針對HTTP/2的解碼器。關(guān)于Http1Codec具體實現(xiàn)這邊就不做展開,關(guān)于http2在okhttp中的使用及邏輯,后續(xù)我們會專門做一期分享,這邊也不展開。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容