OkHttp3的连接池及连接建立过程分析
如我們前面在 OkHttp3 HTTP請求執(zhí)行流程分析 中的分析,OkHttp3通過Interceptor鏈來執(zhí)行HTTP請求,整體的執(zhí)行過程大體如下:
OkHttp Flow
這些Interceptor中每一個的職責,這里不再贅述。
在OkHttp3中,StreamAllocation是用來建立執(zhí)行HTTP請求所需網(wǎng)絡設施的組件,如其名字所顯示的那樣,分配Stream。但它具體做的事情根據(jù)是否設置了代理,以及請求的類型,如HTTP、HTTPS或HTTP/2的不同而有所不同。代理相關的處理,包括TCP連接的建立,在 OkHttp3中的代理與路由 一文中有詳細的說明。
在整個HTTP請求的執(zhí)行過程中,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連接成執(zhí)行鏈。RetryAndFollowUpInterceptor借助于RealInterceptorChain將創(chuàng)建的StreamAllocation對象傳遞給后面執(zhí)行的Interceptor。而在RealInterceptorChain中,StreamAllocation對象并沒有被真正用到。緊跟在RetryAndFollowUpInterceptor之后執(zhí)行的 okhttp3.internal.http.BridgeInterceptor 和 okhttp3.internal.cache.CacheInterceptor,它們的職責分別是補足用戶創(chuàng)建的請求中缺少的必須的請求頭和處理緩存,也沒有真正用到StreamAllocation對象。
在OkHttp3的HTTP請求執(zhí)行過程中,okhttp3.internal.connection.ConnectInterceptor和okhttp3.internal.http.CallServerInterceptor是與網(wǎng)絡交互的關鍵。
CallServerInterceptor負責將HTTP請求寫入網(wǎng)絡IO流,并從網(wǎng)絡IO流中讀取服務器返回的數(shù)據(jù)。而ConnectInterceptor則負責為CallServerInterceptor建立可用的連接。此處 可用的 含義主要為,可以直接寫入HTTP請求的數(shù)據(jù):
- 設置了HTTP代理的HTTP請求,與代理建立好TCP連接;
- 設置了HTTP代理的HTTPS請求,與HTTP服務器建立通過HTTP代理的隧道連接,并完成TLS握手;
- 設置了HTTP代理的HTTP/2請求,與HTTP服務器建立通過HTTP代理的隧道連接,并完成與服務器的TLS握手及協(xié)議協(xié)商;
- 設置了SOCKS代理的HTTP請求,通過代理與HTTP服務器建立好連接;
- 設置了SOCKS代理的HTTPS請求,通過代理與HTTP服務器建立好連接,并完成TLS握手;
- 設置了SOCKS代理的HTTP/2請求,通過代理與HTTP服務器建立好連接,并完成與服務器的TLS握手及協(xié)議協(xié)商;
- 無代理的HTTP請求,與服務器建立好TCP連接;
- 無代理的HTTPS請求,與服務器建立TCP連接,并完成TLS握手;
- 無代理的HTTP/2請求,與服務器建立好TCP連接,完成TLS握手及協(xié)議協(xié)商。
后面我們更詳細地來看一下這個過程。
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對象,執(zhí)行 streamAllocation.newStream() 完成前述所有的連接建立工作,并將這個過程中創(chuàng)建的用于網(wǎng)絡IO的RealConnection對象,以及對于與服務器交互最為關鍵的HttpCodec等對象傳遞給后面的Interceptor,也就是CallServerInterceptor。
OkHttp3的連接池
在具體地分析 streamAllocation.newStream() 的執(zhí)行過程之前,我們先來看一下OkHttp3的連接池的設計實現(xiàn)。
OkHttp3將客戶端與服務器之間的連接抽象為Connection/RealConnection,為了管理這些連接的復用而設計了ConnectionPool。共享相同Address的請求可以復用連接,ConnectionPool實現(xiàn)了哪些連接保持打開狀態(tài)以備后用的策略。
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的容器,且是順序容器,而不是關聯(lián)容器。ConnectionPool用雙端隊列Deque<RealConnection>來保存它所管理的所有RealConnection。
ConnectionPool還會對連接池中最大的空閑連接數(shù)及連接的保活時間進行控制,maxIdleConnections和keepAliveDurationNs成員分別體現(xiàn)對最大空閑連接數(shù)及連接保活時間的控制。這種控制通過匿名的Runnable cleanupRunnable在線程池executor中執(zhí)行,并在向連接池中添加新的RealConnection觸發(fā)。
連接池ConnectionPool的創(chuàng)建
OkHttp3的用戶可以自行創(chuàng)建ConnectionPool,對最大空閑連接數(shù)及連接的保活時間進行配置,并在OkHttpClient創(chuàng)建期間,將其傳給OkHttpClient.Builder,在OkHttpClient中啟用它。沒有定制連接池的情況下,則在OkHttpClient.Builder構造過程中以默認參數(shù)創(chuàng)建:
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個 處于空閑狀態(tài)的連接,且連接的默認保活時間為 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時,若發(fā)現(xiàn)cleanupRunnable還沒有運行會觸發(fā)它的運行。
cleanupRunnable的職責本就是清理無效的RealConnection,只要ConnectionPool中存在RealConnection,則這種清理的需求總是存在的,因而這里會去啟動cleanupRunnable。
根據(jù)需要啟動了cleanupRunnable之后,將RealConnection添加進雙端隊列connections。
這里先啟動 cleanupRunnable,后向 connections 中添加RealConnection。有沒有可能發(fā)生:
啟動cleanupRunnable之后,向connections中添加RealConnection之前,執(zhí)行 put() 的線程被搶占,cleanupRunnable的線程被執(zhí)行,它發(fā)現(xiàn)connections中沒有任何RealConnection,于是從容地退出而導致后面添加的RealConnection永遠不會得得清理。
這樣的情況呢?答案是 不會。為什么呢?put()執(zhí)行之前總是會用ConnectionPool對象鎖來保護,而在ConnectionPool.cleanup()中,遍歷connections也總是會先對ConnectionPool對象加鎖保護的。即使執(zhí)行 put() 的線程被搶占,cleanupRunnable的線程也會由于拿不到ConnectionPool對象鎖而等待 put() 執(zhí)行結束。
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的數(shù)量小于allocationLimit。每個allocation代表在該RealConnection上正在執(zhí)行的一個請求。這個條件用于控制相同連接上,同一時間執(zhí)行的并發(fā)請求的個數(shù)。對于HTTP/2連接而言,allocationLimit限制是在連接建立階段由雙方協(xié)商的。對于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 參數(shù)相等。RealConnection 的 address 描述建立連接所需的配置信息,包括對端的信息等,不難理解只有所有相關配置相等時 RealConnection 才是真正能復用的。具體看一下Address相等性比較的依據(jù):@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 :
Internal.instance的實現(xiàn)在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 參數(shù)來自于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 外的所有構造參數(shù)均來自于OkHttpClient,而Address的url 字段正是根據(jù)這兩個參數(shù)構造的:
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主要是根據(jù)服務器的地址來決定復用的。
- RealConnection還有可分配的Stream。對于HTTP或HTTPS而言,不能同時在相同的連接上執(zhí)行多個請求。即使對于HTTP/2而言,StreamID的空間也是有限的,同一個連接上的StreamID總有分配完的時候,而在StreamID被分配完了之后,該連接就不能再被使用了。
OkHttp內部對ConnectionPool的訪問總是通過Internal.instance來進行。整個OkHttp中也只有StreamAllocation 存取了 ConnectionPool,也就是我們前面列出的StreamAllocation.findConnection() 方法,相關的組件之間的關系大體如下圖:
OkHttp Connection Pool
RealConnection的清理
ConnectionPool 中對于 RealConnection 的清理在put()方法中觸發(fā),執(zhí)行 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每執(zhí)行一次清理動作,都會等待一段時間再次執(zhí)行,而具體等待的時長由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,并從中找到處于空閑狀態(tài)時間最長的一個RealConnection,然后根據(jù)查找結果的不同,分為以下幾種情況處理:
- 找到一個處于空閑狀態(tài)的RealConnection,且該RealConnection處于空閑狀態(tài)的時間超出了設置的保活時間,或者當前ConnectionPool中處于空閑狀態(tài)的連接數(shù)超出了設置的最大空閑連接數(shù),將該RealConnection從connections中移除,并關閉該RealConnection關聯(lián)的底層socket,然后返回0,以此請求cleanupRunnable立即再次檢查所有的連接。
- 找到一個處于空閑狀態(tài)的RealConnection,但該RealConnection處于空閑狀態(tài)的時間尚未超出設置的保活時間,且當前ConnectionPool中處于空閑狀態(tài)的連接數(shù)尚未超出設置的最大空閑連接數(shù),則返回保活時間與該RealConnection處于空閑狀態(tài)的時間之間的差值,請求cleanupRunnable等待這么長一段時間之后再次檢查所有的連接。
- 沒有找到處于空閑狀態(tài)的連接,但找到了使用中的連接,則返回保活時間,請求cleanupRunnable等待這么長一段時間之后再次檢查所有的連接。
- 沒有找到處于空閑狀態(tài)的連接,也沒有找到使用中的連接,也就意味著連接池中尚沒有任何連接,則將 cleanupRunning 置為false,并返回 -1,請求 cleanupRunnable 退出。
cleanup() 通過 pruneAndGetAllocationCount() 檢查正在使用一個特定連接的請求個數(shù),并以此來判斷一個連接是否處于空閑狀態(tài)。后者通遍歷 connection.allocations 并檢查每個元素的StreamAllocation 的狀態(tài),若StreamAllocation 為空,則認為是發(fā)現(xiàn)了一個leak,它會更新連接的空閑時間為當前時間減去保活時間并返回0,以此請求 cleanup() 立即關閉、清理掉該 leak 的連接。
ConnectionPool的用戶接口
OkHttp的用戶可以自己創(chuàng)建 ConnectionPool 對象,這個類也提供了一些用戶接口以方便用戶獲取空閑狀態(tài)的連接數(shù)、總的連接數(shù),以及手動清除空閑狀態(tài)的連接:
/** 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());}}新建流
回到新建流的過程,連接建立的各種細節(jié)處理都在這里。 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,可以直接用來收發(fā)數(shù)據(jù)的組件,它會將請求的數(shù)據(jù)序列化之后發(fā)送到網(wǎng)絡,并將接收的數(shù)據(jù)反序列化為應用程序方便操作的格式。在 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提供了這樣的一些操作:
- 為發(fā)送請求而提供的,寫入請求頭部。
- 為發(fā)送請求而提供的,創(chuàng)建請求體,以用于發(fā)送請求體數(shù)據(jù)。
- 為發(fā)送請求而提供的,結束請求發(fā)送。
- 為獲得響應而提供的,讀取響應頭部。
- 為獲得響應而提供的,打開請求體,以用于后續(xù)獲取請求體數(shù)據(jù)。
- 取消請求執(zhí)行。
StreamAllocation.newStream() 主要做的事情正是創(chuàng)建HttpCodec。StreamAllocation.newStream() 根據(jù) OkHttpClient中的設置,連接超時、讀超時、寫超時及連接失敗是否重試,調用 findHealthyConnection() 完成 連接,即RealConnection 的創(chuàng)建。然后根據(jù)HTTP協(xié)議的版本創(chuàng)建Http1Codec或Http2Codec。
findHealthyConnection() 根據(jù)目標服務器地址查找一個連接,如果它是可用的就直接返回,如果不可用則會重復查找直到找到一個可用的為止。在連接已被破壞而不可用時,還會釋放連接:
/*** 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() 返回一個用于流執(zhí)行底層IO的連接。這個方法優(yōu)先復用已經(jīng)創(chuàng)建的連接;在沒有可復用連接的情況下新建一個。
在同一次 newStream() 的執(zhí)行過程中,有沒有可能兩次執(zhí)行 findConnection() ,第一次connection 字段為空,第二次不為空?這個地方對connection字段的檢查,看起來有點多余。執(zhí)行 findConnection() 時,connection 不為空的話,意味著 codec 不為空,而在方法的開始處已經(jīng)有對codec字段的狀態(tài)做過檢查。真的是這樣的嗎?
答案當然是否定的。同一次 newStream() 的執(zhí)行過程中,沒有可能兩次執(zhí)行findConnection(),第一次connection字段為空,第二次不為空,然而一個HTTP請求的執(zhí)行過程,又不是一定只調用一次newStream()。
newStream()的直接調用者是ConnectInterceptor,所有的Interceptor用RealInterceptorChain鏈起來,在Interceptor鏈中,ConnectInterceptor 和RetryAndFollowUpInterceptor 隔著 CacheInterceptor 和 BridgeInterceptor 。然而newStream() 如果出錯的話,則是會通過拋出Exception返回到RetryAndFollowUpInterceptor 來處理錯誤的。
RetryAndFollowUpInterceptor 中會嘗試基于相同的 StreamAllocation 對象來恢復對HTTP請求的處理。RetryAndFollowUpInterceptor 通過 hasMoreRoutes() 等方法,來檢查StreamAllocation 對象的狀態(tài),通過 streamFailed(IOException e)、release()、streamFinished(boolean noNewStreams, HttpCodec codec)等方法來reset StreamAllocation對象的一些狀態(tài)。
回到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的狀態(tài)。
ConnectionSpec
在OkHttp中,ConnectionSpec用于描述傳輸HTTP流量的socket連接的配置。對于https請求,這些配置主要包括協(xié)商安全連接時要使用的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中創(chuàng)建Address時,ConnectionSpec集合被從OkHttpClient獲取,并由Address引用。
OkHttp還提供了ConnectionSpecSelector,用以從ConnectionSpec集合中選擇與SSLSocket匹配的ConnectionSpec,并對SSLSocket做配置的操作。
在StreamAllocation的findConnection()中,ConnectionSpec集合被從Address中取出來,以用于連接建立過程。
建立連接
回到連接建立的過程。RealConnection.connect()執(zhí)行連接建立的過程(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;}}}}這里的執(zhí)行過程大體如下:
- 檢查連接是否已經(jīng)建立,若已經(jīng)建立,則拋出異常,否則繼續(xù)執(zhí)行。連接是否建立由protocol 標識,它表示在整個連接建立,及可能的協(xié)議協(xié)商過程中選擇的所要使用的協(xié)議。
- 根據(jù)ConnectionSpec集合connectionSpecs構造ConnectionSpecSelector。
- 若請求不是安全的請求,會對請求再執(zhí)行一些額外的限制。這些限制包括:
- ConnectionSpec集合中必須要包含ConnectionSpec.CLEARTEXT。這也就是說,OkHttp的用戶可以通過為OkHttpClient設置不包含ConnectionSpec.CLEARTEXT的ConnectionSpec集合來禁用所有的明文請求。
- 平臺本身的安全策略允許向相應的主機發(fā)送明文請求。對于Android平臺而言,這種安全策略主要由系統(tǒng)的組件android.security.NetworkSecurityPolicy執(zhí)行 (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之后存在這種控制。
- 根據(jù)請求是否需要建立隧道連接,而分別執(zhí)行buildTunneledConnection() 和 buildConnection()。是否需要建立隧道連接的依據(jù)為 (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服務器之間進行數(shù)據(jù)的盲轉發(fā)。
建立隧道連接
建立隧道連接的過程如下:
關于建立隧道連接更詳細的過程可參考 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);}更詳細的過程可參考 OkHttp3中的代理與路由 的相關部分。
建立協(xié)議
不管是建立隧道連接,還是建立普通連接,都少不了 建立協(xié)議 這一步。這一步是在建立好了TCP連接之后,而在該TCP能被拿來收發(fā)數(shù)據(jù)之前執(zhí)行的。它主要為數(shù)據(jù)的加密傳輸做一些初始化,比如TLS握手,HTTP/2的協(xié)議協(xié)商等。
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;}}對于加密的數(shù)據(jù)傳輸,創(chuàng)建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服務器之間經(jīng)過了代理服務器的SSLSocket,一個隧道連接;
對于加密傳輸設置了SOCKS代理的HTTP或HTTP2請求,它是一條經(jīng)過了代理服務器的SSLSocket連接。
對于HTTP/2,會建立HTTP/2連接,并進一步協(xié)商連接參數(shù),如連接上可同時執(zhí)行的并發(fā)請求數(shù)等。而對于非HTTP/2,則將連接上可同時執(zhí)行的并發(fā)請求數(shù)設置為1。
建立TLS連接
進一步來看建立協(xié)議過程中,為安全請求所做的建立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握手,及數(shù)據(jù)收發(fā)過程中的加密解密等功能。在Java中,用SSLSocket來描述。上面建立TLS連接的過程大體為:
具體來看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的應用:
而在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的協(xié)議協(xié)商主要是利用了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實現(xiàn)庫對這些接口的支持程度不一樣,因而這里通過反射機制調用TLS擴展相關的方法。
這里主要配置了3個TLS擴展,分別是session tickets,SNI和ALPN。session tickets用于會話回復,SNI用于支持單個主機配置了多個域名的情況,ALPN則用于HTTP/2的協(xié)議協(xié)商。可以看到為SNI設置的hostname最終來源于Url,也就意味著使用HttpDns時,如果直接將IP地址替換原來Url中的域名來發(fā)起HTTPS請求的話,SNI將是IP地址,這有可能使服務器下發(fā)不恰當?shù)淖C書。
TLS擴展相關方法的OptionalMethod創(chuàng)建過程也在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步,獲取協(xié)議的過程與配置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的協(xié)議協(xié)商等。
總結一下,OkHttp中,IO相關的組件的其關系大體如下圖所示:
Connection Component
Done。
總結
以上是生活随笔為你收集整理的OkHttp3的连接池及连接建立过程分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Cronet android 设计与实现
- 下一篇: 非对称加密与证书