日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

OkHttp3的连接池及连接建立过程分析

發(fā)布時(shí)間:2024/4/11 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 OkHttp3的连接池及连接建立过程分析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

如我們前面在 OkHttp3 HTTP請(qǐng)求執(zhí)行流程分析 中的分析,OkHttp3通過Interceptor鏈來執(zhí)行HTTP請(qǐng)求,整體的執(zhí)行過程大體如下:


OkHttp Flow

這些Interceptor中每一個(gè)的職責(zé),這里不再贅述。

在OkHttp3中,StreamAllocation是用來建立執(zhí)行HTTP請(qǐng)求所需網(wǎng)絡(luò)設(shè)施的組件,如其名字所顯示的那樣,分配Stream。但它具體做的事情根據(jù)是否設(shè)置了代理,以及請(qǐng)求的類型,如HTTP、HTTPS或HTTP/2的不同而有所不同。代理相關(guān)的處理,包括TCP連接的建立,在 OkHttp3中的代理與路由 一文中有詳細(xì)的說明。

在整個(gè)HTTP請(qǐng)求的執(zhí)行過程中,StreamAllocation 對(duì)象分配的比較早,在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的對(duì)象構(gòu)造過程沒有什么特別的:

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對(duì)象傳遞給后面執(zhí)行的Interceptor。而在RealInterceptorChain中,StreamAllocation對(duì)象并沒有被真正用到。緊跟在RetryAndFollowUpInterceptor之后執(zhí)行的 okhttp3.internal.http.BridgeInterceptor 和 okhttp3.internal.cache.CacheInterceptor,它們的職責(zé)分別是補(bǔ)足用戶創(chuàng)建的請(qǐng)求中缺少的必須的請(qǐng)求頭和處理緩存,也沒有真正用到StreamAllocation對(duì)象。

在OkHttp3的HTTP請(qǐng)求執(zhí)行過程中,okhttp3.internal.connection.ConnectInterceptor和okhttp3.internal.http.CallServerInterceptor是與網(wǎng)絡(luò)交互的關(guān)鍵。

CallServerInterceptor負(fù)責(zé)將HTTP請(qǐng)求寫入網(wǎng)絡(luò)IO流,并從網(wǎng)絡(luò)IO流中讀取服務(wù)器返回的數(shù)據(jù)。而ConnectInterceptor則負(fù)責(zé)為CallServerInterceptor建立可用的連接。此處 可用的 含義主要為,可以直接寫入HTTP請(qǐng)求的數(shù)據(jù):

  • 設(shè)置了HTTP代理的HTTP請(qǐng)求,與代理建立好TCP連接;
  • 設(shè)置了HTTP代理的HTTPS請(qǐng)求,與HTTP服務(wù)器建立通過HTTP代理的隧道連接,并完成TLS握手;
  • 設(shè)置了HTTP代理的HTTP/2請(qǐng)求,與HTTP服務(wù)器建立通過HTTP代理的隧道連接,并完成與服務(wù)器的TLS握手及協(xié)議協(xié)商;
  • 設(shè)置了SOCKS代理的HTTP請(qǐng)求,通過代理與HTTP服務(wù)器建立好連接;
  • 設(shè)置了SOCKS代理的HTTPS請(qǐng)求,通過代理與HTTP服務(wù)器建立好連接,并完成TLS握手;
  • 設(shè)置了SOCKS代理的HTTP/2請(qǐng)求,通過代理與HTTP服務(wù)器建立好連接,并完成與服務(wù)器的TLS握手及協(xié)議協(xié)商;
  • 無代理的HTTP請(qǐng)求,與服務(wù)器建立好TCP連接;
  • 無代理的HTTPS請(qǐng)求,與服務(wù)器建立TCP連接,并完成TLS握手;
  • 無代理的HTTP/2請(qǐng)求,與服務(wù)器建立好TCP連接,完成TLS握手及協(xié)議協(xié)商。

后面我們更詳細(xì)地來看一下這個(gè)過程。

ConnectInterceptor的代碼看上去比較簡(jiǎn)單:

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對(duì)象,執(zhí)行 streamAllocation.newStream() 完成前述所有的連接建立工作,并將這個(gè)過程中創(chuàng)建的用于網(wǎng)絡(luò)IO的RealConnection對(duì)象,以及對(duì)于與服務(wù)器交互最為關(guān)鍵的HttpCodec等對(duì)象傳遞給后面的Interceptor,也就是CallServerInterceptor。

OkHttp3的連接池

在具體地分析 streamAllocation.newStream() 的執(zhí)行過程之前,我們先來看一下OkHttp3的連接池的設(shè)計(jì)實(shí)現(xiàn)。

OkHttp3將客戶端與服務(wù)器之間的連接抽象為Connection/RealConnection,為了管理這些連接的復(fù)用而設(shè)計(jì)了ConnectionPool。共享相同Address的請(qǐng)求可以復(fù)用連接,ConnectionPool實(shí)現(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的容器,且是順序容器,而不是關(guān)聯(lián)容器。ConnectionPool用雙端隊(duì)列Deque<RealConnection>來保存它所管理的所有RealConnection。

ConnectionPool還會(huì)對(duì)連接池中最大的空閑連接數(shù)及連接的保活時(shí)間進(jìn)行控制,maxIdleConnections和keepAliveDurationNs成員分別體現(xiàn)對(duì)最大空閑連接數(shù)及連接保活時(shí)間的控制。這種控制通過匿名的Runnable cleanupRunnable在線程池executor中執(zhí)行,并在向連接池中添加新的RealConnection觸發(fā)。

連接池ConnectionPool的創(chuàng)建

OkHttp3的用戶可以自行創(chuàng)建ConnectionPool,對(duì)最大空閑連接數(shù)及連接的保活時(shí)間進(jìn)行配置,并在OkHttpClient創(chuàng)建期間,將其傳給OkHttpClient.Builder,在OkHttpClient中啟用它。沒有定制連接池的情況下,則在OkHttpClient.Builder構(gòu)造過程中以默認(rèn)參數(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的默認(rèn)構(gòu)造過程如下:

/*** 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);}}

在默認(rèn)情況下,ConnectionPool 最多保存 5個(gè) 處于空閑狀態(tài)的連接,且連接的默認(rèn)保活時(shí)間為 5分鐘

RealConnection的存/取

OkHttp內(nèi)部的組件可以通過put()方法向ConnectionPool中添加RealConnection:

void put(RealConnection connection) {assert (Thread.holdsLock(this));if (!cleanupRunning) {cleanupRunning = true;executor.execute(cleanupRunnable);}connections.add(connection);}

在向ConnectionPool中添加RealConnection時(shí),若發(fā)現(xiàn)cleanupRunnable還沒有運(yùn)行會(huì)觸發(fā)它的運(yùn)行。

cleanupRunnable的職責(zé)本就是清理無效的RealConnection,只要ConnectionPool中存在RealConnection,則這種清理的需求總是存在的,因而這里會(huì)去啟動(dòng)cleanupRunnable。

根據(jù)需要啟動(dòng)了cleanupRunnable之后,將RealConnection添加進(jìn)雙端隊(duì)列connections。

這里先啟動(dòng) cleanupRunnable,后向 connections 中添加RealConnection。有沒有可能發(fā)生:

啟動(dòng)cleanupRunnable之后,向connections中添加RealConnection之前,執(zhí)行 put() 的線程被搶占,cleanupRunnable的線程被執(zhí)行,它發(fā)現(xiàn)connections中沒有任何RealConnection,于是從容地退出而導(dǎo)致后面添加的RealConnection永遠(yuǎn)不會(huì)得得清理。

這樣的情況呢?答案是 不會(huì)。為什么呢?put()執(zhí)行之前總是會(huì)用ConnectionPool對(duì)象鎖來保護(hù),而在ConnectionPool.cleanup()中,遍歷connections也總是會(huì)先對(duì)ConnectionPool對(duì)象加鎖保護(hù)的。即使執(zhí)行 put() 的線程被搶占,cleanupRunnable的線程也會(huì)由于拿不到ConnectionPool對(duì)象鎖而等待 put() 執(zhí)行結(jié)束。

OkHttp內(nèi)部的組件可以通過 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 尋找同時(shí)滿足如下三個(gè)條件的RealConnection:

  • RealConnection的allocations的數(shù)量小于allocationLimit。每個(gè)allocation代表在該RealConnection上正在執(zhí)行的一個(gè)請(qǐng)求。這個(gè)條件用于控制相同連接上,同一時(shí)間執(zhí)行的并發(fā)請(qǐng)求的個(gè)數(shù)。對(duì)于HTTP/2連接而言,allocationLimit限制是在連接建立階段由雙方協(xié)商的。對(duì)于HTTP或HTTPS連接而言,這個(gè)值則總是1。從RealConnection.establishProtocol()可以清晰地看到這一點(diǎn):

    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 描述建立連接所需的配置信息,包括對(duì)端的信息等,不難理解只有所有相關(guān)配置相等時(shí) RealConnection 才是真正能復(fù)用的。具體看一下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; } 這種相等性的條件給人感覺還是蠻苛刻的,特別是對(duì)url的對(duì)比。
    這難免會(huì)讓我們有些擔(dān)心,對(duì) Address 如此苛刻的相等性比較,又有多大的機(jī)會(huì)能復(fù)用連接呢?
    我們的擔(dān)心其實(shí)是多余的。只有在 StreamAllocation.findConnection() 中,會(huì)通過Internal.instance 調(diào)用 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的實(shí)現(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 在構(gòu)造時(shí)由外部傳入。構(gòu)造了StreamAllocation對(duì)象的RetryAndFollowUpInterceptor,其構(gòu)造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 外的所有構(gòu)造參數(shù)均來自于OkHttpClient,而Address的url 字段正是根據(jù)這兩個(gè)參數(shù)構(gòu)造的:

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請(qǐng)求url的 schema + host + port 這三部分的信息,而不包含 path 和 query 等信息。ConnectionPool主要是根據(jù)服務(wù)器的地址來決定復(fù)用的。

  • RealConnection還有可分配的Stream。對(duì)于HTTP或HTTPS而言,不能同時(shí)在相同的連接上執(zhí)行多個(gè)請(qǐng)求。即使對(duì)于HTTP/2而言,StreamID的空間也是有限的,同一個(gè)連接上的StreamID總有分配完的時(shí)候,而在StreamID被分配完了之后,該連接就不能再被使用了。

OkHttp內(nèi)部對(duì)ConnectionPool的訪問總是通過Internal.instance來進(jìn)行。整個(gè)OkHttp中也只有StreamAllocation 存取了 ConnectionPool,也就是我們前面列出的StreamAllocation.findConnection() 方法,相關(guān)的組件之間的關(guān)系大體如下圖:


OkHttp Connection Pool

RealConnection的清理

ConnectionPool 中對(duì)于 RealConnection 的清理在put()方法中觸發(fā),執(zhí)行 cleanupRunnable 來完成清理動(dòng)作:

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í)行一次清理動(dòng)作,都會(huì)等待一段時(shí)間再次執(zhí)行,而具體等待的時(shí)長(zhǎng)由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)時(shí)間最長(zhǎng)的一個(gè)RealConnection,然后根據(jù)查找結(jié)果的不同,分為以下幾種情況處理:

  • 找到一個(gè)處于空閑狀態(tài)的RealConnection,且該RealConnection處于空閑狀態(tài)的時(shí)間超出了設(shè)置的保活時(shí)間,或者當(dāng)前ConnectionPool中處于空閑狀態(tài)的連接數(shù)超出了設(shè)置的最大空閑連接數(shù),將該RealConnection從connections中移除,并關(guān)閉該RealConnection關(guān)聯(lián)的底層socket,然后返回0,以此請(qǐng)求cleanupRunnable立即再次檢查所有的連接。
  • 找到一個(gè)處于空閑狀態(tài)的RealConnection,但該RealConnection處于空閑狀態(tài)的時(shí)間尚未超出設(shè)置的保活時(shí)間,且當(dāng)前ConnectionPool中處于空閑狀態(tài)的連接數(shù)尚未超出設(shè)置的最大空閑連接數(shù),則返回保活時(shí)間與該RealConnection處于空閑狀態(tài)的時(shí)間之間的差值,請(qǐng)求cleanupRunnable等待這么長(zhǎng)一段時(shí)間之后再次檢查所有的連接。
  • 沒有找到處于空閑狀態(tài)的連接,但找到了使用中的連接,則返回保活時(shí)間,請(qǐng)求cleanupRunnable等待這么長(zhǎng)一段時(shí)間之后再次檢查所有的連接。
  • 沒有找到處于空閑狀態(tài)的連接,也沒有找到使用中的連接,也就意味著連接池中尚沒有任何連接,則將 cleanupRunning 置為false,并返回 -1,請(qǐng)求 cleanupRunnable 退出。

cleanup() 通過 pruneAndGetAllocationCount() 檢查正在使用一個(gè)特定連接的請(qǐng)求個(gè)數(shù),并以此來判斷一個(gè)連接是否處于空閑狀態(tài)。后者通遍歷 connection.allocations 并檢查每個(gè)元素的StreamAllocation 的狀態(tài),若StreamAllocation 為空,則認(rèn)為是發(fā)現(xiàn)了一個(gè)leak,它會(huì)更新連接的空閑時(shí)間為當(dāng)前時(shí)間減去保活時(shí)間并返回0,以此請(qǐng)求 cleanup() 立即關(guān)閉、清理掉該 leak 的連接。

ConnectionPool的用戶接口

OkHttp的用戶可以自己創(chuàng)建 ConnectionPool 對(duì)象,這個(gè)類也提供了一些用戶接口以方便用戶獲取空閑狀態(tài)的連接數(shù)、總的連接數(shù),以及手動(dòng)清除空閑狀態(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());}}

新建流

回到新建流的過程,連接建立的各種細(xì)節(jié)處理都在這里。 StreamAllocation.newStream() 完成新建流的動(dòng)作:

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ù)的組件,它會(huì)將請(qǐng)求的數(shù)據(jù)序列化之后發(fā)送到網(wǎng)絡(luò),并將接收的數(shù)據(jù)反序列化為應(yīng)用程序方便操作的格式。在 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ā)送請(qǐng)求而提供的,寫入請(qǐng)求頭部。
  • 為發(fā)送請(qǐng)求而提供的,創(chuàng)建請(qǐng)求體,以用于發(fā)送請(qǐng)求體數(shù)據(jù)。
  • 為發(fā)送請(qǐng)求而提供的,結(jié)束請(qǐng)求發(fā)送。
  • 為獲得響應(yīng)而提供的,讀取響應(yīng)頭部。
  • 為獲得響應(yīng)而提供的,打開請(qǐng)求體,以用于后續(xù)獲取請(qǐng)求體數(shù)據(jù)。
  • 取消請(qǐng)求執(zhí)行。

StreamAllocation.newStream() 主要做的事情正是創(chuàng)建HttpCodec。StreamAllocation.newStream() 根據(jù) OkHttpClient中的設(shè)置,連接超時(shí)、讀超時(shí)、寫超時(shí)及連接失敗是否重試,調(diào)用 findHealthyConnection() 完成 連接,即RealConnection 的創(chuàng)建。然后根據(jù)HTTP協(xié)議的版本創(chuàng)建Http1Codec或Http2Codec。

findHealthyConnection() 根據(jù)目標(biāo)服務(wù)器地址查找一個(gè)連接,如果它是可用的就直接返回,如果不可用則會(huì)重復(fù)查找直到找到一個(gè)可用的為止。在連接已被破壞而不可用時(shí),還會(huì)釋放連接:

/*** 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;}}

連接是否可用的標(biāo)準(zhǔn)如下 (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;}

首先要可以進(jìn)行IO,此外對(duì)于HTTP/2,只要http2Connection存在即可。如我們前面在ConnectInterceptor 中看到的,如果HTTP請(qǐng)求的method不是 "GET" ,doExtensiveChecks為true時(shí),需要做額外的檢查。

findHealthyConnection() 通過 findConnection()查找一個(gè)連接:

/*** Returns a connection to host a new stream. This prefers the existing connection if it exists,* then the pool, finally building a new connection.*/private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,boolean connectionRetryEnabled) throws IOException {Route selectedRoute;synchronized (connectionPool) {if (released) throw new IllegalStateException("released");if (codec != null) throw new IllegalStateException("codec != null");if (canceled) throw new IOException("Canceled");RealConnection allocatedConnection = this.connection;if (allocatedConnection != null && !allocatedConnection.noNewStreams) {return allocatedConnection;}// Attempt to get a connection from the pool.RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);if (pooledConnection != null) {this.connection = pooledConnection;return pooledConnection;}selectedRoute = route;}if (selectedRoute == null) {selectedRoute = routeSelector.next();synchronized (connectionPool) {route = selectedRoute;refusedStreamCount = 0;}}RealConnection newConnection = new RealConnection(selectedRoute);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() 返回一個(gè)用于流執(zhí)行底層IO的連接。這個(gè)方法優(yōu)先復(fù)用已經(jīng)創(chuàng)建的連接;在沒有可復(fù)用連接的情況下新建一個(gè)。

在同一次 newStream() 的執(zhí)行過程中,有沒有可能兩次執(zhí)行 findConnection() ,第一次connection 字段為空,第二次不為空?這個(gè)地方對(duì)connection字段的檢查,看起來有點(diǎn)多余。執(zhí)行 findConnection() 時(shí),connection 不為空的話,意味著 codec 不為空,而在方法的開始處已經(jīng)有對(duì)codec字段的狀態(tài)做過檢查。真的是這樣的嗎?

答案當(dāng)然是否定的。同一次 newStream() 的執(zhí)行過程中,沒有可能兩次執(zhí)行findConnection(),第一次connection字段為空,第二次不為空,然而一個(gè)HTTP請(qǐng)求的執(zhí)行過程,又不是一定只調(diào)用一次newStream()。

newStream()的直接調(diào)用者是ConnectInterceptor,所有的Interceptor用RealInterceptorChain鏈起來,在Interceptor鏈中,ConnectInterceptor 和RetryAndFollowUpInterceptor 隔著 CacheInterceptor 和 BridgeInterceptor 。然而newStream() 如果出錯(cuò)的話,則是會(huì)通過拋出Exception返回到RetryAndFollowUpInterceptor 來處理錯(cuò)誤的。

RetryAndFollowUpInterceptor 中會(huì)嘗試基于相同的 StreamAllocation 對(duì)象來恢復(fù)對(duì)HTTP請(qǐng)求的處理。RetryAndFollowUpInterceptor 通過 hasMoreRoutes() 等方法,來檢查StreamAllocation 對(duì)象的狀態(tài),通過 streamFailed(IOException e)、release()、streamFinished(boolean noNewStreams, HttpCodec codec)等方法來reset StreamAllocation對(duì)象的一些狀態(tài)。

回到StreamAllocation的 findConnection()方法。沒有連接存在,且連接池中也沒有找到所需的連接時(shí),它會(huì)新建一個(gè)連接。通過如下的步驟新建連接:

  • 為連接選擇一個(gè)Route。
  • 新建一個(gè)RealConnection對(duì)象。public RealConnection(Route route) {this.route = route; }
  • 將當(dāng)前StreamAllocation對(duì)象的引用保存進(jìn)RealConnection的allocations。如我們前面在分析ConnectionPool時(shí)所見的那樣,這主要是為了追蹤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保存進(jìn)連接池。
  • 保存對(duì)RealConnection的引用。
  • 檢查請(qǐng)求是否被取消,若取消,則拋出異常。
  • 建立連接。
  • 更新RouteDatabase中Route的狀態(tài)。

ConnectionSpec

在OkHttp中,ConnectionSpec用于描述傳輸HTTP流量的socket連接的配置。對(duì)于https請(qǐng)求,這些配置主要包括協(xié)商安全連接時(shí)要使用的TLS版本號(hào)和密碼套件,是否支持TLS擴(kuò)展等;對(duì)于http請(qǐng)求則幾乎不包含什么信息。

OkHttp有預(yù)定義幾組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();

預(yù)定義的這些ConnectionSpec被組織為默認(rèn)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的用戶可以在構(gòu)造OkHttpClient的過程中提供自己的ConnectionSpec集合。默認(rèn)情況下OkHttpClient會(huì)使用前面看到的默認(rèn)ConnectionSpec集合。

在RetryAndFollowUpInterceptor中創(chuàng)建Address時(shí),ConnectionSpec集合被從OkHttpClient獲取,并由Address引用。

OkHttp還提供了ConnectionSpecSelector,用以從ConnectionSpec集合中選擇與SSLSocket匹配的ConnectionSpec,并對(duì)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 標(biāo)識(shí),它表示在整個(gè)連接建立,及可能的協(xié)議協(xié)商過程中選擇的所要使用的協(xié)議。
  • 根據(jù)ConnectionSpec集合connectionSpecs構(gòu)造ConnectionSpecSelector。
  • 若請(qǐng)求不是安全的請(qǐng)求,會(huì)對(duì)請(qǐng)求再執(zhí)行一些額外的限制。這些限制包括:
    • ConnectionSpec集合中必須要包含ConnectionSpec.CLEARTEXT。這也就是說,OkHttp的用戶可以通過為OkHttpClient設(shè)置不包含ConnectionSpec.CLEARTEXT的ConnectionSpec集合來禁用所有的明文請(qǐng)求。
    • 平臺(tái)本身的安全策略允許向相應(yīng)的主機(jī)發(fā)送明文請(qǐng)求。對(duì)于Android平臺(tái)而言,這種安全策略主要由系統(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(); } } 平臺(tái)的這種安全策略并不是每個(gè)Android版本都有的。Android 6.0之后存在這種控制。
  • 根據(jù)請(qǐng)求是否需要建立隧道連接,而分別執(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; } 即對(duì)于設(shè)置了HTTP代理,且安全的連接 (SSL) 需要請(qǐng)求代理服務(wù)器建立一個(gè)到目標(biāo)HTTP服務(wù)器的隧道連接,客戶端與HTTP代理建立TCP連接,以此請(qǐng)求HTTP代理服務(wù)在客戶端與HTTP服務(wù)器之間進(jìn)行數(shù)據(jù)的盲轉(zhuǎn)發(fā)。

建立隧道連接

建立隧道連接的過程如下:

  • 構(gòu)造一個(gè) 建立隧道連接 請(qǐng)求。
  • 與HTTP代理服務(wù)器建立TCP連接。
  • 創(chuàng)建隧道。這主要是將 建立隧道連接 請(qǐng)求發(fā)送給HTTP代理服務(wù)器,并處理它的響應(yīng)。
  • 重復(fù)上面的第2和第3步,知道建立好了隧道連接。至于為什么要重復(fù)多次,及關(guān)于代理認(rèn)證的內(nèi)容,可以參考代理協(xié)議相關(guān)的內(nèi)容。
  • 建立協(xié)議。
  • 關(guān)于建立隧道連接更詳細(xì)的過程可參考 OkHttp3中的代理與路由 的相關(guān)部分。

    建立普通連接

    建立普通連接的過程比較直接:

    /** 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);}
  • 建立一個(gè)TCP連接。
  • 建立協(xié)議。
  • 更詳細(xì)的過程可參考 OkHttp3中的代理與路由 的相關(guān)部分。

    建立協(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;}}
  • 對(duì)于加密的數(shù)據(jù)傳輸,創(chuàng)建TLS連接。對(duì)于明文傳輸,則設(shè)置protocol和socket。
    socket指向直接與應(yīng)用層,如HTTP或HTTP/2,交互的Socket:
    對(duì)于明文傳輸沒有設(shè)置HTTP代理的HTTP請(qǐng)求,它是與HTTP服務(wù)器之間的TCP socket;
    對(duì)于明文傳輸設(shè)置了HTTP代理或SOCKS代理的HTTP請(qǐng)求,它是與代理服務(wù)器之間的TCP socket;
    對(duì)于加密傳輸沒有設(shè)置HTTP代理服務(wù)器的HTTP或HTTP2請(qǐng)求,它是與HTTP服務(wù)器之間的SSLScoket;
    對(duì)于加密傳輸設(shè)置了HTTP代理服務(wù)器的HTTP或HTTP2請(qǐng)求,它是與HTTP服務(wù)器之間經(jīng)過了代理服務(wù)器的SSLSocket,一個(gè)隧道連接;
    對(duì)于加密傳輸設(shè)置了SOCKS代理的HTTP或HTTP2請(qǐng)求,它是一條經(jīng)過了代理服務(wù)器的SSLSocket連接。

  • 對(duì)于HTTP/2,會(huì)建立HTTP/2連接,并進(jìn)一步協(xié)商連接參數(shù),如連接上可同時(shí)執(zhí)行的并發(fā)請(qǐng)求數(shù)等。而對(duì)于非HTTP/2,則將連接上可同時(shí)執(zhí)行的并發(fā)請(qǐng)求數(shù)設(shè)置為1。

  • 建立TLS連接

    進(jìn)一步來看建立協(xié)議過程中,為安全請(qǐng)求所做的建立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連接是對(duì)原始的TCP連接的一個(gè)封裝,以提供TLS握手,及數(shù)據(jù)收發(fā)過程中的加密解密等功能。在Java中,用SSLSocket來描述。上面建立TLS連接的過程大體為:

  • 用SSLSocketFactory基于原始的TCP Socket,創(chuàng)建一個(gè)SSLSocket。
  • 配置SSLSocket。
  • 在前面選擇的ConnectionSpec支持TLS擴(kuò)展參數(shù)時(shí),配置TLS擴(kuò)展參數(shù)。
  • 啟動(dòng)TLS握手。
  • TLS握手完成之后,獲取握手信息。
  • 對(duì)TLS握手過程中傳回來的證書進(jìn)行驗(yàn)證。
  • 檢查證書釘扎。
  • 在前面選擇的ConnectionSpec支持TLS擴(kuò)展參數(shù)時(shí),獲取TLS握手過程中順便完成的協(xié)議協(xié)商過程所選擇的協(xié)議。這個(gè)過程主要用于HTTP/2的ALPN擴(kuò)展。
  • OkHttp主要使用Okio來做IO操作,這里會(huì)基于前面獲取的SSLSocket創(chuàng)建用于執(zhí)行IO的BufferedSource和BufferedSink等,并保存握手信息及所選擇的協(xié)議。
  • 具體來看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;}

    這個(gè)過程分為如下的兩個(gè)步驟:

  • 從為OkHttp配置的ConnectionSpec集合中選擇一個(gè)與SSLSocket兼容的一個(gè)。SSLSocket與ConnectionSpec兼容的標(biāo)準(zhǔn)如下:

    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應(yīng)用在SSLSocket上。OkHttpClient中ConnectionSpec的應(yīng)用:

    @Overridepublic 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版本及密碼套件之間的交集,構(gòu)造新的ConnectionSpec。
    • 重新為SSLSocket設(shè)置啟用的TLS版本及密碼套件為上一步求得的交集。
  • 我們知道HTTP/2的協(xié)議協(xié)商主要是利用了TLS的ALPN擴(kuò)展來完成的。這里再來詳細(xì)的看一下配置TLS擴(kuò)展的過程。對(duì)于Android平臺(tái)而言,這部分邏輯在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擴(kuò)展相關(guān)的方法不是SSLSocket接口的標(biāo)準(zhǔn)方法,不同的SSL/TLS實(shí)現(xiàn)庫(kù)對(duì)這些接口的支持程度不一樣,因而這里通過反射機(jī)制調(diào)用TLS擴(kuò)展相關(guān)的方法。

    這里主要配置了3個(gè)TLS擴(kuò)展,分別是session tickets,SNI和ALPN。session tickets用于會(huì)話回復(fù),SNI用于支持單個(gè)主機(jī)配置了多個(gè)域名的情況,ALPN則用于HTTP/2的協(xié)議協(xié)商。可以看到為SNI設(shè)置的hostname最終來源于Url,也就意味著使用HttpDns時(shí),如果直接將IP地址替換原來Url中的域名來發(fā)起HTTPS請(qǐng)求的話,SNI將是IP地址,這有可能使服務(wù)器下發(fā)不恰當(dāng)?shù)淖C書。

    TLS擴(kuò)展相關(guān)方法的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的過程類似,同樣利用反射調(diào)用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請(qǐng)求,包括設(shè)置了代理的明文HTTP請(qǐng)求,設(shè)置了代理的HTTPS請(qǐng)求,設(shè)置了代理的HTTP/2請(qǐng)求,無代理的明文HTTP請(qǐng)求,無代理的HTTPS請(qǐng)求,無代理的HTTP/2請(qǐng)求的連接建立過程,其中包括TLS的握手,HTTP/2的協(xié)議協(xié)商等。

    總結(jié)一下,OkHttp中,IO相關(guān)的組件的其關(guān)系大體如下圖所示:


    Connection Component

    Done。

    總結(jié)

    以上是生活随笔為你收集整理的OkHttp3的连接池及连接建立过程分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。