OkHttp3的連線池及連線建立過程分析
如我們前面在 OkHttp3 HTTP請求執行流程分析 中的分析,OkHttp3通過Interceptor鏈來執行HTTP請求,整體的執行過程大體如下: OkHttp Flow
這些Interceptor中每一個的職責,這裡不再贅述。
在OkHttp3中,StreamAllocation是用來建立執行HTTP請求所需網路設施的元件,如其名字所顯示的那樣,分配Stream。但它具體做的事情根據是否設定了代理,以及請求的型別,如HTTP、HTTPS或HTTP/2的不同而有所不同。代理相關的處理,包括TCP連線的建立,在 OkHttp3中的代理與路由 一文中有詳細的說明。
在整個HTTP請求的執行過程中,StreamAllocation 物件分配的比較早,在
RetryAndFollowUpInterceptor.intercept(Chain chain)中就完成了:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
StreamAllocation的物件構造過程沒有什麼特別的:
public StreamAllocation(ConnectionPool connectionPool, Address address, Object callStackTrace) {
this.connectionPool = connectionPool;
this.address = address;
this.routeSelector = new RouteSelector(address, routeDatabase());
this.callStackTrace = callStackTrace;
}
在OkHttp3中,okhttp3.internal.http.RealInterceptorChain將Interceptor連線成執行鏈。RetryAndFollowUpInterceptor藉助於RealInterceptorChain將建立的StreamAllocation物件傳遞給後面執行的Interceptor。而在RealInterceptorChain中,StreamAllocation物件並沒有被真正用到。緊跟在RetryAndFollowUpInterceptor之後執行的 okhttp3.internal.http.BridgeInterceptor 和 okhttp3.internal.cache.CacheInterceptor,它們的職責分別是補足使用者建立的請求中缺少的必須的請求頭和處理快取,也沒有真正用到StreamAllocation物件。
在OkHttp3的HTTP請求執行過程中,okhttp3.internal.connection.ConnectInterceptor和okhttp3.internal.http.CallServerInterceptor是與網路互動的關鍵。
CallServerInterceptor負責將HTTP請求寫入網路IO流,並從網路IO流中讀取伺服器返回的資料。而ConnectInterceptor則負責為CallServerInterceptor建立可用的連線。此處 可用的 含義主要為,可以直接寫入HTTP請求的資料:
設定了HTTP代理的HTTP請求,與代理建立好TCP連線;
設定了HTTP代理的HTTPS請求,與HTTP伺服器建立通過HTTP代理的隧道連線,並完成TLS握手;
設定了HTTP代理的HTTP/2請求,與HTTP伺服器建立通過HTTP代理的隧道連線,並完成與伺服器的TLS握手及協議協商;
設定了SOCKS代理的HTTP請求,通過代理與HTTP伺服器建立好連線;
設定了SOCKS代理的HTTPS請求,通過代理與HTTP伺服器建立好連線,並完成TLS握手;
設定了SOCKS代理的HTTP/2請求,通過代理與HTTP伺服器建立好連線,並完成與伺服器的TLS握手及協議協商;
無代理的HTTP請求,與伺服器建立好TCP連線;
無代理的HTTPS請求,與伺服器建立TCP連線,並完成TLS握手;
無代理的HTTP/2請求,與伺服器建立好TCP連線,完成TLS握手及協議協商。
後面我們更詳細地來看一下這個過程。
ConnectInterceptor的程式碼看上去比較簡單:
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從RealInterceptorChain獲取前面的Interceptor傳過來的StreamAllocation物件,執行 streamAllocation.newStream() 完成前述所有的連線建立工作,並將這個過程中建立的用於網路IO的RealConnection物件,以及對於與伺服器互動最為關鍵的HttpCodec等物件傳遞給後面的Interceptor,也就是CallServerInterceptor。 OkHttp3的連線池
在具體地分析 streamAllocation.newStream() 的執行過程之前,我們先來看一下OkHttp3的連線池的設計實現。
OkHttp3將客戶端與伺服器之間的連線抽象為Connection/RealConnection,為了管理這些連線的複用而設計了ConnectionPool。共享相同Address的請求可以複用連線,ConnectionPool實現了哪些連線保持開啟狀態以備後用的策略。 ConnectionPool是什麼?
藉助於ConnectionPool的成員變數宣告來一窺ConnectionPool究竟是什麼:
/**
* Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
* share the same {@link Address} may share a {@link Connection}. This class implements the policy
* of which connections to keep open for future use.
*/
public final class ConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;
private final long keepAliveDurationNs;
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
ConnectionPool的核心是RealConnection的容器,且是順序容器,而不是關聯容器。ConnectionPool用雙端佇列Deque來儲存它所管理的所有RealConnection。
ConnectionPool還會對連線池中最大的空閒連線數及連線的保活時間進行控制,maxIdleConnections和keepAliveDurationNs成員分別體現對最大空閒連線數及連線保活時間的控制。這種控制通過匿名的Runnable cleanupRunnable線上程池executor中執行,並在向連線池中新增新的RealConnection觸發。 連線池ConnectionPool的建立
OkHttp3的使用者可以自行建立ConnectionPool,對最大空閒連線數及連線的保活時間進行配置,並在OkHttpClient建立期間,將其傳給OkHttpClient.Builder,在OkHttpClient中啟用它。沒有定製連線池的情況下,則在OkHttpClient.Builder構造過程中以預設引數建立:
public Builder() {
dispatcher = new Dispatcher();
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
proxySelector = ProxySelector.getDefault();
cookieJar = CookieJar.NO_COOKIES;
socketFactory = SocketFactory.getDefault();
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT;
proxyAuthenticator = Authenticator.NONE;
authenticator = Authenticator.NONE;
connectionPool = new ConnectionPool();
ConnectionPool的預設構造過程如下:
/**
* Create a new connection pool with tuning parameters appropriate for a single-user application.
* The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
* this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
*/
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
在預設情況下,ConnectionPool 最多儲存 5個 處於空閒狀態的連線,且連線的預設保活時間為 5分鐘。 RealConnection的存/取
OkHttp內部的元件可以通過put()方法向ConnectionPool中新增RealConnection:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
在向ConnectionPool中新增RealConnection時,若發現cleanupRunnable還沒有執行會觸發它的執行。
cleanupRunnable的職責本就是清理無效的RealConnection,只要ConnectionPool中存在RealConnection,則這種清理的需求總是存在的,因而這裡會去啟動cleanupRunnable。
根據需要啟動了cleanupRunnable之後,將RealConnection新增進雙端佇列connections。
這裡先啟動 cleanupRunnable,後向 connections 中新增RealConnection。有沒有可能發生:
啟動cleanupRunnable之後,向connections中新增RealConnection之前,執行 put() 的執行緒被搶佔,cleanupRunnable的執行緒被執行,它發現connections中沒有任何RealConnection,於是從容地退出而導致後面新增的RealConnection永遠不會得得清理。
這樣的情況呢?答案是 不會。為什麼呢?put()執行之前總是會用ConnectionPool物件鎖來保護,而在ConnectionPool.cleanup()中,遍歷connections也總是會先對ConnectionPool物件加鎖保護的。即使執行 put() 的執行緒被搶佔,cleanupRunnable的執行緒也會由於拿不到ConnectionPool物件鎖而等待 put() 執行結束。
OkHttp內部的元件可以通過 get() 方法從ConnectionPool中獲取RealConnection:
/** 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;
}
get() 方法遍歷 connections 中的所有 RealConnection 尋找同時滿足如下三個條件的RealConnection:
RealConnection的allocations的數量小於allocationLimit。每個allocation代表在該RealConnection上正在執行的一個請求。這個條件用於控制相同連線上,同一時間執行的併發請求的個數。對於HTTP/2連線而言,allocationLimit限制是在連線建立階段由雙方協商的。對於HTTP或HTTPS連線而言,這個值則總是1。從RealConnection.establishProtocol()可以清晰地看到這一點:
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;
}
RealConnection 的 address 與傳入的 Address 引數相等。RealConnection 的 address 描述建立連線所需的配置資訊,包括對端的資訊等,不難理解只有所有相關配置相等時 RealConnection 才是真正能複用的。具體看一下Address相等性比較的依據:
@Override public boolean equals(Object other) {
if (other instanceof Address) {
Address that = (Address) other;
return this.url.equals(that.url)
&& this.dns.equals(that.dns)
&& this.proxyAuthenticator.equals(that.proxyAuthenticator)
&& this.protocols.equals(that.protocols)
&& this.connectionSpecs.equals(that.connectionSpecs)
&& this.proxySelector.equals(that.proxySelector)
&& equal(this.proxy, that.proxy)
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
&& equal(this.certificatePinner, that.certificatePinner);
}
return false;
}
這種相等性的條件給人感覺還是蠻苛刻的,特別是對url的對比。 這難免會讓我們有些擔心,對 Address 如此苛刻的相等性比較,又有多大的機會能複用連線呢? 我們的擔心其實是多餘的。只有在 StreamAllocation.findConnection() 中,會通過Internal.instance 呼叫 ConnectionPool.get() 來獲取 RealConnection :
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");
}
Internal.instance的實現在OkHttpClient 中:
static {
Internal.instance = new Internal() {
@Override public void addLenient(Headers.Builder builder, String line) {
builder.addLenient(line);
}
@Override public void addLenient(Headers.Builder builder, String name, String value) {
builder.addLenient(name, value);
}
@Override public void setCache(OkHttpClient.Builder builder, InternalCache internalCache) {
builder.setInternalCache(internalCache);
}
@Override public boolean connectionBecameIdle(
ConnectionPool pool, RealConnection connection) {
return pool.connectionBecameIdle(connection);
}
@Override public RealConnection get(
ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
return pool.get(address, streamAllocation);
}
@Override public void put(ConnectionPool pool, RealConnection connection) {
pool.put(connection);
}
@Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
return connectionPool.routeDatabase;
}
@Override public StreamAllocation callEngineGetStreamAllocation(Call call) {
return ((RealCall) call).streamAllocation();
}
可見 ConnectionPool.get() 的 Address 引數來自於StreamAllocation。StreamAllocation的Address 在構造時由外部傳入。構造了StreamAllocation物件的RetryAndFollowUpInterceptor,其構造Address的過程是這樣的:
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());
}
Address 除了 uriHost 和 uriPort 外的所有構造引數均來自於OkHttpClient,而Address的url 欄位正是根據這兩個引數構造的:
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();
可見 Address 的 url 欄位僅包含HTTP請求url的 schema + host + port 這三部分的資訊,而不包含 path 和 query 等資訊。ConnectionPool主要是根據伺服器的地址來決定複用的。
RealConnection還有可分配的Stream。對於HTTP或HTTPS而言,不能同時在相同的連線上執行多個請求。即使對於HTTP/2而言,StreamID的空間也是有限的,同一個連線上的StreamID總有分配完的時候,而在StreamID被分配完了之後,該連線就不能再被使用了。
OkHttp內部對ConnectionPool的訪問總是通過Internal.instance來進行。整個OkHttp中也只有StreamAllocation 存取了 ConnectionPool,也就是我們前面列出的StreamAllocation.findConnection() 方法,相關的元件之間的關係大體如下圖: OkHttp Connection Pool RealConnection的清理
ConnectionPool 中對於 RealConnection 的清理在put()方法中觸發,執行 cleanupRunnable 來完成清理動作:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
cleanupRunnable每執行一次清理動作,都會等待一段時間再次執行,而具體等待的時長由cleanup()方法決定,直到cleanup()方法返回-1退出。cleanup()方法定義如下:
/**
* Performs maintenance on this pool, evicting the connection that has been idle the longest if
* either it has exceeded the keep alive limit or the idle connections limit.
*
* <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
* -1 if no further cleanups are required.
*/
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
/**
* Prunes any leaked allocations and then returns the number of remaining live allocations on
* {@code connection}. Allocations are leaked if the connection is tracking them but the
* application code has abandoned them. Leak detection is imprecise and relies on garbage
* collection.
*/
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
List<Reference<StreamAllocation>> references = connection.allocations;
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
if (reference.get() != null) {
i++;
continue;
}
// We've discovered a leaked allocation. This is an application bug.
StreamAllocation.StreamAllocationReference streamAllocRef =
(StreamAllocation.StreamAllocationReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
references.remove(i);
connection.noNewStreams = true;
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
return references.size();
}
cleanup()方法遍歷connections,並從中找到處於空閒狀態時間最長的一個RealConnection,然後根據查詢結果的不同,分為以下幾種情況處理:
找到一個處於空閒狀態的RealConnection,且該RealConnection處於空閒狀態的時間超出了設定的保活時間,或者當前ConnectionPool中處於空閒狀態的連線數超出了設定的最大空閒連線數,將該RealConnection從connections中移除,並關閉該RealConnection關聯的底層socket,然後返回0,以此請求cleanupRunnable立即再次檢查所有的連線。
找到一個處於空閒狀態的RealConnection,但該RealConnection處於空閒狀態的時間尚未超出設定的保活時間,且當前ConnectionPool中處於空閒狀態的連線數尚未超出設定的最大空閒連線數,則返回保活時間與該RealConnection處於空閒狀態的時間之間的差值,請求cleanupRunnable等待這麼長一段時間之後再次檢查所有的連線。
沒有找到處於空閒狀態的連線,但找到了使用中的連線,則返回保活時間,請求cleanupRunnable等待這麼長一段時間之後再次檢查所有的連線。
沒有找到處於空閒狀態的連線,也沒有找到使用中的連線,也就意味著連線池中尚沒有任何連線,則將 cleanupRunning 置為false,並返回 -1,請求 cleanupRunnable 退出。
cleanup() 通過 pruneAndGetAllocationCount() 檢查正在使用一個特定連線的請求個數,並以此來判斷一個連線是否處於空閒狀態。後者通遍歷 connection.allocations 並檢查每個元素的StreamAllocation 的狀態,若StreamAllocation 為空,則認為是發現了一個leak,它會更新連線的空閒時間為當前時間減去保活時間並返回0,以此請求 cleanup() 立即關閉、清理掉該 leak 的連線。 ConnectionPool的使用者介面
OkHttp的使用者可以自己建立 ConnectionPool 物件,這個類也提供了一些使用者介面以方便使用者獲取空閒狀態的連線數、總的連線數,以及手動清除空閒狀態的連線:
/** Returns the number of idle connections in the pool. */
public synchronized int idleConnectionCount() {
int total = 0;
for (RealConnection connection : connections) {
if (connection.allocations.isEmpty()) total++;
}
return total;
}
/**
* Returns total number of connections in the pool. Note that prior to OkHttp 2.7 this included
* only idle connections and HTTP/2 connections. Since OkHttp 2.7 this includes all connections,
* both active and inactive. Use {@link #idleConnectionCount()} to count connections not currently
* in use.
*/
public synchronized int connectionCount() {
return connections.size();
}
......
/** Close and remove all idle connections in the pool. */
public void evictAll() {
List<RealConnection> evictedConnections = new ArrayList<>();
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
if (connection.allocations.isEmpty()) {
connection.noNewStreams = true;
evictedConnections.add(connection);
i.remove();
}
}
}
for (RealConnection connection : evictedConnections) {
closeQuietly(connection.socket());
}
}
新建流
回到新建流的過程,連線建立的各種細節處理都在這裡。 StreamAllocation.newStream() 完成新建流的動作:
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);
}
}
所謂的流,是封裝了底層的IO,可以直接用來收發資料的元件,它會將請求的資料序列化之後傳送到網路,並將接收的資料反序列化為應用程式方便操作的格式。在 OkHttp3 中,這樣的元件被抽象為HttpCodec。HttpCodec的定義如下 (okhttp/okhttp/src/main/java/okhttp3/internal/http/HttpCodec.java):
/** Encodes HTTP requests and decodes HTTP responses. */
public interface HttpCodec {
/**
* The timeout to use while discarding a stream of input data. Since this is used for connection
* reuse, this timeout should be significantly less than the time it takes to establish a new
* connection.
*/
int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
/** Returns an output stream where the request body can be streamed. */
Sink createRequestBody(Request request, long contentLength);
/** This should update the HTTP engine's sentRequestMillis field. */
void writeRequestHeaders(Request request) throws IOException;
/** Flush the request to the underlying socket. */
void finishRequest() throws IOException;
/** Read and return response headers. */
Response.Builder readResponseHeaders() throws IOException;
/** Returns a stream that reads the response body. */
ResponseBody openResponseBody(Response response) throws IOException;
/**
* Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
* That may happen later by the connection pool thread.
*/
void cancel();
}
HttpCodec提供了這樣的一些操作:
為傳送請求而提供的,寫入請求頭部。
為傳送請求而提供的,建立請求體,以用於傳送請求體資料。
為傳送請求而提供的,結束請求傳送。
為獲得響應而提供的,讀取響應頭部。
為獲得響應而提供的,開啟請求體,以用於後續獲取請求體資料。
取消請求執行。
StreamAllocation.newStream() 主要做的事情正是建立HttpCodec。StreamAllocation.newStream() 根據 OkHttpClient中的設定,連線超時、讀超時、寫超時及連線失敗是否重試,呼叫 findHealthyConnection() 完成 連線,即RealConnection 的建立。然後根據HTTP協議的版本建立Http1Codec或Http2Codec。
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;
}
}
連線是否可用的標準如下 (okhttp/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java):
/** Returns true if this connection is ready to host new streams. */
public boolean isHealthy(boolean doExtensiveChecks) {
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
return false;
}
if (http2Connection != null) {
return true; // TODO: check framedConnection.shutdown.
}
if (doExtensiveChecks) {
try {
int readTimeout = socket.getSoTimeout();
try {
socket.setSoTimeout(1);
if (source.exhausted()) {
return false; // Stream is exhausted; socket is closed.
}
return true;
} finally {
socket.setSoTimeout(readTimeout);
}
} catch (SocketTimeoutException ignored) {
// Read timed out; socket is good.
} catch (IOException e) {
return false; // Couldn't read; socket is closed.
}
}
return true;
}
首先要可以進行IO,此外對於HTTP/2,只要http2Connection存在即可。如我們前面在ConnectInterceptor 中看到的,如果HTTP請求的method不是 “GET” ,doExtensiveChecks為true時,需要做額外的檢查。
findHealthyConnection() 通過 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;
}
` |
---|
findConnection() 返回一個用於流執行底層IO的連線。這個方法優先複用已經建立的連線;在沒有可複用連線的情況下新建一個。
在同一次 newStream() 的執行過程中,有沒有可能兩次執行 findConnection() ,第一次connection 欄位為空,第二次不為空?這個地方對connection欄位的檢查,看起來有點多餘。執行 findConnection() 時,connection 不為空的話,意味著 codec 不為空,而在方法的開始處已經有對codec欄位的狀態做過檢查。真的是這樣的嗎?
答案當然是否定的。同一次 newStream() 的執行過程中,沒有可能兩次執行findConnection(),第一次connection欄位為空,第二次不為空,然而一個HTTP請求的執行過程,又不是一定只調用一次newStream()。
newStream()的直接呼叫者是ConnectInterceptor,所有的Interceptor用RealInterceptorChain鏈起來,在Interceptor鏈中,ConnectInterceptor 和RetryAndFollowUpInterceptor 隔著 CacheInterceptor 和 BridgeInterceptor 。然而newStream() 如果出錯的話,則是會通過丟擲Exception返回到RetryAndFollowUpInterceptor 來處理錯誤的。
RetryAndFollowUpInterceptor 中會嘗試基於相同的 StreamAllocation 物件來恢復對HTTP請求的處理。RetryAndFollowUpInterceptor 通過 hasMoreRoutes() 等方法,來檢查StreamAllocation 物件的狀態,通過 streamFailed(IOException e)、release()、streamFinished(boolean noNewStreams, HttpCodec codec)等方法來reset StreamAllocation物件的一些狀態。
回到StreamAllocation的 findConnection()方法。沒有連線存在,且連線池中也沒有找到所需的連線時,它會新建一個連線。通過如下的步驟新建連線:
為連線選擇一個Route。
新建一個RealConnection物件。
public RealConnection(Route route) {
this.route = route;
}
將當前StreamAllocation物件的引用儲存進RealConnection的allocations。如我們前面在分析ConnectionPool時所見的那樣,這主要是為了追蹤RealConnection。
/**
* 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儲存進連線池。
儲存對RealConnection的引用。
檢查請求是否被取消,若取消,則丟擲異常。
建立連線。
更新RouteDatabase中Route的狀態。
ConnectionSpec
在OkHttp中,ConnectionSpec用於描述傳輸HTTP流量的socket連線的配置。對於https請求,這些配置主要包括協商安全連線時要使用的TLS版本號和密碼套件,是否支援TLS擴充套件等;對於http請求則幾乎不包含什麼資訊。
OkHttp有預定義幾組ConnectionSpec (okhttp/okhttp/src/main/java/okhttp3/ConnectionSpec.java):
/** A modern TLS connection with extensions like SNI and ALPN available. */
public static final ConnectionSpec MODERN_TLS = new Builder(true)
.cipherSuites(APPROVED_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
/** A backwards-compatible fallback connection for interop with obsolete servers. */
public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
/** Unencrypted, unauthenticated connections for {@code http:} URLs. */
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
預定義的這些ConnectionSpec被組織為預設ConnectionSpec集合 (okhttp/okhttp/src/main/java/okhttp3/OkHttpClient.java):
public class OkHttpClient implements Cloneable, Call.Factory {
private static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(
Protocol.HTTP_2, Protocol.HTTP_1_1);
private static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT);
OkHttp中由OkHttpClient管理ConnectionSpec集合 。OkHttp的使用者可以在構造OkHttpClient的過程中提供自己的ConnectionSpec集合。預設情況下OkHttpClient會使用前面看到的預設ConnectionSpec集合。
在RetryAndFollowUpInterceptor中建立Address時,ConnectionSpec集合被從OkHttpClient獲取,並由Address引用。
OkHttp還提供了ConnectionSpecSelector,用以從ConnectionSpec集合中選擇與SSLSocket匹配的ConnectionSpec,並對SSLSocket做配置的操作。
在StreamAllocation的findConnection()中,ConnectionSpec集合被從Address中取出來,以用於連線建立過程。 建立連線
回到連線建立的過程。RealConnection.connect()執行連線建立的過程(okhttp/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java):
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;
}
}
}
}
這裡的執行過程大體如下:
檢查連線是否已經建立,若已經建立,則丟擲異常,否則繼續執行。連線是否建立由protocol 標識,它表示在整個連線建立,及可能的協議協商過程中選擇的所要使用的協議。
根據ConnectionSpec集合connectionSpecs構造ConnectionSpecSelector。
若請求不是安全的請求,會對請求再執行一些額外的限制。這些限制包括:
ConnectionSpec集合中必須要包含ConnectionSpec.CLEARTEXT。這也就是說,OkHttp的使用者可以通過為OkHttpClient設定不包含ConnectionSpec.CLEARTEXT的ConnectionSpec集合來禁用所有的明文請求。
平臺本身的安全策略允許向相應的主機發送明文請求。對於Android平臺而言,這種安全策略主要由系統的元件android.security.NetworkSecurityPolicy執行 (okhttp/okhttp/src/main/java/okhttp3/internal/platform/AndroidPlatform.java):
@Override public boolean isCleartextTrafficPermitted(String hostname) {
try {
Class<?> networkPolicyClass = Class.forName("android.security.NetworkSecurityPolicy");
Method getInstanceMethod = networkPolicyClass.getMethod("getInstance");
Object networkSecurityPolicy = getInstanceMethod.invoke(null);
Method isCleartextTrafficPermittedMethod = networkPolicyClass
.getMethod("isCleartextTrafficPermitted", String.class);
return (boolean) isCleartextTrafficPermittedMethod.invoke(networkSecurityPolicy, hostname);
} catch (ClassNotFoundException | NoSuchMethodException e) {
return super.isCleartextTrafficPermitted(hostname);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new AssertionError();
}
}
平臺的這種安全策略並不是每個Android版本都有的。Android 6.0之後存在這種控制。
根據請求是否需要建立隧道連線,而分別執行buildTunneledConnection() 和 buildConnection()。是否需要建立隧道連線的依據為 (okhttp/okhttp/src/main/java/okhttp3/Route.java):
/**
* 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代理,且安全的連線 (SSL) 需要請求代理伺服器建立一個到目標HTTP伺服器的隧道連線,客戶端與HTTP代理建立TCP連線,以此請求HTTP代理服務在客戶端與HTTP伺服器之間進行資料的盲轉發。 建立隧道連線
建立隧道連線的過程如下:
構造一個 建立隧道連線 請求。
與HTTP代理伺服器建立TCP連線。
建立隧道。這主要是將 建立隧道連線 請求傳送給HTTP代理伺服器,並處理它的響應。
重複上面的第2和第3步,知道建立好了隧道連線。至於為什麼要重複多次,及關於代理認證的內容,可以參考代理協議相關的內容。
建立協議。
關於建立隧道連線更詳細的過程可參考 OkHttp3中的代理與路由 的相關部分。 建立普通連線
建立普通連線的過程比較直接:
/** 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);
}
建立一個TCP連線。
建立協議。
更詳細的過程可參考 OkHttp3中的代理與路由 的相關部分。 建立協議
不管是建立隧道連線,還是建立普通連線,都少不了 建立協議 這一步。這一步是在建立好了TCP連線之後,而在該TCP能被拿來收發資料之前執行的。它主要為資料的加密傳輸做一些初始化,比如TLS握手,HTTP/2的協議協商等。
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;
}
}
對於加密的資料傳輸,建立TLS連線。對於明文傳輸,則設定protocol和socket。
socket指向直接與應用層,如HTTP或HTTP/2,互動的Socket:
對於明文傳輸沒有設定HTTP代理的HTTP請求,它是與HTTP伺服器之間的TCP socket;
對於明文傳輸設定了HTTP代理或SOCKS代理的HTTP請求,它是與代理伺服器之間的TCP socket;
對於加密傳輸沒有設定HTTP代理伺服器的HTTP或HTTP2請求,它是與HTTP伺服器之間的SSLScoket;
對於加密傳輸設定了HTTP代理伺服器的HTTP或HTTP2請求,它是與HTTP伺服器之間經過了代理伺服器的SSLSocket,一個隧道連線;
對於加密傳輸設定了SOCKS代理的HTTP或HTTP2請求,它是一條經過了代理伺服器的SSLSocket連線。
對於HTTP/2,會建立HTTP/2連線,並進一步協商連線引數,如連線上可同時執行的併發請求數等。而對於非HTTP/2,則將連線上可同時執行的併發請求數設定為1。
建立TLS連線
進一步來看建立協議過程中,為安全請求所做的建立TLS連線的過程:
private void connectTls(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
TLS連線是對原始的TCP連線的一個封裝,以提供TLS握手,及資料收發過程中的加密解密等功能。在Java中,用SSLSocket來描述。上面建立TLS連線的過程大體為:
用SSLSocketFactory基於原始的TCP Socket,建立一個SSLSocket。
配置SSLSocket。
在前面選擇的ConnectionSpec支援TLS擴充套件引數時,配置TLS擴充套件引數。
啟動TLS握手。
TLS握手完成之後,獲取握手資訊。
對TLS握手過程中傳回來的證書進行驗證。
檢查證書釘扎。
在前面選擇的ConnectionSpec支援TLS擴充套件引數時,獲取TLS握手過程中順便完成的協議協商過程所選擇的協議。這個過程主要用於HTTP/2的ALPN擴充套件。
OkHttp主要使用Okio來做IO操作,這裡會基於前面獲取的SSLSocket建立用於執行IO的BufferedSource和BufferedSink等,並儲存握手資訊及所選擇的協議。
具體來看ConnectionSpecSelector中配置SSLSocket的過程:
/**
* Configures the supplied {@link SSLSocket} to connect to the specified host using an appropriate
* {@link ConnectionSpec}. Returns the chosen {@link ConnectionSpec}, never {@code null}.
*
* @throws IOException if the socket does not support any of the TLS modes available
*/
public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
ConnectionSpec tlsConfiguration = null;
for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
ConnectionSpec connectionSpec = connectionSpecs.get(i);
if (connectionSpec.isCompatible(sslSocket)) {
tlsConfiguration = connectionSpec;
nextModeIndex = i + 1;
break;
}
}
if (tlsConfiguration == null) {
// This may be the first time a connection has been attempted and the socket does not support
// any the required protocols, or it may be a retry (but this socket supports fewer
// protocols than was suggested by a prior socket).
throw new UnknownServiceException(
"Unable to find acceptable protocols. isFallback=" + isFallback
+ ", modes=" + connectionSpecs
+ ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
}
isFallbackPossible = isFallbackPossible(sslSocket);
Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);
return tlsConfiguration;
}
這個過程分為如下的兩個步驟:
從為OkHttp配置的ConnectionSpec集合中選擇一個與SSLSocket相容的一個。SSLSocket與ConnectionSpec相容的標準如下:
public boolean isCompatible(SSLSocket socket) {
if (!tls) {
return false;
}
if (tlsVersions != null
&& !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
return false;
}
if (cipherSuites != null
&& !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
return false;
}
return true;
}
/**
* An N*M intersection that terminates if any intersection is found. The sizes of both arguments
* are assumed to be so small, and the likelihood of an intersection so great, that it is not
* worth the CPU cost of sorting or the memory cost of hashing.
*/
private static boolean nonEmptyIntersection(String[] a, String[] b) {
if (a == null || b == null || a.length == 0 || b.length == 0) {
return false;
}
for (String toFind : a) {
if (indexOf(b, toFind) != -1) {
return true;
}
}
return false;
}
即ConnectionSpec啟用的TLS版本及密碼套件,與SSLSocket啟用的有交集。 2 將選擇的ConnectionSpec應用在SSLSocket上。OkHttpClient中ConnectionSpec的應用:
@Override public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) { tlsConfiguration.apply(sslSocket, isFallback); }
而在ConnectionSpec中:
/** Applies this spec to {@code sslSocket}. */
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
if (specToApply.tlsVersions != null) {
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
}
if (specToApply.cipherSuites != null) {
sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
/**
* Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
* sslSocket}.
*/
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
String[] cipherSuitesIntersection = cipherSuites != null
? intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
: sslSocket.getEnabledCipherSuites();
String[] tlsVersionsIntersection = tlsVersions != null
? intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
: sslSocket.getEnabledProtocols();
// In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
// the SCSV cipher is added to signal that a protocol fallback has taken place.
if (isFallback && indexOf(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV") != -1) {
cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
}
return new Builder(this)
.cipherSuites(cipherSuitesIntersection)
.tlsVersions(tlsVersionsIntersection)
.build();
}
主要是:
求得ConnectionSpec啟用的TLS版本及密碼套件與SSLSocket啟用的TLS版本及密碼套件之間的交集,構造新的ConnectionSpec。
重新為SSLSocket設定啟用的TLS版本及密碼套件為上一步求得的交集。
我們知道HTTP/2的協議協商主要是利用了TLS的ALPN擴充套件來完成的。這裡再來詳細的看一下配置TLS擴充套件的過程。對於Android平臺而言,這部分邏輯在AndroidPlatform:
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
// Enable SNI and session tickets.
if (hostname != null) {
setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
}
// Enable ALPN.
if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
Object[] parameters = {concatLengthPrefixed(protocols)};
setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
}
}
TLS擴充套件相關的方法不是SSLSocket介面的標準方法,不同的SSL/TLS實現庫對這些介面的支援程度不一樣,因而這裡通過反射機制呼叫TLS擴充套件相關的方法。
這裡主要配置了3個TLS擴充套件,分別是session tickets,SNI和ALPN。session tickets用於會話回覆,SNI用於支援單個主機配置了多個域名的情況,ALPN則用於HTTP/2的協議協商。可以看到為SNI設定的hostname最終來源於Url,也就意味著使用HttpDns時,如果直接將IP地址替換原來Url中的域名來發起HTTPS請求的話,SNI將是IP地址,這有可能使伺服器下發不恰當的證書。
TLS擴充套件相關方法的OptionalMethod建立過程也在AndroidPlatform中:
public AndroidPlatform(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
OptionalMethod<Socket> setHostname, OptionalMethod<Socket> getAlpnSelectedProtocol,
OptionalMethod<Socket> setAlpnProtocols) {
this.sslParametersClass = sslParametersClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
this.getAlpnSelectedProtocol = getAlpnSelectedProtocol;
this.setAlpnProtocols = setAlpnProtocols;
}
......
public static Platform buildIfSupported() {
// Attempt to find Android 2.3+ APIs.
try {
Class<?> sslParametersClass;
try {
sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
sslParametersClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(
null, "setUseSessionTickets", boolean.class);
OptionalMethod<Socket> setHostname = new OptionalMethod<>(
null, "setHostname", String.class);
OptionalMethod<Socket> getAlpnSelectedProtocol = null;
OptionalMethod<Socket> setAlpnProtocols = null;
// Attempt to find Android 5.0+ APIs.
try {
Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
} catch (ClassNotFoundException ignored) {
}
return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
getAlpnSelectedProtocol, setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
return null;
}
建立TLS連線的第7步,獲取協議的過程與配置TLS的過程類似,同樣利用反射呼叫SSLSocket的方法,在AndroidPlatform中:
@Override public String getSelectedProtocol(SSLSocket socket) {
if (getAlpnSelectedProtocol == null) return null;
if (!getAlpnSelectedProtocol.isSupported(socket)) return null;
byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
}
至此我們分析了OkHttp3中,所有HTTP請求,包括設定了代理的明文HTTP請求,設定了代理的HTTPS請求,設定了代理的HTTP/2請求,無代理的明文HTTP請求,無代理的HTTPS請求,無代理的HTTP/2請求的連線建立過程,其中包括TLS的握手,HTTP/2的協議協商等。
總結一下,OkHttp中,IO相關的元件的其關係大體如下圖所示:
https://upload-images.jianshu.io/upload_images/1315506-338a7a0b0a39a278.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000
Connection Component
Done。