署理

OkHttp 支撑设置署理,运用OkHttpClient.proxy()即可设置。

什么是署理?

  1. 依据署理的方针不同,可分为正向署理和反向署理。正向署理署理的是客户端,负责接纳客户端的恳求转发到方针服务器,并将结果回来给客户端。反向署理署理的是服务端,服务端将反向署理看做客户端。
  2. 正向署理一般用于突破拜访约束(如拜访外网),提高拜访速度。反向署理则用于负载均衡(如nginx),资源防护。
  3. 正向署理服务器布置在客户端侧,反向署理服务器布置在服务端侧。
  4. 运用正向署理,方针服务器对客户端来说是通明的,客户端将署理服务器看做是方针服务器。
  5. 运用反向署理,客户端对方针服务器来说的通明的,方针服务器将署理服务器看做是客户端。

署理的类型

依据署理服务器运用署理协议的不同,可分为 Http 署理,Http Tunnel(地道)署理,Socks 署理。3种署理协议的完成原理各有不同,读者可自行查找相关材料了解。

  • Http 署理:咱们知道若一个恳求直接发送到方针服务器时,恳求行中只会包括相对路径的 URL (完好 URL 的 path 部分)。而一个恳求发送到 http 署理服务器,要求它恳求行的url为绝对路径,这遵从了 www.ietf.org/rfc/rfc2616… 5.1.2末节规范的规则。

  • Http Tunnel 署理:也称为 Http 地道署理,最早在 www.ietf.org/rfc/rfc2817… 5.1 末节定义,地道署理的出现为了让署理服务器能跑 https 的流量。地道署理需求客户端首先发送一个恳求办法为CONNECT 的报文,恳求地道署理创立一条抵达任意意图服务器和端口的 TCP 衔接,并对客户端和意图服务器之间的后继数据进行原样转发。

  • Socks 署理:Socks 是最常见的署理服务协议,服务通常运用 1080 端口。Socks 署理与其他类型的署理不同,它仅仅简略地传递数据包,而并不关心是何种应用协议,所以 Socks 署理服务器比其他类型的署理服务器速度要快得多。Socks 署理又分为 Socks4 和 Socks5,二者不同的是 Socks4 署理只支撑 TCP 协议,而 Socks5 署理则既支撑 TCP 协议又支撑 UDP 协议,还支撑各种身份验证机制、服务器端域名解析等。

早在 jdk 1.5中就供给了一个Proxy类来表示署理。

public class Proxy {
    // 署理类型
    public enum Type {
        // 不运用署理,直连方针服务器
        DIRECT,
        // HTTP 协议署理
        HTTP,
        // SOCKS 协议署理
        SOCKS
    };
    // 署理类型
    private Type type;
    // 署理的 IP 套接字地址(IP + 端口号)
    private SocketAddress sa;
    public final static Proxy NO_PROXY = new Proxy();
    // 默许不运用署理
    private Proxy() {
        type = Type.DIRECT;
        sa = null;
    }
}

署理挑选器

jdk 供给了一个名为ProxySelector的类,意为“署理挑选器”。ProxySelector是个抽象类,继承它的类需求完成selectconnectFailed办法,这说明咱们可通过继承ProxySelector自定义署理挑选器,在select办法中回来自定义的署理列表。而当一个署理服务器无法衔接时,调用connectFailed办法告诉署理挑选器当时署理服务器不可用。如下代码,ProxySelector的静态代码块中运用Class方针的newInstance办法创立了一个DefaultProxySelector的方针。

public abstract class ProxySelector {
    private static ProxySelector theProxySelector;
    // 创立 DefaultProxySelector 方针
    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;
        }
    }
    public static ProxySelector getDefault() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);
        }
        return theProxySelector;
    }
    public abstract List<Proxy> select(URI uri);
    public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe);
}

ProxySelector有个两个子类DefaultProxySelectorNullProxySelector

DefaultProxySelector:jdk 中供给的署理挑选器,也是 OkHttp 默许运用的署理挑选器,select回来系统设置的署理列表。

NullProxySelector:OkHttp 中供给的署理挑选器,select回来的署理列表只包括一个NO_PROXY,即不运用署理。

在 OkHttp 中可以运用OkHttpClient.proxy(proxy)设置署理,也可以运用OkHttpClient.proxySelector设置署理挑选器。OkHttp 会优先运用设置的署理去衔接署理服务器,而不是从署理列表中挑选。如下代码, OkHttpClient默许运用DefaultProxySelector署理挑选器,除非getDefault回来null,才运用NullProxySelector

public Builder() {
  proxySelector = ProxySelector.getDefault();
  if (proxySelector == null) {
    proxySelector = new NullProxySelector();
  }
}

路由

什么是路由?

在 OkHttp 中,路由表示一个恳求到方针服务器或署理服务器的具体道路。关于一个恳求来说,假如它的url是域名,经过 DNS 解析之后或许会对应多个 IP 地址,这意味着一个恳求抵达服务器的路由就有多个

Android | 彻底理解 OkHttp 代理与路由

如下程序在我本机环境下运用InetAddress类解析baidu.com这个域名,IP 地址就有两个。

public void domainResolution() throws UnknownHostException {
    InetAddress[] inetAddresses = InetAddress.getAllByName("baidu.com");
    for (InetAddress inetAddress : inetAddresses) {
        System.out.println(inetAddress.toString());
    }
}
baidu.com/39.156.66.10
baidu.com/110.242.68.66

OkHttp 会挑选其中一个路由来树立到服务器的衔接。Route类描绘了一个路由应该包括的信息:装备信息,署理信息,署理或方针服务器地址,是否运用 Http 地道署理。

public final class Route {
  // 与方针服务器树立衔接所需求的装备信息,包括方针主机名、端口、dns 等
  final Address address;
  // 该路由的署理信息
  final Proxy proxy;
  // 署理服务器或方针服务器的地址
  final InetSocketAddress inetSocketAddress;
  // 该路由是否运用 Http 地道署理
  public boolean requiresTunnel() {
    return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
  }  
}

路由数据库

路由数据库是一个路由黑名单库,存储了那些衔接到特定 IP 地址或署理服务器失利的路由。这样在创立新的衔接时,就可以防止运用这些路由。RouteDatabase类如下。

  1. 内部运用 Set 结构来存储路由,保证数据不重复。
  2. failed办法将失利的路由加入到 Set 中。
  3. connected办法表示该路由衔接成功,将它从 Set 中移除。
  4. shouldPostpone办法用于判断该路由是否在黑名单中。
final class RouteDatabase {
  private final Set<Route> failedRoutes = new LinkedHashSet<>();
  /** Records a failure connecting to {@code failedRoute}. */
  public synchronized void failed(Route failedRoute) {
    failedRoutes.add(failedRoute);
  }
  /** Records success connecting to {@code route}. */
  public synchronized void connected(Route route) {
    failedRoutes.remove(route);
  }
  /** Returns true if {@code route} has failed recently and should be avoided. */
  public synchronized boolean shouldPostpone(Route route) {
    return failedRoutes.contains(route);
  }
}

路由挑选器

RouteSelector是 OkHttp 中的路由挑选器,它的next办法可以回来一个合适的路由调集(Selection)用于衔接方针服务器。它的全体作业流程如下所示。

Android | 彻底理解 OkHttp 代理与路由

RouteSelector 内部类 Selection

Selection表示被next办法选中的路由调集。内部有一个路由列表和下一个路由的索引。

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);
    }
}

RouteSelector 成员变量

  • address:方针服务器地址信息,包括 url,dns,端口信息等。
  • routeDatabase:路由黑名单库
  • call:Call 方针
  • eventListener:Http 恳求事情监听器
  • proxies:署理列表
  • nextProxyIndex:下一个署理的索引
  • inetSocketAddresses:用于衔接署理或方针服务器可用的地址列表
  • postponedRoutes:不可用的路由列表
private final Address address;
private final RouteDatabase routeDatabase;
private final Call call;
private final EventListener eventListener;
/* State for negotiating the next proxy to use. */
private List<Proxy> proxies = Collections.emptyList();
private int nextProxyIndex;
/* State for negotiating the next socket address to use. */
private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
/* State for negotiating failed routes */
private final List<Route> postponedRoutes = new ArrayList<>();

RouteSelector 成员办法

// 初始化署理列表
private void resetNextProxy(HttpUrl url, Proxy proxy);
// 是否有下一个署理
private boolean hasNextProxy();
// 是否含有路由可以测验衔接
public boolean hasNext();
// 初始化衔接署理或方针服务器的地址列表
private void resetNextInetSocketAddress(Proxy proxy) throws IOException;
// 回来署理列表中下一个署理
private Proxy nextProxy() throws IOException;
// 回来路由调集
public Selection next() throws IOException;

resetNextProxy-初始化署理列表

resetNextProxy是个私有办法,在RouteSelector类的结构函数内被调用,用于初始化署理列表。前文咱们说过,若OkHttpClient设置了署理,则仅会运用这1个署理。而若没有设置署理则会从署理挑选器获取署理列表。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 {
      // 若没有设置署理,则调用署理挑选器的 select 办法获取署理列表
      // Try each of the ProxySelector choices until one connection succeeds.
      List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
      // 若 select 回来的署理列表为空,以为不运用署理,以 Proxy.NO_PROXY 初始化
      proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
          ? Util.immutableList(proxiesOrNull)
          : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
}

hasNextProxy-是否还有署理

hasNextProxy回来署理列表中是否还有下一个署理用于衔接。

private boolean hasNextProxy() {
	return nextProxyIndex < proxies.size();
}

hasNext-是否还有路由调集

public boolean hasNext() {
	return hasNextProxy() || !postponedRoutes.isEmpty();
}

resetNextInetSocketAddress-初始化地址列表

resetNextInetSocketAddress用于初始化地址列表,这个地址列表是通往署理服务器或方针服务器的,这取决于所运用的署理类型。

  1. 关于DIRECT(直连)和SOCKS类型的署理来说,会运用方针服务器的主机名和端口号。而HTTP类型的署理则会运用署理服务器的主机名和端口号。
  2. SOCKS 类型的署理只会生成一个通往方针服务器的地址
  3. 直连类型的署理,经 DNS 解析方针服务器主机名后,或许生成多个通往方针服务器的地址。
  4. HTTP 类型的署理,经 DNS 解析方针服务器主机名后,或许生成多个通往署理服务器的地址。
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    inetSocketAddresses = new ArrayList<>();
    // 主机名
    String socketHost;
    // 端口号
    int socketPort;
    // 若署理类型为直连或 SOCKS,则运用方针服务器的主机名和端口号
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
      // 若署理类型为 HTTP,则运用署理服务器的主机名和端口号
      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");
    }
    // SOCKS 类型的署理只会生成一个通往方针服务器的地址
    if (proxy.type() == Proxy.Type.SOCKS) {
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
      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));
      }
    }
}

nextProxy-回来署理列表中下一个署理

nextProxy会从署理列表中取出一个署理回来,同时会调用resetNextInetSocketAddress办法传入当时取出的署理,依据这个署理来初始化地址列表。一个署理对应一个地址列表。

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;
}

next-回来路由调集

nextRouteSelector类中最重要的办法,供外部调用。包括了路由挑选器一次完好的作业流程。

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()) {
      // 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.
      // 从署理列表中取出一个署理
      Proxy proxy = nextProxy();
      // 遍历该署理对应的地址列表
      for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
        // 创立该地址对应的路由
        Route route = new Route(address, proxy, inetSocketAddresses.get(i));
        // 若该路由在黑名单,则添加到 postponedRoutes
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes.add(route);
        } else {
        // 不然添加到 routes
          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();
    }
	// 回来路由调集
    return new Selection(routes);
}

总结

本末节详细分析了RouteSelector路由挑选器的源码,并对它的全体作业流程做了分析。最后回来的路由调集便是能抵达署理或方针服务器的全部道路,客户端只需求从中挑选一条路由进行衔接就行了。

写在最后

假如你对我感兴趣,请移步到 blogss.cn ,或重视大众号:程序员小北,进一步了解。

  • 假如本文协助到了你,欢迎点赞和重视,这是我继续创作的动力 ❤️
  • 由于作者水平有限,文中假如有错误,欢迎在评论区指正 ✔️
  • 本文首发于,未经许可禁止转载 ️