美文网首页
OkHttp 源码剖析系列(五)——路由选择机制

OkHttp 源码剖析系列(五)——路由选择机制

作者: N0tExpectErr0r | 来源:发表于2019-08-09 12:32 被阅读0次

    系列索引

    本系列文章基于 OkHttp3.14

    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中的代理与路由

    相关文章

      网友评论

          本文标题:OkHttp 源码剖析系列(五)——路由选择机制

          本文链接:https://www.haomeiwen.com/subject/gfncjctx.html