日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

OkHttp3中的代理与路由

發(fā)布時(shí)間:2024/4/11 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 OkHttp3中的代理与路由 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

HTTP請(qǐng)求的整體處理過(guò)程大體可以理解為,

  • 建立TCP連接。
  • 如果是HTTPS的話,完成SSL/TLS的協(xié)商。
  • 發(fā)送請(qǐng)求。
  • 獲取響應(yīng)。
  • 結(jié)束請(qǐng)求,關(guān)閉連接。
  • 然而,當(dāng)為系統(tǒng)設(shè)置了代理的時(shí)候,整個(gè)數(shù)據(jù)流都會(huì)經(jīng)過(guò)代理服務(wù)器。那么代理設(shè)置究竟是如何工作的呢?它是如何影響我們上面看到的HTTP請(qǐng)求的處理過(guò)程的呢?是在操作系統(tǒng)內(nèi)核的TCP實(shí)現(xiàn)中的策略呢,還是HTTP stack中的機(jī)制?這里我們通過(guò)OkHttp3中的實(shí)現(xiàn)來(lái)一探究竟。

    代理分為兩種類型,一種是SOCKS代理,另一種是HTTP代理。對(duì)于SOCKS代理,在HTTP的場(chǎng)景下,代理服務(wù)器完成TCP數(shù)據(jù)包的轉(zhuǎn)發(fā)工作。而HTTP代理服務(wù)器,在轉(zhuǎn)發(fā)數(shù)據(jù)之外,還會(huì)解析HTTP的請(qǐng)求及響應(yīng),并根據(jù)請(qǐng)求及響應(yīng)的內(nèi)容做一些處理。這里看一下OkHttp中對(duì)代理的處理。

    代理服務(wù)器的描述

    在Java中,通過(guò) java.net.Proxy 類描述一個(gè)代理服務(wù)器:

    public class Proxy {/*** Represents the proxy type.** @since 1.5*/public enum Type {/*** Represents a direct connection, or the absence of a proxy.*/DIRECT,/*** Represents proxy for high level protocols such as HTTP or FTP.*/HTTP,/*** Represents a SOCKS (V4 or V5) proxy.*/SOCKS};private Type type;private SocketAddress sa;/*** A proxy setting that represents a {@code DIRECT} connection,* basically telling the protocol handler not to use any proxying.* Used, for instance, to create sockets bypassing any other global* proxy settings (like SOCKS):* <P>* {@code Socket s = new Socket(Proxy.NO_PROXY);}**/public final static Proxy NO_PROXY = new Proxy();// Creates the proxy that represents a {@code DIRECT} connection.private Proxy() {type = Type.DIRECT;sa = null;}/*** Creates an entry representing a PROXY connection.* Certain combinations are illegal. For instance, for types Http, and* Socks, a SocketAddress <b>must</b> be provided.* <P>* Use the {@code Proxy.NO_PROXY} constant* for representing a direct connection.** @param type the {@code Type} of the proxy* @param sa the {@code SocketAddress} for that proxy* @throws IllegalArgumentException when the type and the address are* incompatible*/public Proxy(Type type, SocketAddress sa) {if ((type == Type.DIRECT) || !(sa instanceof InetSocketAddress))throw new IllegalArgumentException("type " + type + " is not compatible with address " + sa);this.type = type;this.sa = sa;}/*** Returns the proxy type.** @return a Type representing the proxy type*/public Type type() {return type;}/*** Returns the socket address of the proxy, or* {@code null} if its a direct connection.** @return a {@code SocketAddress} representing the socket end* point of the proxy*/public SocketAddress address() {return sa;} ...... }

    只用代理的類型及代理服務(wù)器地址即可描述代理服務(wù)器的全部。對(duì)于HTTP代理,代理服務(wù)器地址可以通過(guò)域名和IP地址等方式來(lái)描述。

    代理選擇器ProxySelector

    在Java中通過(guò)ProxySelector為一個(gè)特定的URI選擇代理:

    public abstract class ProxySelector { ....../*** Selects all the applicable proxies based on the protocol to* access the resource with and a destination address to access* the resource at.* The format of the URI is defined as follow:* <UL>* <LI>http URI for http connections</LI>* <LI>https URI for https connections* <LI>{@code socket://host:port}<br>* for tcp client sockets connections</LI>* </UL>** @param uri* The URI that a connection is required to** @return a List of Proxies. Each element in the* the List is of type* {@link java.net.Proxy Proxy};* when no proxy is available, the list will* contain one element of type* {@link java.net.Proxy Proxy}* that represents a direct connection.* @throws IllegalArgumentException if the argument is null*/public abstract List<Proxy> select(URI uri);/*** Called to indicate that a connection could not be established* to a proxy/socks server. An implementation of this method can* temporarily remove the proxies or reorder the sequence of* proxies returned by {@link #select(URI)}, using the address* and the IOException caught when trying to connect.** @param uri* The URI that the proxy at sa failed to serve.* @param sa* The socket address of the proxy/SOCKS server** @param ioe* The I/O exception thrown when the connect failed.* @throws IllegalArgumentException if either argument is null*/public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe); }

    這個(gè)組件會(huì)讀區(qū)系統(tǒng)中配置的所有代理,并根據(jù)調(diào)用者傳入的URI,返回特定的代理服務(wù)器集合。由于不同系統(tǒng)中,配置代理服務(wù)器的方法,及相關(guān)配置的保存機(jī)制不同,該接口在不同的系統(tǒng)中有著不同的實(shí)現(xiàn)。

    OkHttp3的路由

    OkHttp3中抽象出Route來(lái)描述網(wǎng)絡(luò)數(shù)據(jù)包的傳輸路徑,最主要還是要描述直接與其建立TCP連接的目標(biāo)端點(diǎn)。

    public final class Route {final Address address;final Proxy proxy;final InetSocketAddress inetSocketAddress;public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {if (address == null) {throw new NullPointerException("address == null");}if (proxy == null) {throw new NullPointerException("proxy == null");}if (inetSocketAddress == null) {throw new NullPointerException("inetSocketAddress == null");}this.address = address;this.proxy = proxy;this.inetSocketAddress = inetSocketAddress;}public Address address() {return address;}/*** Returns the {@link Proxy} of this route.** <strong>Warning:</strong> This may disagree with {@link Address#proxy} when it is null. When* the address's proxy is null, the proxy selector is used.*/public Proxy proxy() {return proxy;}public InetSocketAddress socketAddress() {return inetSocketAddress;}/*** Returns true if this route tunnels HTTPS through an HTTP proxy. See <a* href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.*/public boolean requiresTunnel() {return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;} ...... }

    主要通過(guò) 代理服務(wù)器的信息proxy ,及 連接的目標(biāo)地址 描述路由。 連接的目標(biāo)地址inetSocketAddress 根據(jù)代理類型的不同而有著不同的含義,這主要是由不同代理協(xié)議的差異而造成的。對(duì)于無(wú)需代理的情況, 連接的目標(biāo)地址inetSocketAddress 中包含HTTP服務(wù)器經(jīng)過(guò)了DNS域名解析的IP地址及協(xié)議端口號(hào);對(duì)于SOCKS代理,其中包含HTTP服務(wù)器的域名及協(xié)議端口號(hào);對(duì)于HTTP代理,其中則包含代理服務(wù)器經(jīng)過(guò)域名解析的IP地址及端口號(hào)。

    路由選擇器RouteSelector

    HTTP請(qǐng)求處理過(guò)程中所需的TCP連接建立過(guò)程,主要是找到一個(gè)Route,然后依據(jù)代理協(xié)議的規(guī)則與特定目標(biāo)建立TCP連接。對(duì)于無(wú)代理的情況,是與HTTP服務(wù)器建立TCP連接;對(duì)于SOCKS代理及HTTP代理,是與代理服務(wù)器建立TCP連接,雖然都是與代理服務(wù)器建立TCP連接,而SOCKS代理協(xié)議與HTTP代理協(xié)議做這個(gè)動(dòng)作的方式又會(huì)有一定的區(qū)別。

    借助于域名解析做負(fù)載均衡已經(jīng)是網(wǎng)絡(luò)中非常常見(jiàn)的手法了,因而,常常會(huì)有相同域名對(duì)應(yīng)不同IP地址的情況。同時(shí)相同系統(tǒng)也可以設(shè)置多個(gè)代理,這使Route的選擇變得復(fù)雜起來(lái)。

    在OkHttp中,對(duì)Route連接失敗有一定的錯(cuò)誤處理機(jī)制。OkHttp會(huì)逐個(gè)嘗試找到的Route建立TCP連接,直到找到可用的那一個(gè)。這同樣要求,對(duì)Route信息有良好的管理。

    OkHttp3借助于 RouteSelector 類管理所有的路由信息,并幫助選擇路由。 RouteSelector 主要完成3件事:

  • 收集所有可用的路由。

    public final class RouteSelector { private final Address address; private final RouteDatabase routeDatabase;/* The most recently attempted route. */ private Proxy lastProxy; private InetSocketAddress lastInetSocketAddress;/* 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(); private int nextInetSocketAddressIndex;/* State for negotiating failed routes */ private final List<Route> postponedRoutes = new ArrayList<>();public RouteSelector(Address address, RouteDatabase routeDatabase) {this.address = address;this.routeDatabase = routeDatabase;resetNextProxy(address.url(), address.proxy()); } ....../** Prepares the proxy servers to try. */ 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 {// Try each of the ProxySelector choices until one connection succeeds. If none succeed// then we'll try a direct connection below.proxies = new ArrayList<>();List<Proxy> selectedProxies = address.proxySelector().select(url.uri());if (selectedProxies != null) proxies.addAll(selectedProxies);// Finally try a direct connection. We only try it once!proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));proxies.add(Proxy.NO_PROXY);}nextProxyIndex = 0; }

    收集路由分為兩個(gè)步驟:第一步收集所有的代理;第二步則是收集特定代理服務(wù)器選擇情況下的所有 連接的目標(biāo)地址
    收集代理的過(guò)程如上面的這段代碼所示,有兩種方式,一是外部通過(guò)address傳入了代理,此時(shí)代理集合將包含這唯一的代理。address的代理最終來(lái)源于OkHttpClient,我們可以在構(gòu)造OkHttpClient時(shí)設(shè)置代理,來(lái)指定由該client執(zhí)行的所有請(qǐng)求經(jīng)過(guò)特定的代理。
    另一種方式是,借助于ProxySelector獲取多個(gè)代理。ProxySelector最終也來(lái)源于OkHttpClient,OkHttp的用戶當(dāng)然也可以對(duì)此進(jìn)行配置。但通常情況下,使用系統(tǒng)默認(rèn)的ProxySelector,來(lái)獲取系統(tǒng)中配置的代理。
    收集到的所有代理保存在列表 proxies 中。
    為OkHttpClient配置Proxy或ProxySelector的場(chǎng)景大概是,需要讓連接使用代理,但不使用系統(tǒng)的代理配置的情況。
    收集特定代理服務(wù)器選擇情況下的所有路由,因代理類型的不同而有著不同的過(guò)程:

    /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */ 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; }/** Prepares the socket addresses to attempt for the current proxy or host. */ private void resetNextInetSocketAddress(Proxy proxy) throws IOException {// Clear the addresses. Necessary if getAllByName() below throws!inetSocketAddresses = new ArrayList<>();String socketHost;int socketPort;if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {socketHost = address.url().host();socketPort = address.url().port();} else {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");}if (proxy.type() == Proxy.Type.SOCKS) {inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));} else {// Try each address for best behavior in mixed IPv4/IPv6 environments.List<InetAddress> addresses = address.dns().lookup(socketHost);for (int i = 0, size = addresses.size(); i < size; i++) {InetAddress inetAddress = addresses.get(i);inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));}}nextInetSocketAddressIndex = 0; }/** * Obtain a "host" from an {@link InetSocketAddress}. This returns a string containing either an * actual host name or a numeric IP address. */ // Visible for testing static String getHostString(InetSocketAddress socketAddress) {InetAddress address = socketAddress.getAddress();if (address == null) {// The InetSocketAddress was specified with a string (either a numeric IP or a host name). If// it is a name, all IPs for that name should be tried. If it is an IP address, only that IP// address should be tried.return socketAddress.getHostName();}// The InetSocketAddress has a specific address: we should only try that address. Therefore we// return the address and ignore any host name that may be available.return address.getHostAddress(); }

    收集一個(gè)特定代理服務(wù)器選擇下的 連接的目標(biāo)地址 因代理類型的不同而不同,這主要分為3種情況。 對(duì)于沒(méi)有配置代理的情況,會(huì)對(duì)HTTP服務(wù)器的域名進(jìn)行DNS域名解析,并為每個(gè)解析到的IP地址創(chuàng)建 連接的目標(biāo)地址;對(duì)于SOCKS代理,直接以HTTP服務(wù)器的域名及協(xié)議端口號(hào)創(chuàng)建 連接的目標(biāo)地址;而對(duì)于HTTP代理,則會(huì)對(duì)HTTP代理服務(wù)器的域名進(jìn)行DNS域名解析,并為每個(gè)解析到的IP地址創(chuàng)建 連接的目標(biāo)地址
    這里是OkHttp中發(fā)生DNS域名解析唯一的場(chǎng)合。對(duì)于使用代理的場(chǎng)景,沒(méi)有對(duì)HTTP服務(wù)器的域名做DNS域名解析,也就意味著HTTP服務(wù)器的域名解析要由代理服務(wù)器完成。
    代理服務(wù)器的收集是在創(chuàng)建 RouteSelector 完成的;而一個(gè)特定代理服務(wù)器選擇下的 連接的目標(biāo)地址 收集則是在選擇Route時(shí)根據(jù)需要完成的。

  • RouteSelector 做的第二件事情是選擇可用的路由。

    /** * Returns true if there's another route to attempt. Every address has at least one route. */ public boolean hasNext() { return hasNextInetSocketAddress()|| hasNextProxy()|| hasNextPostponed(); }public Route next() throws IOException { // Compute the next route to attempt. if (!hasNextInetSocketAddress()) {if (!hasNextProxy()) {if (!hasNextPostponed()) {throw new NoSuchElementException();}return nextPostponed();}lastProxy = nextProxy(); } lastInetSocketAddress = nextInetSocketAddress();Route route = new Route(address, lastProxy, lastInetSocketAddress); if (routeDatabase.shouldPostpone(route)) {postponedRoutes.add(route);// We will only recurse in order to skip previously failed routes. They will be tried last.return next(); }return route; }/** Returns true if there's another proxy to try. */ private boolean hasNextProxy() { return nextProxyIndex < proxies.size(); }/** Returns true if there's another socket address to try. */ private boolean hasNextInetSocketAddress() { return nextInetSocketAddressIndex < inetSocketAddresses.size(); }/** Returns the next socket address to try. */ private InetSocketAddress nextInetSocketAddress() throws IOException { if (!hasNextInetSocketAddress()) {throw new SocketException("No route to " + address.url().host()+ "; exhausted inet socket addresses: " + inetSocketAddresses); } return inetSocketAddresses.get(nextInetSocketAddressIndex++); }/** Returns true if there is another postponed route to try. */ private boolean hasNextPostponed() { return !postponedRoutes.isEmpty(); }/** Returns the next postponed route to try. */ private Route nextPostponed() { return postponedRoutes.remove(0); }

    RouteSelector 實(shí)現(xiàn)了兩級(jí)迭代器來(lái)提供選擇路由的服務(wù)。

  • 維護(hù)接失敗的路由的信息,以避免浪費(fèi)時(shí)間去連接一些不可用的路由。 RouteSelector 借助于RouteDatabase 維護(hù)失敗的路由的信息。

    /** * Clients should invoke this method when they encounter a connectivity failure on a connection * returned by this route selector. */ public void connectFailed(Route failedRoute, IOException failure) {if (failedRoute.proxy().type() != Proxy.Type.DIRECT && address.proxySelector() != null) {// Tell the proxy selector when we fail to connect on a fresh connection.address.proxySelector().connectFailed(address.url().uri(), failedRoute.proxy().address(), failure);}routeDatabase.failed(failedRoute); }

    RouteDatabase是一個(gè)簡(jiǎn)單的容器:

    public 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 failedRoute}. */ 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); } }

    代理選擇器ProxySelector的實(shí)現(xiàn)

    在OkHttp3中,ProxySelector對(duì)象由OkHttpClient維護(hù)。

  • public class OkHttpClient implements Cloneable, Call.Factory { ......final ProxySelector proxySelector;private OkHttpClient(Builder builder) {this.dispatcher = builder.dispatcher;this.proxy = builder.proxy;this.protocols = builder.protocols;this.connectionSpecs = builder.connectionSpecs;this.interceptors = Util.immutableList(builder.interceptors);this.networkInterceptors = Util.immutableList(builder.networkInterceptors);this.proxySelector = builder.proxySelector;......public ProxySelector proxySelector() {return proxySelector;}......public Builder() {dispatcher = new Dispatcher();protocols = DEFAULT_PROTOCOLS;connectionSpecs = DEFAULT_CONNECTION_SPECS;proxySelector = ProxySelector.getDefault();......Builder(OkHttpClient okHttpClient) {this.dispatcher = okHttpClient.dispatcher;this.proxy = okHttpClient.proxy;this.protocols = okHttpClient.protocols;this.connectionSpecs = okHttpClient.connectionSpecs;this.interceptors.addAll(okHttpClient.interceptors);this.networkInterceptors.addAll(okHttpClient.networkInterceptors);this.proxySelector = okHttpClient.proxySelector;

    在創(chuàng)建OkHttpClient時(shí),可以通過(guò)為OkHttpClient.Builder設(shè)置ProxySelector來(lái)定制ProxySelector。若沒(méi)有指定,則使用系統(tǒng)默認(rèn)的ProxySelector。OpenJDK 1.8版默認(rèn)的ProxySelector為sun.net.spi.DefaultProxySelector:

    public abstract class ProxySelector {/*** The system wide proxy selector that selects the proxy server to* use, if any, when connecting to a remote object referenced by* an URL.** @see #setDefault(ProxySelector)*/private static ProxySelector theProxySelector;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;}}/*** Gets the system-wide proxy selector.** @throws SecurityException* If a security manager has been installed and it denies* {@link NetPermission}{@code ("getProxySelector")}* @see #setDefault(ProxySelector)* @return the system-wide {@code ProxySelector}* @since 1.5*/public static ProxySelector getDefault() {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);}return theProxySelector;}

    在Android平臺(tái)上,默認(rèn)ProxySelector所用的則是另外的實(shí)現(xiàn):

    public abstract class ProxySelector {private static ProxySelector defaultSelector = new ProxySelectorImpl();/*** Returns the default proxy selector, or null if none exists.*/public static ProxySelector getDefault() {return defaultSelector;}/*** Sets the default proxy selector. If {@code selector} is null, the current* proxy selector will be removed.*/public static void setDefault(ProxySelector selector) {defaultSelector = selector;}

    Android平臺(tái)下,默認(rèn)的ProxySelector ProxySelectorImpl,其實(shí)現(xiàn) (不同Android版本實(shí)現(xiàn)不同,這里以android-6.0.1_r61為例) 如下:

    package java.net; import java.io.IOException; import java.util.Collections; import java.util.List; final class ProxySelectorImpl extends ProxySelector {@Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {if (uri == null || sa == null || ioe == null) {throw new IllegalArgumentException();}}@Override public List<Proxy> select(URI uri) {return Collections.singletonList(selectOneProxy(uri));}private Proxy selectOneProxy(URI uri) {if (uri == null) {throw new IllegalArgumentException("uri == null");}String scheme = uri.getScheme();if (scheme == null) {throw new IllegalArgumentException("scheme == null");}int port = -1;Proxy proxy = null;String nonProxyHostsKey = null;boolean httpProxyOkay = true;if ("http".equalsIgnoreCase(scheme)) {port = 80;nonProxyHostsKey = "http.nonProxyHosts";proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);} else if ("https".equalsIgnoreCase(scheme)) {port = 443;nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support thisproxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);} else if ("ftp".equalsIgnoreCase(scheme)) {port = 80; // not 21 as you might guessnonProxyHostsKey = "ftp.nonProxyHosts";proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);} else if ("socket".equalsIgnoreCase(scheme)) {httpProxyOkay = false;} else {return Proxy.NO_PROXY;}if (nonProxyHostsKey != null&& isNonProxyHost(uri.getHost(), System.getProperty(nonProxyHostsKey))) {return Proxy.NO_PROXY;}if (proxy != null) {return proxy;}if (httpProxyOkay) {proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);if (proxy != null) {return proxy;}}proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);if (proxy != null) {return proxy;}return Proxy.NO_PROXY;}/*** Returns the proxy identified by the {@code hostKey} system property, or* null.*/private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {String host = System.getProperty(hostKey);if (host == null || host.isEmpty()) {return null;}int port = getSystemPropertyInt(portKey, defaultPort);return new Proxy(type, InetSocketAddress.createUnresolved(host, port));}private int getSystemPropertyInt(String key, int defaultValue) {String string = System.getProperty(key);if (string != null) {try {return Integer.parseInt(string);} catch (NumberFormatException ignored) {}}return defaultValue;}/*** Returns true if the {@code nonProxyHosts} system property pattern exists* and matches {@code host}.*/private boolean isNonProxyHost(String host, String nonProxyHosts) {if (host == null || nonProxyHosts == null) {return false;}// construct patternStringBuilder patternBuilder = new StringBuilder();for (int i = 0; i < nonProxyHosts.length(); i++) {char c = nonProxyHosts.charAt(i);switch (c) {case '.':patternBuilder.append("\\.");break;case '*':patternBuilder.append(".*");break;default:patternBuilder.append(c);}}// check whether the host is the nonProxyHosts.String pattern = patternBuilder.toString();return host.matches(pattern);} }

    在Android平臺(tái)上,主要是從系統(tǒng)屬性System properties中獲取代理服務(wù)器的配置信息,這里會(huì)過(guò)濾掉不能進(jìn)行代理的主機(jī)的訪問(wèn)。

    前面我們看到 RouteSelector 通過(guò) Address 提供的Proxy和ProxySelector來(lái)收集Proxy信息及連接的目標(biāo)地址信息。OkHttp3中用 Address 描述建立連接所需的配置信息,包括HTTP服務(wù)器的地址,DNS,SocketFactory,Proxy,ProxySelector及TLS所需的一些設(shè)施等等:

    public final class Address {final HttpUrl url;final Dns dns;final SocketFactory socketFactory;final Authenticator proxyAuthenticator;final List<Protocol> protocols;final List<ConnectionSpec> connectionSpecs;final ProxySelector proxySelector;final Proxy proxy;final SSLSocketFactory sslSocketFactory;final HostnameVerifier hostnameVerifier;final CertificatePinner certificatePinner;public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,CertificatePinner certificatePinner, Authenticator proxyAuthenticator, Proxy proxy,List<Protocol> protocols, List<ConnectionSpec> connectionSpecs, ProxySelector proxySelector) {this.url = new HttpUrl.Builder().scheme(sslSocketFactory != null ? "https" : "http").host(uriHost).port(uriPort).build();if (dns == null) throw new NullPointerException("dns == null");this.dns = dns;if (socketFactory == null) throw new NullPointerException("socketFactory == null");this.socketFactory = socketFactory;if (proxyAuthenticator == null) {throw new NullPointerException("proxyAuthenticator == null");}this.proxyAuthenticator = proxyAuthenticator;if (protocols == null) throw new NullPointerException("protocols == null");this.protocols = Util.immutableList(protocols);if (connectionSpecs == null) throw new NullPointerException("connectionSpecs == null");this.connectionSpecs = Util.immutableList(connectionSpecs);if (proxySelector == null) throw new NullPointerException("proxySelector == null");this.proxySelector = proxySelector;this.proxy = proxy;this.sslSocketFactory = sslSocketFactory;this.hostnameVerifier = hostnameVerifier;this.certificatePinner = certificatePinner;}/*** Returns a URL with the hostname and port of the origin server. The path, query, and fragment of* this URL are always empty, since they are not significant for planning a route.*/public HttpUrl url() {return url;}/** Returns the service that will be used to resolve IP addresses for hostnames. */public Dns dns() {return dns;}/** Returns the socket factory for new connections. */public SocketFactory socketFactory() {return socketFactory;}/** Returns the client's proxy authenticator. */public Authenticator proxyAuthenticator() {return proxyAuthenticator;}/*** Returns the protocols the client supports. This method always returns a non-null list that* contains minimally {@link Protocol#HTTP_1_1}.*/public List<Protocol> protocols() {return protocols;}public List<ConnectionSpec> connectionSpecs() {return connectionSpecs;}/*** Returns this address's proxy selector. Only used if the proxy is null. If none of this* selector's proxies are reachable, a direct connection will be attempted.*/public ProxySelector proxySelector() {return proxySelector;}/*** Returns this address's explicitly-specified HTTP proxy, or null to delegate to the {@linkplain* #proxySelector proxy selector}.*/public Proxy proxy() {return proxy;}/** Returns the SSL socket factory, or null if this is not an HTTPS address. */public SSLSocketFactory sslSocketFactory() {return sslSocketFactory;}/** Returns the hostname verifier, or null if this is not an HTTPS address. */public HostnameVerifier hostnameVerifier() {return hostnameVerifier;}/** Returns this address's certificate pinner, or null if this is not an HTTPS address. */public CertificatePinner certificatePinner() {return certificatePinner;}...... }

    OkHttp3中通過(guò)職責(zé)鏈執(zhí)行HTTP請(qǐng)求。在其中的RetryAndFollowUpInterceptor里創(chuàng)建Address對(duì)象時(shí),從OkHttpClient對(duì)象獲取ProxySelector。Address對(duì)象會(huì)被用于創(chuàng)建StreamAllocation對(duì)象。StreamAllocation在建立連接時(shí),從Address對(duì)象中獲取ProxySelector以選擇路由。

    public final class RetryAndFollowUpInterceptor implements Interceptor { ......private Address createAddress(HttpUrl url) {SSLSocketFactory sslSocketFactory = null;HostnameVerifier hostnameVerifier = null;CertificatePinner certificatePinner = null;if (url.isHttps()) {sslSocketFactory = client.sslSocketFactory();hostnameVerifier = client.hostnameVerifier();certificatePinner = client.certificatePinner();}return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());}

    在StreamAllocation中,Address對(duì)象會(huì)被用于創(chuàng)建 RouteSelector 對(duì)象:

    public final class StreamAllocation { ......public StreamAllocation(ConnectionPool connectionPool, Address address) {this.connectionPool = connectionPool;this.address = address;this.routeSelector = new RouteSelector(address, routeDatabase());}

    代理協(xié)議

    如我們?cè)?OkHttp3 HTTP請(qǐng)求執(zhí)行流程分析 中看到的,OkHttp3對(duì)HTTP請(qǐng)求是通過(guò)Interceptor鏈來(lái)處理的。
    RetryAndFollowUpInterceptor創(chuàng)建StreamAllocation對(duì)象,處理http的重定向及出錯(cuò)重試。對(duì)后續(xù)Interceptor的執(zhí)行的影響為修改Request并創(chuàng)建StreamAllocation對(duì)象。
    BridgeInterceptor補(bǔ)全缺失的一些http header。對(duì)后續(xù)Interceptor的執(zhí)行的影響主要為修改了Request。
    CacheInterceptor處理http緩存。對(duì)后續(xù)Interceptor的執(zhí)行的影響為,若緩存中有所需請(qǐng)求的響應(yīng),則后續(xù)Interceptor不再執(zhí)行。
    ConnectInterceptor借助于前面分配的StreamAllocation對(duì)象建立與服務(wù)器之間的連接,并選定交互所用的協(xié)議是HTTP 1.1還是HTTP 2。對(duì)后續(xù)Interceptor的執(zhí)行的影響為,創(chuàng)建了HttpStream和connection。
    CallServerInterceptor作為Interceptor鏈中的最后一個(gè)Interceptor,用于處理IO,與服務(wù)器進(jìn)行數(shù)據(jù)交換。

    在OkHttp3中,收集的路由信息,是在ConnectInterceptor中建立連接時(shí)用到的。ConnectInterceptor 借助于 StreamAllocation 完成整個(gè)連接的建立,包括TCP連接建立,代理協(xié)議所要求的協(xié)商,以及SSL/TLS協(xié)議的協(xié)商,如ALPN等。我們暫時(shí)略過(guò)整個(gè)連接建立的完整過(guò)程,主要關(guān)注TCP連接建立及代理協(xié)議的協(xié)商過(guò)程的部分。

    StreamAllocation 的findConnection()用來(lái)為某次特定的網(wǎng)絡(luò)請(qǐng)求尋找一個(gè)可用的連接。

    /*** Returns a connection to host a new stream. This prefers the existing connection if it exists,* then the pool, finally building a new connection.*/private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,boolean connectionRetryEnabled) throws IOException {Route selectedRoute;synchronized (connectionPool) {if (released) throw new IllegalStateException("released");if (codec != null) throw new IllegalStateException("codec != null");if (canceled) throw new IOException("Canceled");RealConnection allocatedConnection = this.connection;if (allocatedConnection != null && !allocatedConnection.noNewStreams) {return allocatedConnection;}// Attempt to get a connection from the pool.RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);if (pooledConnection != null) {this.connection = pooledConnection;return pooledConnection;}selectedRoute = route;}if (selectedRoute == null) {selectedRoute = routeSelector.next();synchronized (connectionPool) {route = selectedRoute;refusedStreamCount = 0;}}RealConnection newConnection = new RealConnection(selectedRoute);acquire(newConnection);synchronized (connectionPool) {Internal.instance.put(connectionPool, newConnection);this.connection = newConnection;if (canceled) throw new IOException("Canceled");}newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),connectionRetryEnabled);routeDatabase().connected(newConnection.route());return newConnection;}

    OkHttp3中有一套連接池的機(jī)制,這里先嘗試從連接池中尋找可用的連接,找不到時(shí)才會(huì)新建連接。新建連接的過(guò)程是:

  • 選擇一個(gè)Route;
  • 創(chuàng)建 RealConnection 連接對(duì)象。
  • 將連接對(duì)象保存進(jìn)連接池中。
  • 建立連接。
  • RealConnection 中建立連接的過(guò)程是這樣的:

    public final class RealConnection extends Http2Connection.Listener implements Connection {private final Route route;/** The low-level TCP socket. */private Socket rawSocket;/*** The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or* {@link #rawSocket} itself if this connection does not use SSL.*/public Socket socket;private Handshake handshake;private Protocol protocol;public volatile Http2Connection http2Connection;public int successCount;public BufferedSource source;public BufferedSink sink;public int allocationLimit;public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();public boolean noNewStreams;public long idleAtNanos = Long.MAX_VALUE;public RealConnection(Route route) {this.route = route;}public void connect(int connectTimeout, int readTimeout, int writeTimeout,List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) {if (protocol != null) throw new IllegalStateException("already connected");RouteException routeException = null;ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);if (route.address().sslSocketFactory() == null) {if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {throw new RouteException(new UnknownServiceException("CLEARTEXT communication not enabled for client"));}String host = route.address().url().host();if (!Platform.get().isCleartextTrafficPermitted(host)) {throw new RouteException(new UnknownServiceException("CLEARTEXT communication to " + host + " not permitted by network security policy"));}}while (protocol == null) {try {if (route.requiresTunnel()) {buildTunneledConnection(connectTimeout, readTimeout, writeTimeout,connectionSpecSelector);} else {buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);}} catch (IOException e) {closeQuietly(socket);closeQuietly(rawSocket);socket = null;rawSocket = null;source = null;sink = null;handshake = null;protocol = null;if (routeException == null) {routeException = new RouteException(e);} else {routeException.addConnectException(e);}if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {throw routeException;}}}}

    在這個(gè)方法中,SSLSocketFactory為空,也就是要求請(qǐng)求/響應(yīng)明文傳輸時(shí),先做安全性檢查,以確認(rèn)系統(tǒng)允許明文傳輸,允許以請(qǐng)求的域名做明文傳輸。

    然后根據(jù)路由的具體情況,執(zhí)行不同的連接建立過(guò)程。對(duì)于需要?jiǎng)?chuàng)建隧道連接的路由,執(zhí)行buildTunneledConnection(),對(duì)于其它情況,則執(zhí)行buildConnection()。

    判斷是否要建立隧道連接的依據(jù)是代理的類型,以及連接的類型:

    /*** Returns true if this route tunnels HTTPS through an HTTP proxy. See <a* href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.*/public boolean requiresTunnel() {return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;}

    如果是HTTP代理,且請(qǐng)求建立SSL/TLS加密通道 (http/1.1的https和http2) ,則需要建立隧道連接。其它情形不需要建立隧道連接。

    非隧道連接的建立

    非隧道連接的建立過(guò)程為:

    /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,ConnectionSpecSelector connectionSpecSelector) throws IOException {connectSocket(connectTimeout, readTimeout);establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);}private void connectSocket(int connectTimeout, int readTimeout) throws IOException {Proxy proxy = route.proxy();Address address = route.address();rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP? address.socketFactory().createSocket(): new Socket(proxy);rawSocket.setSoTimeout(readTimeout);try {Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);} catch (ConnectException e) {throw new ConnectException("Failed to connect to " + route.socketAddress());}source = Okio.buffer(Okio.source(rawSocket));sink = Okio.buffer(Okio.sink(rawSocket));}

    有 3 種情況需要建立非隧道連接:

  • 無(wú)代理。
  • 明文的HTTP代理。
  • SOCKS代理。
  • 非隧道連接的建立過(guò)程為建立TCP連接,然后在需要時(shí)完成SSL/TLS的握手及HTTP/2的握手建立Protocol。建立TCP連接的過(guò)程為:

  • 創(chuàng)建Socket。非SOCKS代理的情況下,通過(guò)SocketFactory創(chuàng)建;在SOCKS代理則傳入proxy手動(dòng)new一個(gè)出來(lái)。
  • 為Socket設(shè)置讀超時(shí)。
  • 完成特定于平臺(tái)的連接建立。
  • 創(chuàng)建用語(yǔ)IO的source和sink。
  • AndroidPlatformconnectSocket() 是這樣的:

    @Override public void connectSocket(Socket socket, InetSocketAddress address,int connectTimeout) throws IOException {try {socket.connect(address, connectTimeout);} catch (AssertionError e) {if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);throw e;} catch (SecurityException e) {// Before android 4.3, socket.connect could throw a SecurityException// if opening a socket resulted in an EACCES error.IOException ioException = new IOException("Exception in connect");ioException.initCause(e);throw ioException;}}

    設(shè)置了SOCKS代理的情況下,僅有的特別之處在于,是通過(guò)傳入proxy手動(dòng)創(chuàng)建的Socket。route的socketAddress包含著目標(biāo)HTTP服務(wù)器的域名。由此可見(jiàn)SOCKS協(xié)議的處理,主要是在Java標(biāo)準(zhǔn)庫(kù)的 java.net.Socket 中處理的。對(duì)于外界而言,就好像是于HTTP服務(wù)器直接建立連接一樣,因?yàn)檫B接時(shí)傳入的地址都是HTTP服務(wù)器的域名。

    而對(duì)于明文HTTP代理的情況下,這里沒(méi)有任何特殊的處理。route的socketAddress包含著代理服務(wù)器的IP地址。HTTP代理自身會(huì)根據(jù)請(qǐng)求及響應(yīng)的實(shí)際內(nèi)容,建立與HTTP服務(wù)器的TCP連接,并轉(zhuǎn)發(fā)數(shù)據(jù)。猜測(cè)HTTP代理服務(wù)器是根據(jù)HTTP請(qǐng)求中的"Host"等header內(nèi)容來(lái)確認(rèn)HTTP服務(wù)器地址的。

    暫時(shí)先略過(guò)對(duì)建立協(xié)議過(guò)程的分析。

    HTTP代理的隧道連接

    buildTunneledConnection()用于建立隧道連接:

    /*** Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a* proxy server can issue an auth challenge and then close the connection.*/private void buildTunneledConnection(int connectTimeout, int readTimeout, int writeTimeout,ConnectionSpecSelector connectionSpecSelector) throws IOException {Request tunnelRequest = createTunnelRequest();HttpUrl url = tunnelRequest.url();int attemptedConnections = 0;int maxAttempts = 21;while (true) {if (++attemptedConnections > maxAttempts) {throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);}connectSocket(connectTimeout, readTimeout);tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);if (tunnelRequest == null) break; // Tunnel successfully created.// The proxy decided to close the connection after an auth challenge. We need to create a new// connection, but this time with the auth credentials.closeQuietly(rawSocket);rawSocket = null;sink = null;source = null;}establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);}

    這里主要是兩個(gè)過(guò)程:

  • 建立隧道連接。
  • 建立Protocol。
  • 建立隧道連接的過(guò)程又分為幾個(gè)步驟:

    • 創(chuàng)建隧道請(qǐng)求
    • 建立Socket連接
    • 發(fā)送請(qǐng)求建立隧道

    隧道請(qǐng)求是一個(gè)常規(guī)的HTTP請(qǐng)求,只是請(qǐng)求的內(nèi)容有點(diǎn)特殊。最初創(chuàng)建的隧道請(qǐng)求如:

    /*** Returns a request that creates a TLS tunnel via an HTTP proxy. Everything in the tunnel request* is sent unencrypted to the proxy server, so tunnels include only the minimum set of headers.* This avoids sending potentially sensitive data like HTTP cookies to the proxy unencrypted.*/private Request createTunnelRequest() {return new Request.Builder().url(route.address().url()).header("Host", Util.hostHeader(route.address().url(), true)).header("Proxy-Connection", "Keep-Alive").header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid..build();}

    一個(gè)隧道請(qǐng)求的例子如下:


    Tunnel Request

    請(qǐng)求的"Host" header中包含了目標(biāo)HTTP服務(wù)器的域名。建立socket連接的過(guò)程這里不再贅述。

    創(chuàng)建隧道的過(guò)程是這樣子的:

    /*** To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create* the proxy connection. This may need to be retried if the proxy requires authorization.*/private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,HttpUrl url) throws IOException {// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";while (true) {Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);source.timeout().timeout(readTimeout, MILLISECONDS);sink.timeout().timeout(writeTimeout, MILLISECONDS);tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);tunnelConnection.finishRequest();Response response = tunnelConnection.readResponse().request(tunnelRequest).build();// The response body from a CONNECT should be empty, but if it is not then we should consume// it before proceeding.long contentLength = HttpHeaders.contentLength(response);if (contentLength == -1L) {contentLength = 0L;}Source body = tunnelConnection.newFixedLengthSource(contentLength);Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);body.close();switch (response.code()) {case HTTP_OK:// Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If// that happens, then we will have buffered bytes that are needed by the SSLSocket!// This check is imperfect: it doesn't tell us whether a handshake will succeed, just// that it will almost certainly fail because the proxy has sent unexpected data.if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {throw new IOException("TLS tunnel buffered too many bytes!");}return null;case HTTP_PROXY_AUTH:tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");if ("close".equalsIgnoreCase(response.header("Connection"))) {return tunnelRequest;}break;default:throw new IOException("Unexpected response code for CONNECT: " + response.code());}}}

    在前面創(chuàng)建的TCP連接之上,完成與代理服務(wù)器的HTTP請(qǐng)求/響應(yīng)交互。請(qǐng)求的內(nèi)容類似下面這樣:

    "CONNECT m.taobao.com:443 HTTP/1.1"

    這里可能會(huì)根據(jù)HTTP代理是否需要認(rèn)證而有多次HTTP請(qǐng)求/響應(yīng)交互。

    總結(jié)一下OkHttp3中代理相關(guān)的處理:

  • 沒(méi)有設(shè)置代理的情況下,直接與HTTP服務(wù)器建立TCP連接,然后進(jìn)行HTTP請(qǐng)求/響應(yīng)的交互。
  • 設(shè)置了SOCKS代理的情況下,創(chuàng)建Socket時(shí),為其傳入proxy,連接時(shí)還是以HTTP服務(wù)器為目標(biāo)地址。在標(biāo)準(zhǔn)庫(kù)的Socket中完成SOCKS協(xié)議相關(guān)的處理。此時(shí)基本上感知不到代理的存在。
  • 設(shè)置了HTTP代理時(shí)的HTTP請(qǐng)求,與HTTP代理服務(wù)器建立TCP連接。HTTP代理服務(wù)器解析HTTP請(qǐng)求/響應(yīng)的內(nèi)容,并根據(jù)其中的信息來(lái)完成數(shù)據(jù)的轉(zhuǎn)發(fā)。也就是說(shuō),如果HTTP請(qǐng)求中不包含"Host" header,則有可能在設(shè)置了HTTP代理的情況下無(wú)法與HTTP服務(wù)器建立連接。
  • 設(shè)置了HTTP代理時(shí)的HTTPS/HTTP2請(qǐng)求,與HTTP服務(wù)器建立通過(guò)HTTP代理的隧道連接。HTTP代理不再解析傳輸?shù)臄?shù)據(jù),僅僅完成數(shù)據(jù)轉(zhuǎn)發(fā)的功能。此時(shí)HTTP代理的功能退化為如同SOCKS代理類似。
  • 設(shè)置了代理時(shí),HTTP服務(wù)器的域名解析會(huì)被交給代理服務(wù)器執(zhí)行。其中設(shè)置了HTTP代理時(shí),會(huì)對(duì)HTTP代理的域名做域名解析。
  • 關(guān)于HTTP代理的更多內(nèi)容,可以參考HTTP 代理原理及實(shí)現(xiàn)(一)。

    OkHttp3中代理相關(guān)的處理大體如此。

    總結(jié)

    以上是生活随笔為你收集整理的OkHttp3中的代理与路由的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。