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

系列索引

本系列文章基于 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中的代理与路由

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349