OkHttp源碼解析 (三)——代理和路由

目錄

一、前言

初看OkHttp源碼,由于對Address、Route、Proxy、ProxySelector、RouteSelector等理解不夠,讀源碼非常吃力,看了幾遍依然對于尋找復用連接、創建連接、連接服務器、連接代理服務器、創建隧道連接等邏輯似懂非懂,本篇決定梳理一遍相關的概念及基本原理。

二、基礎知識

(1)HTTP網絡協議:

● HTTP/1.1(HTTPS)
● HTTP/2
● SPDY

(2)HTTP請求流程

一個http請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定端口,則用默認端口,http請求用80;
3、創建socket,根據ip和端口連接服務器(socket內部會完成3次TCP握手);
4、socket成功連接后,發送http報文數據。

一個https請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定端口,則用默認端口,https請求用443;
3、創建socket,根據ip和端口連接服務器(socket內部會完成3次TCP握手);
4、socket成功連接后進行TLS握手,可通過java標準款提供的SSLSocket完成;
5、握手成功后,發送https報文數據。

(3)代理相關說明

1、分類
● HTTP代理:普通代理、隧道代理
● SOCKS代理:SOCKS4、SOCKS5

2、HTTP代理分類及說明
普通代理
HTTP/1.1 協議的第一部分。其代理過程為:
● client 請求 proxy
● proxy 解析請求獲取 origin server 地址
● proxy 向 origin server 轉發請求
● proxy 接收 origin server 的響應
● proxy 向 client 轉發響應
其中proxy獲取目的服務器地址的標準方法是解析 request line 里的 request-URL。因為proxy需要解析報文,因此普通代理無法適用于https,因為報文都是加密的。

隧道代理
通過 Web 代理服務器用隧道方式傳輸基于 TCP 的協議。
請求包括兩個階段,一是連接(隧道)建立階段,二是數據通信(請求響應)階段,數據通信是基于 TCP packet ,代理服務器不會對請求及響應的報文作任何的處理,都是原封不動的轉發,因此可以代理 HTTPS請求和響應。
代理過程為:
● client 向 proxy 發送 CONNET 請求(包含了 origin server 的地址)
● proxy 與 origin server 建立 TCP 連接
● proxy 向 client 發送響應
● client 向 proxy 發送請求,proxy 原封不動向 origin server 轉發請求,請求數據不做任何封裝,為原生 TCP packet.

3、SOCKS代理分類及說明
● SOCKS4:只支持TCP協議(即傳輸控制協議)
● SOCKS5: 既支持TCP協議又支持UDP協議(即用戶數據包協議),還支持各種身份驗證機制、服務器端域名解析等。
SOCK4能做到的SOCKS5都可得到,但反過來卻不行,比如我們常用的聊天工具QQ在使用代理時就要求用SOCKS5代理,因為它需要使用UDP協議來傳輸數據。

三、OkHttp路由

有了上面的基礎知識,下面分析結合源碼分析OkHttp路由相關的邏輯。OkHttp用Address來描述與目標服務器建立連接的配置信息,但請求輸入的可能是域名,一個域名可能對于多個ip,真正建立連接是其中一個ip,另外,如果設置了代理,客戶端是與代理服務器建立直接連接,而不是目標服務器,代理又可能是域名,可能對應多個ip。因此,這里用Route來描述最終選擇的路由,即客戶端與哪個ip建立連接,是代理還是直連。下面對比下Address及Route的屬性,及路由選擇器RouteSelector。

(1)Address解析

描述與目標服務器建立連接所需要的配置信息,包括目標主機名、端口、dns,SocketFactory,如果是https請求,包括TLS相關的SSLSocketFactory 、HostnameVerifier 、CertificatePinner,代理服務器信息Proxy 、ProxySelector 。

public final class Address {
  final HttpUrl url; //與目標服務器連接的配置,包括協議、主機名、端口等
  final Dns dns; //dns解析器
  final SocketFactory socketFactory; //原始Socket創建工廠
  final Authenticator proxyAuthenticator;
  final List<Protocol> protocols;
  final List<ConnectionSpec> connectionSpecs;
  final ProxySelector proxySelector; //代理選擇器,默認用系統的
  final @Nullable Proxy proxy;//設置的代理,可為空
  final @Nullable SSLSocketFactory sslSocketFactory; //SSLSocket創建工廠
  final @Nullable HostnameVerifier hostnameVerifier; //用于https證書檢驗
  final @Nullable CertificatePinner certificatePinner;//用于https證書檢驗

  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) {
}

(2)Route解析

Route提供了真正連接服務器所需要的動態信息,明確需要連接的服務器IP地址及代理服務器,一個Address可能會有很多個路由Route供選擇(一個DNS對應對個IP)。

public final class 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;
  }

(3)RouteSelector解析

Address和Route都是數據對象,沒有提供操作方法,OkHttp另外定義了RouteSelector來完成選擇的路由的操作。

public final class RouteSelector {
  private final Address address;
  //用于記錄連接失敗的路由的黑名單,如果在黑名單,不用嘗試
  private final RouteDatabase routeDatabase;
  private final Call call;
  private final EventListener eventListener;
  private List<Proxy> proxies = Collections.emptyList(); //代理列表
  private int nextProxyIndex;//下一個嘗試的代理地址

  private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();  //sockect地址列表
  private final List<Route> postponedRoutes = new ArrayList<>();//失敗的路由列表

  public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
      EventListener eventListener) {
    ...
    resetNextProxy(address.url(), address.proxy());
  }

 /*
  * 獲取下個需要嘗試的Route集
  */
 public Selection next() throws IOException {}
 
 /*
  * 記錄失敗路由
  */
 public void connectFailed(Route failedRoute, IOException failure) {}

  /*
   * 讀取代理服務器信息
   */
 private void resetNextProxy(HttpUrl url, Proxy proxy) {}

 /* 
  * 準備需要嘗試的socket地址(目標服務器或者代理服務器)
  private void resetNextInetSocketAddress(Proxy proxy) throws IOException {}
}

1、讀取代理配置信息:resetNextProxy()

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

讀取代理配置:
● 如果有指定代理(不讀取系統配置,在OkHttpClient實例中指定),則只用1個該指定代理;
● 如果沒有指定,則讀取系統配置的,可能有多個。

2、獲取需要嘗試的socket地址(目標服務器或者代理服務器):resetNextInetSocketAddress()

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

結合Address的host和代理,解析要嘗試的套接字地址(ip+端口)列表:
● 直連或者SOCK代理, 則用目標服務器的主機名和端口,如果是HTTP代理,則用代理服務器的主機名和端口;
● 如果是SOCK代理,根據目標服務器主機名和端口號創建未解析的套接字地址,列表只有1個地址;
● 如果是直連或HTTP代理,先DNS解析,得到InetAddress列表(沒有端口),再創建InetSocketAddress列表(帶上端口),InetSocketAddress與InetAddress的區別是前者帶端口信息。

3、獲取路由列表:next()

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

  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 true if there's another proxy to try. */
  private boolean hasNextProxy() {
    return nextProxyIndex < proxies.size();
  }

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

選擇路由的流程解析:
● 遍歷每個代理對象,可能多個,直連的代理對象為Proxy.DIRECT(實際是沒有中間代理的);
● 對每個代理獲取套接字地址列表;
● 遍歷地址列表,創建Route,判斷Route如果在路由黑名單中,則添加到失敗路由列表,不在黑名單中則添加到待返回的Route列表;
● 如果最后待返回的Route列表為空,即可能所有路由都在黑名單中,實在沒有新路由了,則將失敗的路由集合返回;
● 傳入Route列表創建Selection對象,對象比較簡單,就是一個目標路由集合,及讀取方法。

(4)RouteDatabase 解析

為了避免不必要的嘗試,OkHttp會把連接失敗的路由加入到黑名單中,由RouteDatabase管理,該類比較簡單,就是一個失敗路由集合。

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

(5)選擇路由的流程分析

1、創建Address
Address的創建在RetryAndFollowUpInteceptor里,每次請求會聲明一個新的Address及StreamAllocation對象,而StreamAllocation使用Address創建RouteSelector對象,在連接時RouteSelector確定請求的路由。

public final class RetryAndFollowUpInterceptor implements Interceptor {
 @Override public Response intercept(Chain chain) throws IOException {
    ...
    //原始請求
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    ...
    while (true) {
             ...
             //原始請求返回
             response = realChain.proceed(request, streamAllocation, null, null);
             ...
             //創建重定向請求
             Request followUp = followUpRequest(response, streamAllocation.route());
             ... 
             //重定向StreamAllocation
             streamAllocation = new StreamAllocation(client.connectionPool(),createAddress(followUp.url()), call, 
             eventListener, callStackTrace);
    }
}

private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
  }

每個Requst都會構造一個Address對象,構造好了Address對象只是有了與服務器連接的配置信息,但沒有確定最終服務器的ip,也沒有確定連接的路由。

2、創建RouteSelector
在StreamAllocation聲明的同時會聲明路由選擇器RouteSelector,為一次請求尋找路由。

public final class StreamAllocation {
  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
      EventListener eventListener, Object callStackTrace) {
      ...
    //每個StreamAllocation會聲明一個RouteSelector
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
  }
}

3、選擇可用的路由Route

public final class StreamAllocation {
   private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
          ... 
          //根據address判斷ConnectionPool中是否有可重用的連接
          Internal.instance.get(connectionPool, address, this, null);
          ...
          //ConnectionPool沒有可用的,根據Address和proxy獲取路由集合
          if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
                  newRouteSelection = true;
                   routeSelection = routeSelector.next();
          }
      
          synchronized (connectionPool) {
                 if (canceled) throw new IOException("Canceled");
                 if (newRouteSelection) {
                       // Now that we have a set of IP addresses, make another attempt at getting a connection from
                      // the pool. This could match due to connection coalescing.
                      //遍歷每個路由(IP地址),根據Route判斷線程池是否有符合重用的連接
                      List<Route> routes = routeSelection.getAll();
                      for (int i = 0, size = routes.size(); i < size; i++) {
                      Route route = routes.get(i);
                      Internal.instance.get(connectionPool, address, this, route);
                      if (connection != null) {
                            foundPooledConnection = true;
                            result = connection;
                            this.route = route;
                             break;
                       }
                }
          }
         //線程池沒有匹配的,從集合中選擇一個路由
         if (!foundPooledConnection) {
             if (selectedRoute == null) {
                 selectedRoute = routeSelection.next();
         }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        // 根據剛選擇的路由創建新連接RealConnection,并分配給當前流
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false);
      }
    }
   }
}

四、實例分析

下面在測試過程跟蹤實例對象來理解,分別測試直連和HTTP代理HTTP2請求路由的選擇過程:
● 直連請求流程
● HTTP代理HTTPS流程
請求url:http://www.lxweimin.com/p/63ba15d8877a

(1)直連請求

1、構造address對象

構造address

scheme:https
host:www.lxweimin.com
端口:默認443
proxy:null

2、讀取代理配置:resetNextProxy

讀取proxy

沒有設置代理服務器,返回的代理對象為DIRECT。

3、解析目標服務器套接字地址:resetNextInetSocketAddress

解析InetSocketAddress

域名:www.lxweimin.com
IP地址:8個
端口:默認443

4、選擇Route創建RealConnection

創建RealConnection

選中了路由列表中第一個,IP為183.240.216.228

5、確定協議

確定協議

通過SSLSockect完成ssl握手后,判斷協議版本,獲得的協議是h2,可以看出簡書用了HTTP/2.0.

(2)HTTP代理

測試方法:
● 在PC端打開Charles,設置端口,如何設置代理,網上有教程,比較簡單;
● 手機打開WIFI,選擇連接的WIFI修改網絡,在高級選項中設置中指定了代理服務器,ip為PC的ip,端口是Charles剛設置的端口;
● OkHttpClient不指定代理,發起請求。

1、構造address對象

構造address

address對象與直連一樣。
scheme:https
host:www.lxweimin.com
端口:默認443
proxy:null

2、讀取代理配置:resetNextProxy

讀取proxy

可以看出通過默認的代理選擇器讀取到一個代理地址。

3、解析目標服務器套接字地址:resetNextInetSocketAddress

解析InetSocketAddress

可以看到目標地址就是代理服務器地址,只有1個,可以HTTP代理時客戶端與代理服務器建立socket連接。

4、選擇Route創建RealConnection

創建RealConnection

創建與代理服務器的連接對象。

5、創建隧道
由于是代理https請求,需要用到隧道代理。

創建隧道

隧道請求頭部

隧道請求內容

從圖可以看出,建立隧道其實是發送CONNECT請求,header包括字段Proxy-Connection,目標主機名,請求內容類似:

CONNECT www.lxweimin.com:443 HTTP/1.1

6、確定協議,SSL握手

ssl握手

隧道代理,客戶端需要與代理服務器建立安全連接。

五、總結

1、代理可分為HTTP代理和SOCK代理;
2、HTTP代理又分為普通代理和隧道代理;普通代理適合明文傳輸,即http請求;隧道代理僅轉發TCP包,適合加密傳輸,即https/http2;
3、SOCK代理又分為SOCK4和SOCK5,區別是后者支持UDP傳輸,適合代理聊天工具如QQ;
4、沒有設置代理(OkHttpClient沒有指定同時系統也沒有設置),客戶端直接與目標服務器建立TCP連接;
5、設置了代理,代理http請求時,客戶端與代理服務器建立TCP連接,如果代理服務器是域名,則解釋代理服務器域名,而目標服務器的域名由代理服務器解析;
6、設置了代理,代理https/http2請求時,客戶端與代理服務器建立TCP連接,發送CONNECT請求與代理服務器建立隧道,并進行SSL握手,代理服務器不解析數據,僅轉發TCP數據包。

參考

如何正確使用 HTTP proxy
OkHttp3中的代理與路由
HTTP 代理原理及實現(一)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容