路由是什麼呢?路由便是網絡數據包在網絡中的傳輸路徑,或者說數據包在傳輸過程當中所通過的網絡節點,好比路由器,代理服務器之類的。html
那像OkHttp3這樣的網絡庫對於數據包的路由須要作些什麼事呢?用戶能夠爲終端設置代理服務器,HTTP/HTTPS代理或SOCK代理。OkHttp3中的路由相關邏輯,須要從系統中獲取用戶設置的代理服務器的地址,將HTTP請求轉換爲代理協議的數據包,發給代理服務器,而後等待代理服務器幫助完成了網絡請求以後,從代理服務器讀取響應數據返回給用戶。只有這樣,用戶設置的代理才能生效。若是網絡庫無視用戶設置的代理服務器,直接進行DNS並作網絡請求,則用戶設置的代理服務器不生效。java
這裏就來看一下OkHttp3中路由相關的處理。android
如同Internet上的其它設備同樣,每一個路由節點都有本身的IP地址,加上端口號,則能夠肯定惟一的路由服務。以域名描述的HTTP/HTTPS代理服務器地址,可能對應於多個實際的代理服務器主機,於是一個代理服務器可能包含有多條路由。而SOCK代理服務器,則有着惟一肯定的IP地址和端口號。web
OkHttp3藉助於RouteSelector來選擇路由節點,並維護路由的信息。緩存
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()); } /** * 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; } /** * 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); } /** 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; } /** Returns true if there's another proxy to try. */ private boolean hasNextProxy() { return nextProxyIndex < proxies.size(); } /** 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(); } /** 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
主要作了這樣一些事情:服務器
RouteSelector
對象建立時,獲取並保存用戶設置的全部的代理。這裏主要經過ProxySelector
,根據uri來獲得系統中的全部代理,並保存在Proxy列表proxies中。RouteSelector
中維護的下一個可用路由。調用者在鏈接失敗時,能夠再次調用這個接口來獲取下一個路由。這個接口會逐個地返回每一個代理的每一個代理主機服務給調用者。在全部的代理的每一個代理主機都被訪問過了以後,還會返回曾經鏈接失敗的路由。RouteDatabase
用於維護鏈接失敗的路由的信息,以免浪費時間去鏈接一些不可用的路由。RouteDatabase
中的路由信息主要由RouteSelector
來維護。RouteDatabase
是一個簡單的容器:cookie
package okhttp3.internal.connection; import java.util.LinkedHashSet; import java.util.Set; import okhttp3.Route; /** * A blacklist of failed routes to avoid when creating a new connection to a target address. This is * used so that OkHttp can learn from its mistakes: if there was a failure attempting to connect to * a specific IP address or proxy server, that failure is remembered and alternate routes are * preferred. */ 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); } }
OkHttp3主要用(Address, Proxy, InetSocketAddress)的三元組來描述路由信息:網絡
package okhttp3; import java.net.InetSocketAddress; import java.net.Proxy; /** * The concrete route used by a connection to reach an abstract origin server. When creating a * connection the client has many options: * * <ul> * <li><strong>HTTP proxy:</strong> a proxy server may be explicitly configured for the client. * Otherwise the {@linkplain java.net.ProxySelector proxy selector} is used. It may return * multiple proxies to attempt. * <li><strong>IP address:</strong> whether connecting directly to an origin server or a proxy, * opening a socket requires an IP address. The DNS server may return multiple IP addresses * to attempt. * </ul> * * <p>Each route is a specific selection of these options. */ 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; } @Override public boolean equals(Object obj) { if (obj instanceof Route) { Route other = (Route) obj; return address.equals(other.address) && proxy.equals(other.proxy) && inetSocketAddress.equals(other.inetSocketAddress); } return false; } @Override public int hashCode() { int result = 17; result = 31 * result + address.hashCode(); result = 31 * result + proxy.hashCode(); result = 31 * result + inetSocketAddress.hashCode(); return result; } }
在StreamAllocation中創建鏈接時,會經過RouteSelector
獲取可用路由。app
在OkHttp3中,ProxySelector
對象主要由OkHttpClient維護。socket
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;
在建立OkHttpClient時,能夠經過爲OkHttpClient.Builder設置ProxySelector
來定製ProxySelector
。若沒有指定,則全部的爲默認ProxySelector
。OpenJDK 1.8版默認的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平臺上,默認ProxySelector
所用的則是另外的實現:
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平臺下,默認的ProxySelector
ProxySelectorImpl,其實現(不一樣版本的Android,實現不一樣,這裏是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 this proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port); } else if ("ftp".equalsIgnoreCase(scheme)) { port = 80; // not 21 as you might guess nonProxyHostsKey = "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 pattern StringBuilder 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平臺上,主要是從System properties中獲取的代理服務器的主機及其端口號,會過濾掉不能進行代理的主機的訪問。
回到OkHttp中,在RetryAndFollowUpInterceptor中,建立Address對象時,從OkHttpClient對象獲取ProxySelector。Address對象會被用於建立StreamAllocation對象,StreamAllocation在創建鏈接時,從Address對象中獲取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()); }
OkHttp3發送給HTTP代理服務器的HTTP請求,與直接發送給HTTP服務器的HTTP請求有什麼樣的區別呢,仍是說二者其實毫無差異呢?也就是HTTP代理的協議是什麼樣的呢?這裏咱們就經過對代碼進行分析來仔細地看一下。
如咱們在OkHttp3 HTTP請求執行流程分析中看到的,OkHttp3對HTTP請求是經過Interceptor鏈來處理的。 RetryAndFollowUpInterceptor
建立StreamAllocation
對象,處理http的重定向及出錯重試。對後續Interceptor的執行的影響爲修改Request並建立StreamAllocation對象。 BridgeInterceptor
補全缺失的一些http header。對後續Interceptor的執行的影響主要爲修改了Request。 CacheInterceptor
處理http緩存。對後續Interceptor的執行的影響爲,若緩存中有所需請求的響應,則後續Interceptor再也不執行。 ConnectInterceptor
藉助於前面分配的StreamAllocation
對象創建與服務器之間的鏈接,並選定交互所用的協議是HTTP 1.1仍是HTTP 2。對後續Interceptor的執行的影響爲,建立了HttpStream和connection。 CallServerInterceptor
做爲Interceptor鏈中的最後一個Interceptor,用於處理IO,與服務器進行數據交換。
OkHttp3對代理的處理是在ConnectInterceptor
和CallServerInterceptor
中完成的。再來看ConnectInterceptor
的定義:
package okhttp3.internal.connection; import java.io.IOException; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.internal.http.HttpCodec; import okhttp3.internal.http.RealInterceptorChain; /** Opens a connection to the target server and proceeds to the next interceptor. */ public final class ConnectInterceptor implements Interceptor { public final OkHttpClient client; public ConnectInterceptor(OkHttpClient client) { this.client = client; } @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } }
ConnectInterceptor
利用前面的Interceptor建立的StreamAllocation對象,建立stream HttpCodec,以及RealConnection connection。而後把這些對象傳給鏈中後繼的Interceptor,也就是CallServerInterceptor
處理。
爲了釐清StreamAllocation的兩個操做的詳細執行過程,這裏再回過頭來看一下StreamAllocation
對象的建立。StreamAllocation對象在RetryAndFollowUpInterceptor
中建立:
@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); streamAllocation = new StreamAllocation( client.connectionPool(), createAddress(request.url()), callStackTrace);
建立StreamAllocation
對象時,傳入的ConnectionPool來自於OkHttpClient,建立的Address主要用於描述HTTP服務的目標地址相關的信息。
public final class StreamAllocation { public final Address address; private Route route; private final ConnectionPool connectionPool; private final Object callStackTrace; // State guarded by connectionPool. private final RouteSelector routeSelector; private int refusedStreamCount; private RealConnection connection; private boolean released; private boolean canceled; private HttpCodec codec; public StreamAllocation(ConnectionPool connectionPool, Address address, Object callStackTrace) { this.connectionPool = connectionPool; this.address = address; this.routeSelector = new RouteSelector(address, routeDatabase()); this.callStackTrace = callStackTrace; }
建立StreamAllocation
對象時,除了建立RouteSelector
以外,並無其它特別的地方。
而後來看ConnectInterceptor
中用來建立HttpCodec的newStream()方法:
public final class StreamAllocation { ...... public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) { int connectTimeout = client.connectTimeoutMillis(); int readTimeout = client.readTimeoutMillis(); int writeTimeout = client.writeTimeoutMillis(); boolean connectionRetryEnabled = client.retryOnConnectionFailure(); try { RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks); HttpCodec resultCodec; if (resultConnection.http2Connection != null) { resultCodec = new Http2Codec(client, this, resultConnection.http2Connection); } else { resultConnection.socket().setSoTimeout(readTimeout); resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS); resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS); resultCodec = new Http1Codec( client, this, resultConnection.source, resultConnection.sink); } synchronized (connectionPool) { codec = resultCodec; return resultCodec; } } catch (IOException e) { throw new RouteException(e); } }
這個方法的執行流程爲:
而後來看findHealthyConnection()
中建立鏈接的過程:
/** * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated * until a healthy connection is found. */ private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException { while (true) { RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled); // If this is a brand new connection, we can skip the extensive health checks. synchronized (connectionPool) { if (candidate.successCount == 0) { return candidate; } } // Do a (potentially slow) check to confirm that the pooled connection is still good. If it // isn't, take it out of the pool and start again. if (!candidate.isHealthy(doExtensiveHealthChecks)) { noNewStreams(); continue; } return candidate; } }
在這個方法中,是找到一個鏈接,而後判斷其是否可用。若是可用則將找到的鏈接返回給調用者,不然尋找下一個鏈接。尋找鏈接多是創建一個新的鏈接,也多是複用鏈接池中的一個鏈接。
接着來看尋找鏈接的過程findConnection()
:
/** * 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); synchronized (connectionPool) { acquire(newConnection); 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; }
這個過程大致爲:
get(Address address, StreamAllocation streamAllocation)
方法來嘗試獲取RealConnection。 若能從鏈接池中找到所須要的鏈接,則將鏈接返回給調用者。這裏再來看一下在ConnectionPool的get()操做執行的過程:
private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning; /** Returns a recycled connection to {@code address}, or null if no such connection exists. */ RealConnection get(Address address, StreamAllocation streamAllocation) { assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address) && !connection.noNewStreams) { streamAllocation.acquire(connection); return connection; } } return null; }
ConnectionPool鏈接池是鏈接的容器,這裏用了一個Deque來保存全部的鏈接RealConnection。而get的過程就是,遍歷保存的全部鏈接來匹配address。同時connection.allocations.size()要知足connection.allocationLimit的限制。 在找到了所須要的鏈接以後,會acquire該鏈接。
acquire鏈接的過程又是什麼樣的呢?
public final class StreamAllocation { ...... /** * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to * {@link #release} on the same connection. */ public void acquire(RealConnection connection) { assert (Thread.holdsLock(connectionPool)); connection.allocations.add(new StreamAllocationReference(this, callStackTrace)); }
基本上就是給RealConnection的allocations添加一個到該StreamAllocation的引用。這樣看來,同一個鏈接RealConnection彷佛同時能夠爲多個HTTP請求服務。而咱們知道,多個HTTP/1.1請求是不能在同一個鏈接上交叉處理的。那這又是怎麼回事呢?
咱們來看connection.allocationLimit的更新設置。RealConnection中以下的兩個地方會設置這個值:
public final class RealConnection extends Http2Connection.Listener implements Connection { ...... private void establishProtocol(int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException { if (route.address().sslSocketFactory() != null) { connectTls(readTimeout, writeTimeout, connectionSpecSelector); } else { protocol = Protocol.HTTP_1_1; socket = rawSocket; } if (protocol == Protocol.HTTP_2) { socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. Http2Connection http2Connection = new Http2Connection.Builder(true) .socket(socket, route.address().url().host(), source, sink) .listener(this) .build(); http2Connection.start(); // Only assign the framed connection once the preface has been sent successfully. this.allocationLimit = http2Connection.maxConcurrentStreams(); this.http2Connection = http2Connection; } else { this.allocationLimit = 1; } } /** When settings are received, adjust the allocation limit. */ @Override public void onSettings(Http2Connection connection) { allocationLimit = connection.maxConcurrentStreams(); }
能夠看到,若不是HTTP/2的鏈接,則allocationLimit的值老是1。因而可知,StreamAllocation以及RealConnection的allocations/allocationLimit這樣的設計,主要是爲了實現HTTP/2 multi stream的特性。不然的話,大概爲RealConnection用一個inUse標記就能夠了。 那
回到StreamAllocation的findConnection()
,來看新建立的RealConnection對象創建鏈接的過程,即RealConnection的connect():
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; } } } }
根據路由的類型,來執行不一樣的建立鏈接的過程。對於須要建立隧道鏈接的路由,執行buildTunneledConnection(),而對於普通鏈接,則執行buildConnection()。
如何判斷是否要創建隧道鏈接呢?來看
/** * 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; }
能夠看到,經過代理服務器,來作https請求的鏈接(http/1.1的https和http2)須要創建隧道鏈接,而其它的鏈接則不須要創建隧道鏈接。
用於創建隧道鏈接的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); }
基本上是兩個過程:
創建隧道鏈接的過程,又分爲了幾個過程:
隧道請求是一個常規的HTTP請求,只是請求的內容有點特殊。初始的隧道請求如:
/** * 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(); }
創建socket鏈接的過程以下:
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)); }
主要是建立一個到代理服務器或HTTP服務器的Socket鏈接。socketFactory最終來自於OkHttpClient,對於OpenJDK 8而言,默認爲DefaultSocketFactory:
/** * Returns a copy of the environment's default socket factory. * * @return the default <code>SocketFactory</code> */ public static SocketFactory getDefault() { synchronized (SocketFactory.class) { if (theFactory == null) { // // Different implementations of this method SHOULD // work rather differently. For example, driving // this from a system property, or using a different // implementation than JavaSoft's. // theFactory = new DefaultSocketFactory(); } } return theFactory; }
建立隧道的過程是這樣子的:
/** * 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()); } } }
主要HTTP 的 CONNECT 方法創建隧道。
而創建常規的鏈接的過程則爲:
/** 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); }
創建socket鏈接,而後創建Protocol。創建Protocol的過程爲:
private void establishProtocol(int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException { if (route.address().sslSocketFactory() != null) { connectTls(readTimeout, writeTimeout, connectionSpecSelector); } else { protocol = Protocol.HTTP_1_1; socket = rawSocket; } if (protocol == Protocol.HTTP_2) { socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. Http2Connection http2Connection = new Http2Connection.Builder(true) .socket(socket, route.address().url().host(), source, sink) .listener(this) .build(); http2Connection.start(); // Only assign the framed connection once the preface has been sent successfully. this.allocationLimit = http2Connection.maxConcurrentStreams(); this.http2Connection = http2Connection; } else { this.allocationLimit = 1; } }
HTTP/2協議的協商過程在connectTls()的過程當中完成。
總結一下OkHttp3的鏈接RealConnection的含義,或者說是ConnectInterceptor從StreamAllocation中獲取的RealConnection對象的狀態:
關於HTTP代理的更多內容,能夠參考HTTP 代理原理及實現(一)。
OkHttp3中對路由的處理大致如此。