OkHttp3中的代理与路由
HTTP請(qǐng)求的整體處理過(guò)程大體可以理解為,
然而,當(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ò)程:
收集一個(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ù)。
在創(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ò)程是:
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 種情況需要建立非隧道連接:
非隧道連接的建立過(guò)程為建立TCP連接,然后在需要時(shí)完成SSL/TLS的握手及HTTP/2的握手建立Protocol。建立TCP連接的過(guò)程為:
AndroidPlatform 的 connectSocket() 是這樣的:
@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ò)程:
建立隧道連接的過(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)的處理:
關(guān)于HTTP代理的更多內(nèi)容,可以參考HTTP 代理原理及實(shí)現(xiàn)(一)。
OkHttp3中代理相關(guān)的處理大體如此。
總結(jié)
以上是生活随笔為你收集整理的OkHttp3中的代理与路由的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 用FlatBuffers提升Androi
- 下一篇: 使用QUIC