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

作者:程序员小北 时间:2023-03-17 01:21:51 

代理

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是个抽象类,继承它的类需要实现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类如下。

  • 内部使用 Set 结构来存储路由,保证数据不重复。

  • failed方法将失败的路由加入到 Set 中。

  • connected方法表示该路由连接成功,将它从 Set 中移除。

  • 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 &lt; 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.
     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);
}

来源:https://juejin.cn/post/7208239217943101495

标签:Android,OkHttp,代理,路由
0
投稿

猜你喜欢

  • C#实现简单合并word文档的方法

    2022-10-27 10:50:00
  • Android调用OpenCV2.4.10实现二维码区域定位

    2023-06-05 10:17:32
  • Java堆&优先级队列示例讲解(上)

    2023-04-09 11:09:59
  • 基于C#对用户密码使用MD5加密与解密

    2022-11-24 23:05:23
  • 详解C#读写Excel的几种方法

    2022-10-23 14:23:28
  • C# 使用SharpZipLib生成压缩包的实例代码

    2021-08-29 20:32:57
  • Android编程ProgressBar自定义样式之动画模式实现方法

    2022-02-10 12:54:29
  • JAVA设计模式之备忘录模式原理与用法详解

    2023-08-24 13:23:37
  • Android串口开发之使用JNI实现ANDROID和串口通信详解

    2023-12-02 17:17:01
  • Android DataBinding类关系深入探究

    2022-08-17 19:23:51
  • java获取文件的inode标识符的方法

    2021-06-19 15:10:49
  • SpringBoot接口调用之后报404问题的解决方案

    2021-08-31 15:25:03
  • Java 多线程学习详细总结

    2023-11-18 22:55:24
  • Java并发LinkedBlockingQueue源码分析

    2022-09-01 08:35:30
  • C#以流方式读socket超时设置的实例

    2021-10-04 15:09:11
  • Android SQLite基本用法详解

    2023-07-02 05:14:58
  • Android实现花瓣飘落效果的步骤

    2021-11-23 17:16:44
  • flutter仿微信底部图标渐变功能的实现代码

    2023-08-18 14:31:39
  • AndroidStduio3.0 使用gradle将module打包jar文件的方法

    2023-07-01 06:57:20
  • C#实现控制台飞行棋小游戏

    2023-01-01 15:43:12
  • asp之家 软件编程 m.aspxhome.com