OkHttp 源码剖析系列(五)——路由选择机制
系列索引
本系列文章基于 OkHttp3.14
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 连接了。