【Android】OkHttp 源码剖析系列(五)——路由选择与代理机制

本源码剖析系列基于 OkHttp 3.14

文章目录:

【Android】OkHttp 源码剖析系列(一)——请求的发起及拦截器机制概述

【Android】OkHttp 源码剖析系列(二)——拦截器大体流程分析

【Android】OkHttp 源码剖析系列(三)——缓存机制

【Android】OkHttp 源码剖析系列(四)——连接的建立概述

【Android】OkHttp 源码剖析系列(五)——路由选择与代理机制

【Android】OkHttp 源码剖析系列(六)——连接复用机制及连接的建立

【Android】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 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 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 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 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 routes;
    private int nextRouteIndex = 0;
    Selection(List 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 getAll() {
        return new ArrayList<>(routes);
    }
}

它的实现很简单,内部维护了一个路由列表。之后,寻找连接时就可以根据这个 Selection 来获取具体的 Route,并建立 TCP 连接了。

参考资料

OkHttp3中的代理与路由

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注

%d 博主赞过: