OkHttp 原始碼剖析系列(五)——代理路由選擇

N0tExpectErr0r發表於2020-01-03

你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發

我的個人部落格:blog.N0tExpectErr0r.cn

OkHttp 原始碼剖析系列文章目錄:

OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述

OkHttp 原始碼剖析系列(二)——攔截器整體流程分析

OkHttp 原始碼剖析系列(三)——快取機制

OkHttp 原始碼剖析系列(四)——連線建立概述

OkHttp 原始碼剖析系列(五)——代理路由選擇

OkHttp 原始碼剖析系列(六)——連線複用機制及連線的建立

OkHttp 原始碼剖析系列(七)——請求的發起及響應的讀取

路由選擇

注:這裡的路由選擇並不是指網路層的路由選擇,而是指經過代理髮送資料路徑的選擇

當我們第一次嘗試從連線池獲取連線獲取不到時,若檢查發現代理路由選擇器中沒有可供選擇的路徑,首先會進行一次路由選擇的過程,因為 HTTP 請求的過程中,需要先找到一個可用的代理路由,再根據代理協議規則與目標建立 TCP 連線。

Route

我們先了解一下 OkHttp 中的 Route 類:

public final class Route {
    final Address address;
    final Proxy proxy;
    final InetSocketAddress inetSocketAddress;
    // ...
}
複製程式碼

它是一個用於描述一條路由的類,主要通過了代理伺服器資訊 proxy、連線目標地址 InetSocketAddress 來描述一條路由。由於代理協議不同,這裡 InetSocketAddress 會有不同的含義:

  • 沒有代理的情況下它包含的資訊是經過了 DNS 解析的 IP 以及協議的埠號
  • SOCKS 代理的情況下,它包含了 HTTP 伺服器的域名和協議埠號
  • HTTP 代理的情況下,它包含了代理伺服器經過了 DNS 解析的 IP 地址及埠號

Proxy

接著我們瞭解一下 Proxy 類,它是由 Java 原生提供的:

public class Proxy {
    public enum Type {
        // 表示不使用代理
        DIRECT,
        // HTTP代理
        HTTP,
        // SOCKS代理
        SOCKS
    };
    private Type type;
    private SocketAddress sa;
    // ...
}
複製程式碼

它是一個用於描述代理伺服器的類,主要包含了代理協議的型別以及代理伺服器對應的 SocketAddress 類,有以下三種型別:

  • DIRECT:不使用代理
  • HTTP:HTTP 代理
  • SOCKS:SOCKS 代理

RouteSelector

在程式碼中是通過 RouteSelector.next 方法進行的路由選擇的過程,RouteSelecter 是一個負責負責管理路由資訊,並輔助選擇路由的類。它主要有三個職責:

  1. 收集可用的路由
  2. 選擇可用的路由
  3. 維護連線失敗路由資訊

下面我們對它的三個職責的實現分別進行介紹。

代理的收集

代理的收集過程在 RouteSelector 的建構函式中實現,RouteSelector 在建立 ExchangeFinder 時建立:

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) {
        // 若使用者有設定代理,使用使用者設定的代理
        proxies = Collections.singletonList(proxy);
    } else {
        // 藉助ProxySelector獲取代理列表
        List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
        proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
                ? Util.immutableList(proxiesOrNull)
                : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
}
複製程式碼

可以看到,它首先檢查了一下我們的 address 中有沒有使用者設定的代理(通過 OkHttpClient 傳入),若有使用者設定的代理,則直接使用使用者設定的代理。

若使用者沒有設定的代理,則嘗試使用 ProxySelector.select 方法來獲取代理列表。這裡的 ProxySelector 也可以通過 OkHttpClient 進行設定,預設情況下會使用系統預設的 ProxySelector 來獲取系統配置中的代理列表。

選擇可用路由

在代理選擇成功之後,會進行可用路由的選擇工作,我們可以看到 RouteSelector.next 方法:

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()) {
        // 優先採用正常的路由
        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()) {
        // 若找不到正常的路由,則只能採用連線失敗的路由
        routes.addAll(postponedRoutes);
        postponedRoutes.clear();
    }
    return new Selection(routes);
}
複製程式碼

可以看到,上面的步驟主要是一個核心思想——優先採用普通的路由,如果實在找不到普通的路由,再去採用連線失敗的路由

我們可以先看到 nextProxy 方法做了什麼:

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;
}
複製程式碼

它主要就是在之前收集的代理列表中獲取下一個代理的資訊,並且呼叫 resetNextInetSocketAddress 方法根據代理協議獲取對應的 Address 相關資訊填入 inetSocketAddresses 中。

我們看到 resetNextInetSocketAddress 的實現:

/**
 * Prepares the socket addresses to attempt for the current proxy or host.
 */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    inetSocketAddresses = new ArrayList<>();
    String socketHost;
    int socketPort;
    // 若是DIRECT及SOCKS代理,則向原目標的host和port進行請求
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
        socketHost = address.url().host();
        socketPort = address.url().port();
    } else {
    	// 若是HTTP代理,通過代理的地址請求代理伺服器的host
        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) {
    	// 代理型別為SOCKS則直接填入原目標的host和port(因為不需要DNS解析)
        inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
    	// HTTP和DIRECT代理,進行DNS解析後填入dns解析後的ip地址和埠
        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));
        }
    }
}
複製程式碼

上面主要是一些對不同代理的型別的處理,最後將解析後的地址填入了 inetSocketAddresses 中。其中代理型別分別有 DIRECTSOCKSHTTP 三種。

對於不同的代理型別,它分別有如下的處理:

  • DIRECT:經過 DNS 對目標伺服器的地址進行解析,之後將解析後的 IP 地址及埠號填入
  • SOCKS:直接填入代理伺服器的域名及埠號
  • HTTP:首先通過 DNS 對代理伺服器地址進行解析,將解析後的 IP 地址及埠號填入

之後,它根據剛剛的 inetSocketAddress 構建出了對應的 Route 物件,然後呼叫了 routeDatabase.shouldPostpone(route) 判斷它是否是連線失敗的路由。若不是則直接返回,否則只有所有正常路由耗盡的情況下才會採用它。

維護連線失敗的路由資訊

OkHttp 採用了 RouteDatabase 類來維護連線失敗的路由資訊,可以看到它的實現:

final class RouteDatabase {
    private final Set<Route> failedRoutes = new LinkedHashSet<>();
   
    public synchronized void failed(Route failedRoute) {
        failedRoutes.add(failedRoute);
    }

    public synchronized void connected(Route route) {
        failedRoutes.remove(route);
    }

    public synchronized boolean shouldPostpone(Route route) {
        return failedRoutes.contains(route);
    }
}
複製程式碼

可以看到,它維護了一個連線失敗的路由 Set,如果連線失敗則會呼叫它的 failed 方法將失敗路由儲存進佇列,如果連線成功則會呼叫它的 connected 方法將這條路由從失敗路由中移除。可以通過 shouldPostpone 方法判斷一個路由是否是連線失敗的。

返回路由資訊

最後通過 RouteSelector.Selection 這個類返回了我們所選擇的路由的資訊。它的定義如下:

public static final class Selection {
    private final List<Route> routes;
    private int nextRouteIndex = 0;
    Selection(List<Route> routes) {
        this.routes = routes;
    }
    public boolean hasNext() {
        return nextRouteIndex < routes.size();
    }
    public Route next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return routes.get(nextRouteIndex++);
    }
    public List<Route> getAll() {
        return new ArrayList<>(routes);
    }
}
複製程式碼

它的實現很簡單,內部維護了一個路由列表。之後,尋找連線時就可以根據這個 Selection 來獲取具體的 Route,並建立 TCP 連線了。

參考資料

OkHttp3中的代理與路由

相關文章