你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發
我的個人部落格:blog.N0tExpectErr0r.cn
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
是一個負責負責管理路由資訊,並輔助選擇路由的類。它主要有三個職責:
- 收集可用的路由
- 選擇可用的路由
- 維護連線失敗路由資訊
下面我們對它的三個職責的實現分別進行介紹。
代理的收集
代理的收集過程在 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
中。其中代理型別分別有 DIRECT
、SOCKS
、HTTP
三種。
對於不同的代理型別,它分別有如下的處理:
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 連線了。