OkHttp3中的代理與路由
路由是什么呢?路由即是網絡數據包在網絡中的傳輸路徑,或者說數據包在傳輸過程中所經過的網絡節點,比如路由器,代理服務器之類的。
那像OkHttp3這樣的網絡庫對于數據包的路由需要做些什么事呢?用戶可以為終端設置代理服務器,HTTP/HTTPS代理或SOCK代理。OkHttp3中的路由相關邏輯,需要從系統中獲取用戶設置的代理服務器的地址,將HTTP請求轉換為代理協議的數據包,發給代理服務器,然后等待代理服務器幫助完成了網絡請求之后,從代理服務器讀取響應數據返回給用戶。只有這樣,用戶設置的代理才能生效。如果網絡庫無視用戶設置的代理服務器,直接進行DNS并做網絡請求,則用戶設置的代理服務器不生效。
這里就來看一下OkHttp3中路由相關的處理。
路由選擇
如同Internet上的其它設備一樣,每個路由節點都有自己的IP地址,加上端口號,則可以確定唯一的路由服務。以域名描述的HTTP/HTTPS代理服務器地址,可能對應于多個實際的代理服務器主機,因而一個代理服務器可能包含有多條路由。而SOCK代理服務器,則有著唯一確定的IP地址和端口號。
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中。
- 給調用者提供接口,來選擇可用的路由。調用者通過next()可以獲取 RouteSelector 中維護的下一個可用路由。調用者在連接失敗時,可以再次調用這個接口來獲取下一個路由。這個接口會逐個地返回每個代理的每個代理主機服務給調用者。在所有的代理的每個代理主機都被訪問過了之后,還會返回曾經連接失敗的路由。
- 維護路由節點的信息。 RouteDatabase 用于維護連接失敗的路由的信息,以避免浪費時間去連接一些不可用的路由。 RouteDatabase 中的路由信息主要由 RouteSelector 來維護。
RouteDatabase 是一個簡單的容器:
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
* >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 獲取可用路由。
在OkHttp3中, ProxySelector 對象主要由OkHttpClient維護。
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()方法來建立連接,后面我們通過分析這個方法的實現來了解連接的具體含義。 - 用前面創建的連接來創建HttpCodec。
對于HTTP/1.1創建Http1Codec,對于HTTP/2則創建Http2Codec。HttpCodec用于處理與HTTP具體協議相關的部分。比如HTTP/1.1是基于文本的協議,而HTTP/2則是基于二進制格式的協議,HttpCodec用于將請求編碼為對應協議要求的傳輸格式,并在得到響應時,對數據進行解碼。
然后來看 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;
}
這個過程大體為:
- 檢查上次分配的連接是否可用,若可用則,則將上次分配的連接返回給調用者。
- 上次分配的連接不存在,或不可用,則從連接池中查找一個連接,查找的依據就是Address,也就是連接的對端地址,以及路由等信息。Internal.instance指向OkHttpClient的一個內部類的對象,Internal.instance.get()實際會通過ConnectionPool的 get(Address address, StreamAllocation streamAllocation) 方法來嘗試獲取RealConnection。
若能從連接池中找到所需要的連接,則將連接返回給調用者。 - 從連接池中沒有找到所需要的連接,則會首先選擇路由。
- 然后創建新的連接RealConnection對象。
- acquire新創建的連接RealConnection對象,并將它放進連接池。不太確定這個地方的synchronized是不是太長了。貌似只有Internal.instance.put(connectionPool, newConnection)涉及到了全局對象的訪問,而其它操作并沒有。
- 調用newConnection.connect()建立連接。
這里再來看一下在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
* >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);
}
基本上是兩個過程:
- 建立隧道連接。
- 建立Protocol。
建立隧道連接的過程,又分為了幾個過程:
- 創建隧道請求
- 建立Socket連接
- 發送請求建立隧道
隧道請求是一個常規的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請求,為一個到HTTP服務器的Socket連接。后續直接向該Socket連接中寫入常規的HTTP請求,并從中讀取常規的HTTP響應。
- 對于不使用代理的https請求,為一個到https服務器的Socket連接,但經過了TLS握手,協議協商等過程。后續直接向該Socket連接中寫入常規的請求,并從中讀取常規的響應。
- 對于使用HTTP代理的HTTP請求,為一個到HTTP代理服務器的Socket連接。后續直接向該Socket連接中寫入常規的HTTP請求,并從中讀取常規的HTTP響應。
- 對于使用代理的https請求,為一個到代理服務器的隧道連接,但經過了TLS握手,協議協商等過程。后續直接向該Socket連接中寫入常規的請求,并從中讀取常規的響應。
來自:http://www.jianshu.com/p/5c98999bc34f