okhttp--RouteSelector

1,191 阅读10分钟

okhttp中涉及到的网络基础知识

HTTP1.0与HTTP2.0的区别

分析RouteSelector时, 由于网络知识积累的不够, 分析起来比较吃力, 对网络知识进行总结之后再来阅读RouteSelector源码流程, 如果遇到吃力的地方, 继续对 okhttp中涉及到的网络基础知识 进行完善补充.

1. Transmitter {
   1. 在RealCall中进行初始化, 一个Request对应一个Transmitter.
   2. 一个Transmitter对应一个RealConnectionPool.
   3. 一个Transmitter对应一个RealConnection.
}
2. RealConnection {
    1. 一个Request对应一个RealConnection.
    2. 一个RealConnection可以对应多个Request.
    3. 一个RealConnection对应一个Route.
    4. 结合Transmitter也就是多个Transmitter可以持有同一个RealConnection.
}
3. RealConnectionPool {
    1. 提供RealConnection
}
4. RouteSelector {
    1. 提供Selection
}
5. RouteSelector.Selection {
    1. 持有Route的集合
}
6. Route {
    1. 一个Route对应一个IP地址与一个Proxy
}

一、RouteSelector初始化

ExchangeFinder(Transmitter transmitter, RealConnectionPool connectionPool,
      Address address, Call call, EventListener eventListener) {
    this.transmitter = transmitter;
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, connectionPool.routeDatabase, call, eventListener);
}
  • RouteSelector涉及到四个参数: Address, RealConnectionPool.routeDatabase, Call, EventListener;
1.1 Address参数
private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }
    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
}
  • Address在Transmitter中被创建, 从代码上来看, 一个url对应一个address, 一个address持有了请求需要的主机名, 域名, dns, 端口号, 协议等, 到这里先暂时把一个Address理解成一个容器.
1.2 RouteSelector
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());
}
  • RouteSelector初始化时通过resetNextProxy对代理服务器进行操作.
1.3 RouteSelector.resetNextProxy
private void resetNextProxy(HttpUrl url, Proxy proxy) {
    if (proxy != null) {
        // If the user specifies a proxy, try that and only that.
        proxies = Collections.singletonList(proxy);
    } else {
        // Try each of the ProxySelector choices until one connection succeeds.
        List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
        proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
            ? Util.immutableList(proxiesOrNull)
            : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
}
  • 1、如果proxy不为空, 也就是自定义过代理服务器, 则使用自定义的代理服务器. 如果没有自定义服务器, 则使用默认的代理服务器
  • 2、关于ProxySelector在OkHttpClient中进行初始化
  • 3、ProxySelector通过传入的uri获取对应的代理服务器
  • 4、这里有一个很大的疑问啊,
1.3 ProxySelector

ProxySelector代理选择器, 通过传入的url获取对应的代理服务器.

public Builder() {
    proxySelector = ProxySelector.getDefault();
    if (proxySelector == null) {
        proxySelector = new NullProxySelector();
    }
}
  • 通过ProxySelector.getDefault()获取系统默认的ProxySelector, 如果获取失败, 则返回NullProxySelector
private static ProxySelector theProxySelector;
static {
    try {
        Class<?> c = Class.forName("sun.net.spi.DefaultProxySelector");
        if (c != null && ProxySelector.class.isAssignableFrom(c)) {
            theProxySelector = (ProxySelector) c.newInstance();
        }
    } catch (Exception e) {
        theProxySelector = null;
    }
}
  • 截止到目前其实还是不知道ProxySelector, RouteSelector, Proxy, Route到底用什么用. 接下来通过对ExchangeFinder.find的分析来尝试了解这几个类的作用

二、对ExchangeFinder.find

ExchangeFinder.find
---ExchangeFinder.findHealthyConnection
------ExchangeFinder.findConnection
---RealConnection.newCodec
2.1 ExchangeFinder.find
public ExchangeCodec find(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    try {
        RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
        return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
        trackFailure();
        throw e;
    } catch (IOException e) {
        trackFailure();
        throw new RouteException(e);
    }
}
2.2 ExchangeFinder.findHealthyConnection
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
        // 1.获取连接
        RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
            pingIntervalMillis, connectionRetryEnabled);
        // If this is a brand new connection, we can skip the extensive health checks.
        synchronized (connectionPool) {
            // 2.如果是新创建的连接, 不需要对连接进行校验 
            if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
                return candidate;
            }
        }
        // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
        // isn't, take it out of the pool and start again.
        // 3.如果是从连接池中获取的连接, 需要对该连接进行校验, 判断是否可用
        if (!candidate.isHealthy(doExtensiveHealthChecks)) {
            candidate.noNewExchanges();
            continue;
        }
        return candidate;
    }
}
  • 1、获取连接RealConnection
  • 2、如果该连接不是从连接池中获取,而是通过new的方式新创建的, 对于新创建的连接, 会与服务器建立通信, 建立成功之后才会返回该连接, 所以不需要进行连接可用的校验
  • 3、对于从来连接池中获取的复用的连接, 此时还需要对该连接进行校验, 校验通过才能使用.
2.3 ExchangeFinder.findConnection
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        hasStreamFailure = false; // This is a fresh attempt.
        // Attempt to use an already-allocated connection. We need to be careful here because our
        // already-allocated connection may have been restricted from creating new exchanges.
        releasedConnection = transmitter.connection;
        toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
            ? transmitter.releaseConnectionNoEvents()
            : null;
        //1. RealCall初始化中对transmitter进行初始化, connection默认为null.
        //2. 如果成功获取了连接, 则直接使用该连接.
        if (transmitter.connection != null) {
            // We had an already-allocated connection and it's good.
            result = transmitter.connection;
            releasedConnection = null;
        }
        if (result == null) {
            // transmitter没有与RealConnection进行过绑定, 则尝试从连接池中获取可用连接.
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if (nextRouteToTry != null) {
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
                selectedRoute = transmitter.connection.route();
            }
        }
    }
    closeQuietly(toClose);

    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
        // If we found an already-allocated or pooled connection, we're done.
        return result;
    }
    // 假设从当前连接池中没有获取到可用的连接, 继续往下执行.
    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        // 获取路由选择器. 
        // 这里涉及到代理服务器, 主机名与IP地址的关系的知识点
        routeSelection = routeSelector.next();
    }
    List<Route> routes = null;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        if (newRouteSelection) {
            // Now that we have a set of IP addresses, make another attempt at getting a connection from
            // the pool. This could match due to connection coalescing.
            routes = routeSelection.getAll();
            // 因为到这里, 已经切换到下一个代理服务器了, 所以此时再次尝试获取可复用的连接RealConnection
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            }
        }
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            // Create a connection and assign it to this allocation immediately. This makes it possible
            // for an asynchronous cancel() to interrupt the handshake we're about to do.
            // 如果没有获取到可复用的连接RealConnection, 重新构建一个连接
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
        }
    }
    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }
    ...
    synchronized (connectionPool) {
        ...
        // 将新建的连接RealConnection添加到连接池中
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
        ...
    }
    ...
    return result;
}
  • 大致总结一下获取连接的过程:
  • 1、尝试从连接池中获取可复用的连接
  • 2、如果当前连接池中没有找到可复用的连接, 切换到下一个代理服务器(如果是直连, 就没有代理服务器这一说, 不需要切换), 然后获取下一个代理服务器对应的IP地址的集合, 再次尝试获取可复用的连接.
  • 3、如果没有找到, 则重新创建连接.
  • 4、连接RealConnection创建完成之后, 与服务器建立通信.
  • 5、如果建立成功, 将该连接添加到连接池中.
2.4 从连接池中获取可用连接RealConnectionPool.transmitterAcquirePooledConnection
boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
      @Nullable List<Route> routes, boolean requireMultiplexed) {
    assert (Thread.holdsLock(this));
    // 1.遍历连接池, 获取可复用的连接
    for (RealConnection connection : connections) {
        // 2.如果这是一个HTTP/2连接,则返回true。这样的连接可以同时用于多个HTTP请求。
        if (requireMultiplexed && !connection.isMultiplexed()) continue;
        // 3.判断连接是否可以复用
        if (!connection.isEligible(address, routes)) continue;
        // 4.如果连接可复用, 将Transmitter与RealConnection进行绑定.
        transmitter.acquireConnectionNoEvents(connection);
        return true;
    }
    return false;
}
  • 这里涉及到HTTP1.x与HTTP2.x的知识:
2.5 判断连接是否可复用RealConnection.isEligible
boolean isEligible(Address address, @Nullable List<Route> routes) {
    // 1. 这里涉及到HTTP2.x的一个知识, HTTP2.0支持多路复用, 同一个连接可以并发处理多个请求.
    //    每一个请求Request对应一个Transmitter, 如果一个RealConnection上的Transmitter数量超过
    //    设定的负载, 则认为该连接已处于饱和状态不可再复用.
    if (transmitters.size() >= allocationLimit || noNewExchanges) return false;
    // 2. 判断是否满足连接可复用的条件
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    // 3. 如果两个请求的主机名一致, 连接可复用.
    if (address.url().host().equals(this.route().address().url().host())) {
        return true; // This connection is a perfect match.
    }
    // 执行到这里, 说明request与当前RealConnection对应的主机名不一致, 但是主机名不一致
    // 并不代表不是同一个连接, 这里涉及到了IP与域名的关系. 同一个IP可以对应多个主机名.
    // 1. 主机名不同的情况下, 如果当前连接不是HTTP2.0协议, 则无法进行连接复用
    if (http2Connection == null) return false;
    // 2. 主机名不同的情况下, 直连并且IP地址必须相同.
    // 这里的判断要求必须是直连并且IP地址必须相同, 这里的原因是因为如果采用了代理服务器, 
    // 代理服务器是无法告诉我们源服务器的IP地址.
    if (routes == null || !routeMatchesAny(routes)) return false
    // 3. This connection's server certificate's must cover the new host.
    if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;
    // 4. Certificate pinning must match the host.
    try {
        address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
        return false;
    }
    return true; // The caller's address can be carried by this connection.
}
  • 判断连接是否可用涉及到一下几个知识:
  • 1、HTTP2.0多路复用机制, 同一个连接可以并发处理多个请求
  • 2、IP与域名是1:N的关系, 也就是多个域名可以对应同一个IP地址
  • 3、代理服务器的工作流程, 客户端访问代理服务器, 代理服务器替客户端完成网络访问
  • 4、路由节点
2.6 获取路由选择器RouteSelector.next
public Selection next() throws IOException {
    // Compute the next set of routes to attempt.
    List<Route> routes = new ArrayList<>();
    // 1.如果有代理服务器
    while (hasNextProxy()) {
        // Postponed routes are always tried last. For example, if we have 2 proxies and all the
        // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted
        // all the good routes will we attempt the postponed routes.
        // 2.获取下一个代理服务器. 获取路由选择器的重点都在nextProxy()这里
        Proxy proxy = nextProxy();
        for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
            // 3.创建Route与IP地址进行绑定
            Route route = new Route(address, proxy, inetSocketAddresses.get(i));
            if (routeDatabase.shouldPostpone(route)) {
                // 3.过滤掉不可用的IP地址, 避免对不可用的IP进行连接, 浪费资源
                postponedRoutes.add(route);
            } else {
                routes.add(route);
            }
        }
        if (!routes.isEmpty()) {
            break;
        }
    }
    if (routes.isEmpty()) {
        // We've exhausted all Proxies so fallback to the postponed routes.
        routes.addAll(postponedRoutes);
        postponedRoutes.clear();
    }
    // 4.返回路由选择器
    return new Selection(routes);
}
  • 获取路由选择器的重点都是在nextProxy(), 涉及到的知识点比较多
2.7 获取下一个代理服务器RouteSelector.nextProxy
nextProxy()
---resetNextInetSocketAddress()
/** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    // 1.遍历过程中每次获取下一个代理服务器并对url进行解析时, 都会首先清掉缓存
    inetSocketAddresses = new ArrayList<>();
    String socketHost;
    int socketPort;
    // 2.如果是直连或者SOCKS代理服务器, 主机名和端口号就是url中的主机名与端口号
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
        socketHost = address.url().host();
        socketPort = address.url().port();
    } else {
        // 2.如果是HTTP代理服务器, 获取代理服务器的主机名与端口号
        SocketAddress proxyAddress = proxy.address();
        InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
        socketHost = getHostString(proxySocketAddress);
        socketPort = proxySocketAddress.getPort();
    }
    if (proxy.type() == Proxy.Type.SOCKS) {
        // 3.如果是SOCKS代理服务器, 直接将主机名与端口号对应的InetSocketAddress放入集合中
        inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
        // 4.如果是直连或者是HTTP代理服务器
        eventListener.dnsStart(call, socketHost);
        // Try each address for best behavior in mixed IPv4/IPv6 environments.
        // 5.通过主机名获取对应的IP地址的集合(这里用到的DNS域名解析)
        // 如果是直连, 直接获取服务器的IP地址列表;
        // 如果是HTTP代理服务器, 获取的是代理服务器的IP地址的集合
        List<InetAddress> addresses = address.dns().lookup(socketHost);
        eventListener.dnsEnd(call, socketHost, addresses);
        for (int i = 0, size = addresses.size(); i < size; i++) {
            InetAddress inetAddress = addresses.get(i);
            // 6.根据IP列表地址创建多个InetSocketAddress放入到集合中.
            inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
        }
    }
}
  • SOCKS代理服务器、HTTP代理服务器、直连:(blog.csdn.net/qq_37816453…)
  • 涉及到的知识点: SOCKS代理服务器、HTTP代理服务器、直连、DNS域名解析.
2.8 对连接进行校验RealConnection.isHealthy
public boolean isHealthy(boolean doExtensiveChecks) {
    // 1.如果socket中断, 认为该连接不可用
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
        return false;
    }
    // 2.如果是HTTP2.0,
    if (http2Connection != null) {
      return http2Connection.isHealthy(System.nanoTime());
    }
    if (doExtensiveChecks) {
        try {
            int readTimeout = socket.getSoTimeout();
            try {
                socket.setSoTimeout(1);
                if (source.exhausted()) {
                    return false; // Stream is exhausted; socket is closed.
                }
                return true;
            } finally {
                socket.setSoTimeout(readTimeout);
            }
        } catch (SocketTimeoutException ignored) {
            // Read timed out; socket is good.
        } catch (IOException e) {
            return false; // Couldn't read; socket is closed.
        }
    }
    return true;
}