代理
OkHttp 支持设置代理,使用OkHttpClient.proxy()
即可设置。
什么是代理?
- 根据代理的对象不同,可分为正向代理和反向代理。正向代理代理的是客户端,负责接收客户端的请求转发到目标服务器,并将结果返回给客户端。反向代理代理的是服务端,服务端将反向代理看做客户端。
- 正向代理一般用于突破访问限制(如访问外网),提高访问速度。反向代理则用于负载均衡(如nginx),资源防护。
- 正向代理服务器部署在客户端侧,反向代理服务器部署在服务端侧。
- 使用正向代理,目标服务器对客户端来说是透明的,客户端将代理服务器看做是目标服务器。
- 使用反向代理,客户端对目标服务器来说的透明的,目标服务器将代理服务器看做是客户端。
代理的类型
根据代理服务器使用代理协议的不同,可分为 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
是个抽象类,继承它的类需要实现select
和connectFailed
方法,这说明我们可通过继承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 Listselect(URI uri); public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe); }
ProxySelector
有个两个子类DefaultProxySelector
和NullProxySelector
。
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 地址,这意味着一个请求到达服务器的路由就有多个。
如下程序在我本机环境下使用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
类如下。
- 内部使用 Set 结构来存储路由,保证数据不重复。
failed
方法将失败的路由加入到 Set 中。connected
方法表示该路由连接成功,将它从 Set 中移除。shouldPostpone
方法用于判断该路由是否在黑名单中。
final class RouteDatabase { private final SetfailedRoutes = 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)用于连接目标服务器。它的整体工作流程如下所示。
RouteSelector 内部类 Selection
Selection
表示被next
方法选中的路由集合。内部有一个路由列表和下一个路由的索引。
public static final class Selection { // 路由列表 private final Listroutes; // 下一个路由的索引 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); } }
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 Listproxies = Collections.emptyList(); private int nextProxyIndex; /* State for negotiating the next socket address to use. */ private List inetSocketAddresses = Collections.emptyList(); /* State for negotiating failed routes */ private final List 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. ListproxiesOrNull = 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
用于初始化地址列表,这个地址列表是通往代理服务器或目标服务器的,这取决于所使用的代理类型。
- 对于
DIRECT
(直连)和SOCKS
类型的代理来说,会使用目标服务器的主机名和端口号。而HTTP
类型的代理则会使用代理服务器的主机名和端口号。 - SOCKS 类型的代理只会生成一个通往目标服务器的地址。
- 直连类型的代理,经 DNS 解析目标服务器主机名后,可能生成多个通往目标服务器的地址。
- 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. Listaddresses = 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-返回路由集合
next
是RouteSelector
类中最重要的方法,供外部调用。包含了路由选择器一次完整的工作流程。
public Selection next() throws IOException { // 若没有路由集合了,抛出异常 if (!hasNext()) { throw new NoSuchElementException(); } // Compute the next set of routes to attempt. Listroutes = 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
路由选择器的源码,并对它的整体工作流程做了分析。最后返回的路由集合就是能到达代理或目标服务器的全部路线,客户端只需要从中选择一条路由进行连接就行了。
更多关于Android OkHttp代理路由的资料请关注脚本之家其它相关文章!