Android OkHttp代理與路由的徹底理解
代理
OkHttp 支持設(shè)置代理,使用OkHttpClient.proxy()即可設(shè)置。
什么是代理?
- 根據(jù)代理的對象不同,可分為正向代理和反向代理。正向代理代理的是客戶端,負責(zé)接收客戶端的請求轉(zhuǎn)發(fā)到目標服務(wù)器,并將結(jié)果返回給客戶端。反向代理代理的是服務(wù)端,服務(wù)端將反向代理看做客戶端。
- 正向代理一般用于突破訪問限制(如訪問外網(wǎng)),提高訪問速度。反向代理則用于負載均衡(如nginx),資源防護。
- 正向代理服務(wù)器部署在客戶端側(cè),反向代理服務(wù)器部署在服務(wù)端側(cè)。
- 使用正向代理,目標服務(wù)器對客戶端來說是透明的,客戶端將代理服務(wù)器看做是目標服務(wù)器。
- 使用反向代理,客戶端對目標服務(wù)器來說的透明的,目標服務(wù)器將代理服務(wù)器看做是客戶端。
代理的類型
根據(jù)代理服務(wù)器使用代理協(xié)議的不同,可分為 Http 代理,Http Tunnel(隧道)代理,Socks 代理。3種代理協(xié)議的實現(xiàn)原理各有不同,讀者可自行查找相關(guān)資料了解。
Http 代理:我們知道若一個請求直接發(fā)送到目標服務(wù)器時,請求行中只會包含相對路徑的 URL (完整 URL 的 path 部分)。而一個請求發(fā)送到 http 代理服務(wù)器,要求它請求行的url為絕對路徑,這遵循了 www.ietf.org/rfc/rfc2616… 5.1.2小節(jié)標準的規(guī)定。
Http Tunnel 代理:也稱為 Http 隧道代理,最早在 www.ietf.org/rfc/rfc2817… 5.1 小節(jié)定義,隧道代理的出現(xiàn)為了讓代理服務(wù)器能跑 https 的流量。隧道代理需要客戶端首先發(fā)送一個請求方法為CONNECT 的報文,請求隧道代理創(chuàng)建一條到達任意目的服務(wù)器和端口的 TCP 連接,并對客戶端和目的服務(wù)器之間的后繼數(shù)據(jù)進行原樣轉(zhuǎn)發(fā)。
Socks 代理:Socks 是最常見的代理服務(wù)協(xié)議,服務(wù)通常使用 1080 端口。Socks 代理與其他類型的代理不同,它只是簡單地傳遞數(shù)據(jù)包,而并不關(guān)心是何種應(yīng)用協(xié)議,所以 Socks 代理服務(wù)器比其他類型的代理服務(wù)器速度要快得多。Socks 代理又分為 Socks4 和 Socks5,二者不同的是 Socks4 代理只支持 TCP 協(xié)議,而 Socks5 代理則既支持 TCP 協(xié)議又支持 UDP 協(xié)議,還支持各種身份驗證機制、服務(wù)器端域名解析等。
早在 jdk 1.5中就提供了一個Proxy類來表示代理。
public class Proxy {
// 代理類型
public enum Type {
// 不使用代理,直連目標服務(wù)器
DIRECT,
// HTTP 協(xié)議代理
HTTP,
// SOCKS 協(xié)議代理
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是個抽象類,繼承它的類需要實現(xiàn)select和connectFailed方法,這說明我們可通過繼承ProxySelector自定義代理選擇器,在select方法中返回自定義的代理列表。而當一個代理服務(wù)器無法連接時,調(diào)用connectFailed方法通知代理選擇器當前代理服務(wù)器不可用。如下代碼,ProxySelector的靜態(tài)代碼塊中使用Class對象的newInstance方法創(chuàng)建了一個DefaultProxySelector的對象。
public abstract class ProxySelector {
private static ProxySelector theProxySelector;
// 創(chuàng)建 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有個兩個子類DefaultProxySelector和NullProxySelector。
DefaultProxySelector:jdk 中提供的代理選擇器,也是 OkHttp 默認使用的代理選擇器,select返回系統(tǒng)設(shè)置的代理列表。
NullProxySelector:OkHttp 中提供的代理選擇器,select返回的代理列表只包含一個NO_PROXY,即不使用代理。
在 OkHttp 中可以使用OkHttpClient.proxy(proxy)設(shè)置代理,也可以使用OkHttpClient.proxySelector設(shè)置代理選擇器。OkHttp 會優(yōu)先使用設(shè)置的代理去連接代理服務(wù)器,而不是從代理列表中選擇。如下代碼, OkHttpClient默認使用DefaultProxySelector代理選擇器,除非getDefault返回null,才使用NullProxySelector。
public Builder() {
proxySelector = ProxySelector.getDefault();
if (proxySelector == null) {
proxySelector = new NullProxySelector();
}
}
路由
什么是路由?
在 OkHttp 中,路由表示一個請求到目標服務(wù)器或代理服務(wù)器的具體路線。對于一個請求來說,如果它的url是域名,經(jīng)過 DNS 解析之后可能會對應(yīng)多個 IP 地址,這意味著一個請求到達服務(wù)器的路由就有多個。

如下程序在我本機環(huán)境下使用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 會選擇其中一個路由來建立到服務(wù)器的連接。Route類描述了一個路由應(yīng)該包含的信息:配置信息,代理信息,代理或目標服務(wù)器地址,是否使用 Http 隧道代理。
public final class Route {
// 與目標服務(wù)器建立連接所需要的配置信息,包括目標主機名、端口、dns 等
final Address address;
// 該路由的代理信息
final Proxy proxy;
// 代理服務(wù)器或目標服務(wù)器的地址
final InetSocketAddress inetSocketAddress;
// 該路由是否使用 Http 隧道代理
public boolean requiresTunnel() {
return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
}
}
路由數(shù)據(jù)庫
路由數(shù)據(jù)庫是一個路由黑名單庫,存儲了那些連接到特定 IP 地址或代理服務(wù)器失敗的路由。這樣在創(chuàng)建新的連接時,就可以避免使用這些路由。RouteDatabase類如下。
- 內(nèi)部使用 Set 結(jié)構(gòu)來存儲路由,保證數(shù)據(jù)不重復(fù)。
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)用于連接目標服務(wù)器。它的整體工作流程如下所示。

RouteSelector 內(nèi)部類 Selection
Selection表示被next方法選中的路由集合。內(nèi)部有一個路由列表和下一個路由的索引。
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:目標服務(wù)器地址信息,包括 url,dns,端口信息等。
- routeDatabase:路由黑名單庫
- call:Call 對象
- eventListener:Http 請求事件監(jiān)聽器
- proxies:代理列表
- nextProxyIndex:下一個代理的索引
- inetSocketAddresses:用于連接代理或目標服務(wù)器可用的地址列表
- 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(); // 初始化連接代理或目標服務(wù)器的地址列表 private void resetNextInetSocketAddress(Proxy proxy) throws IOException; // 返回代理列表中下一個代理 private Proxy nextProxy() throws IOException; // 返回路由集合 public Selection next() throws IOException;
resetNextProxy-初始化代理列表
resetNextProxy是個私有方法,在RouteSelector類的構(gòu)造函數(shù)內(nèi)被調(diào)用,用于初始化代理列表。前文我們說過,若OkHttpClient設(shè)置了代理,則僅會使用這1個代理。而若沒有設(shè)置代理則會從代理選擇器獲取代理列表。resetNextProxy方法的實現(xiàn)正遵循這樣的規(guī)則。
private void resetNextProxy(HttpUrl url, Proxy proxy) {
// 若設(shè)置了代理,僅使用這一個代理
if (proxy != null) {
// If the user specifies a proxy, try that and only that.
proxies = Collections.singletonList(proxy);
} else {
// 若沒有設(shè)置代理,則調(diào)用代理選擇器的 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用于初始化地址列表,這個地址列表是通往代理服務(wù)器或目標服務(wù)器的,這取決于所使用的代理類型。
- 對于
DIRECT(直連)和SOCKS類型的代理來說,會使用目標服務(wù)器的主機名和端口號。而HTTP類型的代理則會使用代理服務(wù)器的主機名和端口號。 - SOCKS 類型的代理只會生成一個通往目標服務(wù)器的地址。
- 直連類型的代理,經(jīng) DNS 解析目標服務(wù)器主機名后,可能生成多個通往目標服務(wù)器的地址。
- HTTP 類型的代理,經(jīng) DNS 解析目標服務(wù)器主機名后,可能生成多個通往代理服務(wù)器的地址。
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// Clear the addresses. Necessary if getAllByName() below throws!
inetSocketAddresses = new ArrayList<>();
// 主機名
String socketHost;
// 端口號
int socketPort;
// 若代理類型為直連或 SOCKS,則使用目標服務(wù)器的主機名和端口號
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
socketHost = address.url().host();
socketPort = address.url().port();
} else {
// 若代理類型為 HTTP,則使用代理服務(wù)器的主機名和端口號
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 類型的代理只會生成一個通往目標服務(wù)器的地址
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會從代理列表中取出一個代理返回,同時會調(diào)用resetNextInetSocketAddress方法傳入當前取出的代理,根據(jù)這個代理來初始化地址列表。一個代理對應(yīng)一個地址列表。
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類中最重要的方法,供外部調(diào)用。包含了路由選擇器一次完整的工作流程。
public Selection next() throws IOException {
// 若沒有路由集合了,拋出異常
if (!hasNext()) {
throw new NoSuchElementException();
}
// Compute the next set of routes to attempt.
List<Route> routes = new ArrayList<>();
// 循環(huán)直到?jīng)]有代理可用
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();
// 遍歷該代理對應(yīng)的地址列表
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
// 創(chuàng)建該地址對應(yīng)的路由
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
// 若該路由在黑名單,則添加到 postponedRoutes
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
} else {
// 否則添加到 routes
routes.add(route);
}
}
// 若該代理對應(yīng)的地址列表不為空,退出循環(huán)
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);
}
總結(jié)
本小節(jié)詳細分析了RouteSelector路由選擇器的源碼,并對它的整體工作流程做了分析。最后返回的路由集合就是能到達代理或目標服務(wù)器的全部路線,客戶端只需要從中選擇一條路由進行連接就行了。
更多關(guān)于Android OkHttp代理路由的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Compose?for?Desktop?鼠標事件示例demo
這篇文章主要為大家介紹了Compose?for?Desktop?鼠標事件示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Android中應(yīng)用前后臺切換監(jiān)聽的實現(xiàn)詳解
這篇文章主要給大家介紹了關(guān)于Android中應(yīng)用前后臺切換監(jiān)聽實現(xiàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-07-07
Android自定義控件實現(xiàn)帶文本與數(shù)字的圓形進度條
這篇文章主要為大家詳細介紹了Android自定義控件實現(xiàn)帶文本與數(shù)字的圓形進度條,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12
Android在不使用數(shù)據(jù)庫的情況下存儲數(shù)據(jù)的方法
這篇文章主要介紹了Android在不使用數(shù)據(jù)庫的情況下存儲數(shù)據(jù)的方法,涉及Android存儲數(shù)據(jù)的相關(guān)技巧,需要的朋友可以參考下2015-04-04
Android實現(xiàn)QQ新用戶注冊界面遇到問題及解決方法
這篇文章主要介紹了Android實現(xiàn)QQ新用戶注冊界面遇到問題及解決方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09
Android中TelephonyManager類的方法實例分析
這篇文章主要介紹了Android中TelephonyManager類的方法,以實例形式較為詳細的分析了Android基于TelephonyManager類獲取手機各種常用信息的相關(guān)技巧,需要的朋友可以參考下2015-09-09
Android使用OkHttp進行網(wǎng)絡(luò)同步異步操作
這篇文章主要為大家詳細介紹了Android使用OkHttp進行網(wǎng)絡(luò)同步異步操作,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
Android SharedPreferences存儲用法詳解
這篇文章主要為大家詳細介紹了Android SharedPreferences存儲用法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02

