您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Android OkHttp代理與路由怎么應用”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Android OkHttp代理與路由怎么應用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
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 List<Proxy> select(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 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)用于連接目標服務器。它的整體工作流程如下所示。
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); } }
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<>();
// 初始化代理列表 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
是個私有方法,在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
返回代理列表中是否還有下一個代理用于連接。
private boolean hasNextProxy() { return nextProxyIndex < proxies.size(); }
public boolean hasNext() { return hasNextProxy() || !postponedRoutes.isEmpty(); }
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
會從代理列表中取出一個代理返回,同時會調用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
是RouteSelector
類中最重要的方法,供外部調用。包含了路由選擇器一次完整的工作流程。
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); }
讀到這里,這篇“Android OkHttp代理與路由怎么應用”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。