Okhttp系列文章:
你想要的系列:網絡請求框架OkHttp3全解系列 - (一)OkHttp的基本使用
你想要的系列:網絡請求框架OkHttp3全解系列 - (二)OkHttp的工作流程分析
你想要的系列:網絡請求框架OkHttp3全解系列 - (三)攔截器詳解1:重試重定向、橋、緩存(重點)
你想要的系列:網絡請求框架OkHttp3全解系列 - (四)攔截器詳解2:連接、請求服務(重點)
在本系列的上一篇文章你想要的系列:網絡請求框架OkHttp3全解系列 - (三)攔截器詳解1:重試重定向、橋、緩存(重點)中,我們分析了OkHttp攔截器鏈中的前三個攔截器:RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor,它們在請求建立連接之前做了一些預處理。
如果請求經過這三個攔截器後,要繼續往下傳遞,說明是需要進行網絡請求的(緩存不能直接滿足),也就是今天要分析的內容——剩下的兩個攔截器:ConnectInterceptor、CallServerInterceptor,分別負責 連接建立、請求服務讀寫。
背景 - HTTP協議發展
在講解攔截器之前,我們有必要了解http協議相關背景知識,因爲okhttp的網絡連接正是基於此實現的。HTTP協議經歷了以下三個版本階段。
HTTP1.0
在HTTP1.0中,一次請求 會建立一個TCP連接,請求完成後主動斷開連接。這種方法的好處是簡單,各個請求互不干擾。
但每次請求都會經歷 3次握手、2次或4次揮手的連接建立和斷開過程——極大影響網絡效率和系統開銷。
HTTP1.1
在HTTP1.1中,解決了HTTP1.0中連接不能複用的問題,支持持久連接——使用keep-alive機制:一次HTTP請求結束後不會立即斷開TCP連接,如果此時有新的HTTP請求,且其請求的Host同上次請求相同,那麼會直接複用TCP連接。這樣就減少了建立和關閉連接的消耗和延遲。keep-alive機制在HTTP1.1中是默認打開的——即在請求頭添加:connection:keep-alive。(keep-alive不會永久保持連接,它有一個保持時間,可在不同的服務器軟件(如Apache)中設定這個時間)
HTTP2.0
HTTP1.1中,連接的複用是串行的:一個請求建立了TCP連接,請求完成後,下一個相同host的請求繼續使用這個連接。 但客戶端想 同時 發起多個並行請求,那麼必須建立多個TCP連接。將會產生網絡延遲、增大網路開銷。
並且HTTP1.1不會壓縮請求和響應報頭,導致了不必要的網絡流量;HTTP1.1不支持資源優先級導致底層TCP連接利用率低下。在HTTP2.0中,這些問題都會得到解決,HTTP2.0主要有以下特性:
- 新的二進制格式(Binary Format):http/1.x使用的是明文協議,其協議格式由三部分組成:request line,header,body,其協議解析是基於文本,但是這種方式存在天然缺陷,文本的表現形式有多樣性,要做到健壯性考慮的場景必然很多,二進制則不同,只認0和1的組合;基於這種考慮,http/2.0的協議解析決定採用二進制格式,實現方便且健壯
- 多路複用(MultiPlexing):即連接共享,使用streamId用來區分請求,一個request對應一個stream並分配一個id,這樣一個TCP連接上可以有多個stream,每個stream的frame可以隨機的混雜在一起,接收方可以根據stream id將frame再歸屬到各自不同的request裏面
- 優先級和依賴(Priority、Dependency):每個stream都可以設置優先級和依賴,優先級高的stream會被server優先處理和返回給客戶端,stream還可以依賴其它的sub streams;優先級和依賴都是可以動態調整的,比如在APP上瀏覽商品列表,用戶快速滑動到底部,但是前面的請求已經發出,如果不把後面的優先級設高,那麼當前瀏覽的圖片將會在最後展示出來,顯然不利於用戶體驗
- header壓縮:http2.0使用encoder來減少需要傳輸的header大小,通訊雙方各自cache一份header fields表,既避免了重複header的傳輸,又減小了需要傳輸的大小
- 重置連接:很多APP裏都有停止下載圖片的需求,對於http1.x來說,是直接斷開連接,導致下次再發請求必須重新建立連接;http2.0引入RST_STREAM類型的frame,可以在不斷開連接的前提下取消某個request的stream
其中涉及了兩個新的概念:
- 數據流-stream:基於TCP連接之上的邏輯雙向字節流,用於承載雙向消息,對應一個請求及其響應。客戶端每發起一個請求就建立一個數據流,後續該請求及其響應的所有數據都通過該數據流傳輸。每個數據流都有一個唯一的標識符和可選的優先級信息。
- 幀-frame:HTTP/2的最小數據切片單位,承載着特定類型的數據,例如 HTTP 標頭、消息負載,等等。 來自不同數據流的幀可以交錯發送,然後再根據每個幀頭的數據流標識符重新組裝,從而在宏觀上實現了多個請求或響應並行傳輸的效果。
這裏的 多路複用機制 就實現了 在同一個TCP連接上 多個請求 並行執行。
無論是HTTP1.1的Keep-Alive機制還是HTTP2.0的多路複用機制,在實現上都需要引入連接池來維護網絡連接。下面就開始分析 OkHttp中的連接池實現——連接攔截器ConnectInterceptor。
ConnectInterceptor
連接攔截器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();
Transmitter transmitter = realChain.transmitter();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
return realChain.proceed(request, transmitter, exchange);
}
}
代碼很少,主要是使用transmitter.newExchange獲取Exchange實例,並作爲參數調用攔截器鏈的proceed的方法。注意到前面分析過的攔截器調用的proceed方法是一個參數的,而這裏是三個參數的。這是因爲下一個攔截器(如果沒有配置網絡攔截器的話,就是CallServerInterceptor,也是最後一個)需要進行真正的網絡IO操作,而 Exchange(意爲交換)主要作用就是真正的IO操作:寫入請求、讀取響應(會在下一個攔截器做介紹)。
實際上獲取Exchange實例的邏輯處理都封裝在Transmitter中了。前面的文章提到過Transmitter,它是“發射器”,是把 請求 從應用端 發射到 網絡層,它持有請求的 連接、請求、響應 和 流,一個請求對應一個Transmitter實例,一個數據流。下面就看下它的newExchange方法:
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
synchronized (connectionPool) {
if (noMoreExchanges) {
throw new IllegalStateException("released");
}
if (exchange != null) {
throw new IllegalStateException("cannot make a new request because the previous response "
+ "is still open: please call response.close()");
}
}
ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
synchronized (connectionPool) {
this.exchange = result;
this.exchangeRequestDone = false;
this.exchangeResponseDone = false;
return result;
}
}
若是第一次請求,前面兩個if是沒走進去的。接着看到使用exchangeFinder的find方法獲取到了ExchangeCodec實例,然後作爲參數構建了Exchange實例,並返回。嗯,看起來也很簡單的樣子。 注意到這個方法裏涉及了連接池RealConnectionPool、交換尋找器ExchangeFinder、交換編解碼ExchangeCodec、交換管理Exchange這幾個類(翻譯成這樣盡力了😊,意會吧)。
- RealConnectionPool,連接池,負責管理請求的連接,包括新建、複用、關閉,理解上類似線程池。
- ExchangeCodec,接口類,負責真正的IO操作—寫請求、讀響應,實現類有Http1ExchangeCodec、Http2ExchangeCodec,分別對應HTTP1.1協議、HTTP2.0協議。
- Exchange,管理IO操作,可以理解爲 數據流,是ExchangeCodec的包裝,增加了事件回調;一個請求對應一個Exchange實例。傳給下個攔截器CallServerInterceptor使用。
- ExchangeFinder,(從連接池中)尋找可用TCP連接,然後通過連接得到ExchangeCodec。
ExchangeFinder
ExchangeFinder的作用從名字就可以看出——Exchange尋找者,本質是爲請求尋找一個TCP連接。如果已有可用連接就直接使用,沒有則打開一個新的連接。 一個網絡請求的執行,需要先有一個指向目標服務的TCP連接,然後再進行寫請求、讀響應的IO操作。ExchangeFinder是怎麼尋找的呢?繼續往下看~
我們先看exchangeFinder初始化的地方:
public void prepareToConnect(Request request) {
...
this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
call, eventListener);
}
看到這裏應該會想起上一篇文章中分析RetryAndFollowUpInterceptor時提到過,prepareToConnect這個方法作用是連接準備,就是創建了ExchangeFinder實例。主要到傳入的參數有connectionPool、createAddress方法返回的Address、call、eventListener。connectionPool是連接池,稍後分析,先看下createAddress方法:
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());
}
使用url和client配置 創建一個Address實例。Address意思是指向服務的連接的地址,可以理解爲請求地址及其配置。Address有一個重要作用:相同Address的HTTP請求 共享 相同的連接。這可以作爲前面提到的 HTTP1.1和HTTP2.0 複用連接 的請求的判斷。
回頭看exchangeFinder的find方法
public ExchangeCodec find(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
//找到一個健康的連接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
//利用連接實例化ExchangeCodec對象,如果是HTTP/2返回Http2ExchangeCodec,否則返回Http1ExchangeCodec
return resultConnection.newCodec(client, chain);
} catch (RouteException e) {
trackFailure();
throw e;
} catch (IOException e) {
trackFailure();
throw new RouteException(e);
}
}
主要就是通過findHealthyConnection方法獲取連接RealConnection實例,然後用RealConnection的newCodec方法獲取了ExchangeCodec實例,如果是HTTP/2返回Http2ExchangeCodec,否則返回Http1ExchangeCodec,然後返回。
findHealthyConnection方法名透露着 就是去尋找可用TCP連接的,而我們猜測這個方法內部肯定和連接池ConnectionPool有緊密的關係。接着跟進findHealthyConnection方法:
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
//找連接
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// 是新連接 且不是HTTP2.0 就不用體檢
synchronized (connectionPool) {
if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
return candidate;
}
}
// 體檢不健康,繼續找
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
//標記不可用
candidate.noNewExchanges();
continue;
}
return candidate;
}
}
循環尋找連接:如果是不健康的連接,標記不可用(標記後會移除,後面講連接池會講到),然後繼續找。健康是指連接可以承載新的數據流,socket是連接狀態。我們跟進findConnection方法,看看到底是怎麼 找連接 的:
//爲承載新的數據流 尋找 連接。尋找順序是 已分配的連接、連接池、新建連接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
RealConnection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
//請求已被取消(Call的cancel方法->transmitter的cancel方法),拋異常
if (transmitter.isCanceled()) throw new IOException("Canceled");
hasStreamFailure = false;
// 嘗試使用 已給數據流分配的連接.(例如重定向請求時,可以複用上次請求的連接)
releasedConnection = transmitter.connection;
//有已分配的連接,但已經被限制承載新的數據流,就嘗試釋放掉(如果連接上已沒有數據流),並返回待關閉的socket。
toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
? transmitter.releaseConnectionNoEvents()
: null;
if (transmitter.connection != null) {
// 不爲空,說明上面沒有釋放掉,那麼此連接可用
result = transmitter.connection;
releasedConnection = null;
}
if (result == null) {
// 沒有已分配的可用連接,就嘗試從連接池獲取。(連接池稍後詳細講解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
foundPooledConnection = true;
result = transmitter.connection;
} else if (nextRouteToTry != null) {
selectedRoute = nextRouteToTry;//有可嘗試的路由
nextRouteToTry = null;
} else if (retryCurrentRoute()) {
selectedRoute = transmitter.connection.route();
}
}
}
closeQuietly(toClose);//(如果有)關閉待關閉的socket
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);//(如果有)回調連接釋放事件
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);//(如果有)回調(從連接池)獲取連接事件
}
if (result != null) {
// 如果有已分配可用連接 或 從連接池獲取到連接,結束! 沒有 就走下面的新建連接過程。
return result;
}
// 如果需要路由信息,就獲取。是阻塞操作
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
List<Route> routes = null;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
if (newRouteSelection) {
//現在有了IP地址,再次嘗試從連接池獲取。可能會因爲連接合並而匹配。(這裏傳入了routes,上面的傳的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(
address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
//第二次連接池也沒找到,就新建連接
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
result = new RealConnection(connectionPool, selectedRoute);
connectingConnection = result;
}
}
// 如果第二次從連接池的嘗試成功了,結束,因爲連接池中的連接是已經和服務器建立連接的
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// 第二次沒成功,就把新建的連接,進行TCP + TLS 握手,與服務端建立連接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
connectionPool.routeDatabase.connected(result.route());//從失敗名單中移除
Socket socket = null;
synchronized (connectionPool) {
connectingConnection = null;
// 最後一次嘗試從連接池獲取,注意最後一個參數爲true,即要求 多路複用(http2.0)
//意思是,如果本次是http2.0,那麼爲了保證 多路複用性,(因爲上面的握手操作不是線程安全)會再次確認連接池中此時是否已有同樣連接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果獲取到,就關閉我們創建裏的連接,返回獲取的連接
result.noNewExchanges = true;
socket = result.socket();
result = transmitter.connection;
// 那麼這個剛剛連接成功的路由 就可以 用作下次 嘗試的路由
nextRouteToTry = selectedRoute;
} else {
//最後一次嘗試也沒有的話,就把剛剛新建的連接存入連接池
connectionPool.put(result);
transmitter.acquireConnectionNoEvents(result);//把連接賦給transmitter
}
}
closeQuietly(socket);//如果剛剛建立的連接沒用到,就關閉
eventListener.connectionAcquired(call, result);
return result;
}
代碼看着很長,已經加了註釋,方法目的就是 爲 承載新的數據流 尋找 連接。尋找順序是 已分配的連接、連接池、新建連接。梳理如下:
- 首先會嘗試使用 已給數據流分配的連接。(已分配連接的情況例如重定向時的再次請求,說明上次已經有了連接)
- 若沒有 已分配的可用連接,就嘗試從連接池中 匹配獲取。因爲此時沒有路由信息,所以匹配條件:address一致——host、port、代理等一致,且 匹配的連接可以接受新的數據流。
- 若從連接池沒有獲取到,則取下一個代理的路由信息(多個Route,即多個IP地址),再次嘗試從連接池獲取,此時可能因爲連接合並而匹配到。
- 若第二次也沒有獲取到,就創建RealConnection實例,進行TCP + TLS 握手,與服務端建立連接。
- 此時爲了確保Http2.0連接的多路複用性,會第三次從連接池匹配。因爲新建立的連接的握手過程是非線程安全的,所以此時可能連接池新存入了相同的連接。
- 第三次若匹配到,就使用已有連接,釋放剛剛新建的連接;若未匹配到,則把新連接存入連接池並返回。
流程圖如下:
看到這裏,小盆友,你是否有很多問號?
- 開始若有了已分配的連接,但已經被限制承載新的數據流,是如何釋放的呢?
- 代理路由信息是如何獲取的呢?
- 如何從連接池獲取連接的?三次有什麼不同?
沒關係,慢慢來,我們先看第二個問號,代理路由信息的獲取。
RouteSelector
先來看下Route類:
public final class Route {
final Address address;
final Proxy proxy;//代理
final InetSocketAddress inetSocketAddress;//連接目標地址
public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
...
this.address = address;
this.proxy = proxy;
this.inetSocketAddress = inetSocketAddress;
}
Route,通過代理服務器信息 proxy、連接目標地址 InetSocketAddress 來描述一條 連接服務器的具體路由。
- proxy代理:可以爲客戶端顯式配置代理服務器。否則,將使用ProxySelector代理選擇器。可能會返回多個代理。
- IP地址:無論是直連還是代理,打開socket連接都需要IP地址。 DNS服務可能返回多個IP地址嘗試。
上面分析的findConnection方法中是使用routeSelection.getAll()獲取Route集合routes,而routeSelection是通過routeSelector.next()獲取,routeSelector是在ExchangeFinder的構造方法內創建的,也就是說routeSelector在RetryAndFollowUpInterceptor中就創建了,那麼我們看下RouteSelector:
RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
EventListener eventListener) {
this.address = address;
this.routeDatabase = routeDatabase;//連接池中的路由黑名單(連接失敗的路由)
this.call = call;
this.eventListener = eventListener;
resetNextProxy(address.url(), address.proxy());
}
//收集代理服務器
private void resetNextProxy(HttpUrl url, Proxy proxy) {
if (proxy != null) {
// 若指定了代理,那麼就這一個。(就是初始化OkhttpClient時配置的)
proxies = Collections.singletonList(proxy);
} else {
//沒配置就使用ProxySelector獲取代理(若初始化OkhttpClient時沒有配置ProxySelector,會使用系統默認的)
List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
? Util.immutableList(proxiesOrNull)
: Util.immutableList(Proxy.NO_PROXY);
}
nextProxyIndex = 0;
}
注意到RouteSelector的構造方法中傳入了routeDatabase,是連接失敗的路由黑名單(後面連接池也會講到),並使用resetNextProxy方法獲取代理服務器列表:若沒有指定proxy就是用ProxySelector獲取proxy列表(若沒有配置ProxySelector會使用系統默認)。接着看next方法:
//收集代理的路由信息
public Selection next() throws IOException {
if (!hasNext()) {//還有下一個代理
throw new NoSuchElementException();
}
List<Route> routes = new ArrayList<>();
while (hasNextProxy()) {
Proxy proxy = nextProxy();
//遍歷proxy經DNS後的所有IP地址,組裝成Route
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
if (routeDatabase.shouldPostpone(route)) {//此路由在黑名單中,存起來最後嘗試
postponedRoutes.add(route);
} else {
routes.add(route);
}
}
if (!routes.isEmpty()) {
break;
}
}
if (routes.isEmpty()) {
// 若沒有拿到路由,就嘗試上面存的黑名單的路由
routes.addAll(postponedRoutes);
postponedRoutes.clear();
}
//routes包裝成Selection返回
return new Selection(routes);
}
next方法主要就是獲取下一個代理Proxy的代理信息,即多個路由。具體是在resetNextInetSocketAddress方法中實現,主要是對代理服務地址進行DNS解析獲取多個IP地址,這裏就不展開了,具體可以參考OkHttp中的代理和路由。
好了,到這裏 就解決了第二個問號。其他兩個問號涉及 連接池RealConnectionPool、連接RealConnection,下面就來瞅瞅。
ConnectionPool
ConnectionPool,即連接池,用於管理http1.1/http2.0連接重用,以減少網絡延遲。相同Address的http請求可以共享一個連接,ConnectionPool就是實現了連接的複用。
public final class ConnectionPool {
final RealConnectionPool delegate;
//最大空閒連接數5,最大空閒時間5分鐘
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
}
//返回空閒連接數
public int idleConnectionCount() {
return delegate.idleConnectionCount();
}
//返回池子中的連接數
public int connectionCount() {
return delegate.connectionCount();
}
//關閉並移除所以空閒連接
public void evictAll() {
delegate.evictAll();
}
}
ConnectionPool看起來比較好理解,默認配置是最大空閒連接數5,最大空閒時間5分鐘(即一個連接空閒時間超過5分鐘就移除),我們也可以在初始化okhttpClient時進行不同的配置。需要注意的是ConnectionPool是用於應用層,實際管理者是RealConnectionPool。RealConnectionPool是okhttp內部真實管理連接的地方。
連接池對連接的管理無非是 存、取、刪,上面的兩個問號分別對應 刪、取,跟進RealConnectionPool我們一個個看:
存
private final Deque<RealConnection> connections = new ArrayDeque<>();
private final Runnable cleanupRunnable = () -> {
//循環清理
while (true) {
//清理
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (RealConnectionPool.this) {
try {
//下一次清理之前的等待
RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
};
//存
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
connections是用於存連接的隊列Deque。看到在add之前 使用線程池executor執行了cleanupRunnable,意思是清理連接,爲啥要清理呢?上面提到過 連接池有 最大空閒連接數、最大空閒時間的限制,所以不滿足時是要進行清理的。並且注意到清理是一個循環,並且下一次清理前要等待waitNanos時間,啥意思呢?我們看下cleanup方法:
long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的連接數
int idleConnectionCount = 0;//空閒連接數
RealConnection longestIdleConnection = null;//空閒時間最長的連接
long longestIdleDurationNs = Long.MIN_VALUE;//最長的空閒時間
//遍歷連接:找到待清理的連接, 找到下一次要清理的時間(還未到最大空閒時間)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//若連接正在使用,continue,正在使用連接數+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空閒連接數+1
idleConnectionCount++;
// 賦值最長的空閒時間和對應連接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最長的空閒時間大於5分鐘 或 空閒數 大於5,就移除並關閉這個連接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 還剩多久到達5分鐘,然後wait這個時間再來清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//連接沒有空閒的,就5分鐘後再嘗試清理.
return keepAliveDurationNs;
} else {
// 沒有連接,不清理
cleanupRunning = false;
return -1;
}
}
//關閉移除的連接
closeQuietly(longestIdleConnection.socket());
//關閉移除後 立刻 進行下一次的 嘗試清理
return 0;
}
思路還是很清晰的:
- 有空閒連接的話,如果最長的空閒時間大於5分鐘 或 空閒數 大於5,就移除關閉這個最長空閒連接;如果 空閒數 不大於5 且 最長的空閒時間不大於5分鐘,就返回到5分鐘的剩餘時間,然後等待這個時間再來清理。
- 沒有空閒連接就等5分鐘後再嘗試清理。
- 沒有連接不清理。
其中判斷連接正在使用的方法pruneAndGetAllocationCount我們來看下:
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//連接上的數據流,弱引用列表
List<Reference<Transmitter>> references = connection.transmitters;
for (int i = 0; i < references.size(); ) {
Reference<Transmitter> reference = references.get(i);
if (reference.get() != null) {
i++;
continue;
}
// 到這裏,transmitter是泄漏的,要移除,且此連接不能再承載新的數據流(泄漏的原因就是下面的message)
TransmitterReference transmitterRef = (TransmitterReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace);
references.remove(i);
connection.noNewExchanges = true;
//連接因爲泄漏沒有數據流了,那麼可以立即移除了。所以設置 開始空閒時間 是5分鐘前(厲害厲害!)
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
//返回連接上的數據流數量,大於0說明正在使用。
return references.size();
}
邏輯註釋已經標明瞭,很好理解。其中connection.transmitters,表示在此連接上的數據流,transmitters size大於1即表示多個請求複用此連接。
另外,在findConnection中,使用connectionPool.put(result)存連接後,又調用transmitter.acquireConnectionNoEvents方法,瞅下:
void acquireConnectionNoEvents(RealConnection connection) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
connection.transmitters.add(new TransmitterReference(this, callStackTrace));
}
先把連接賦給transmitter,表示數據流transmitter依附在這個connection上;然後connection.transmitters add 這個transmitter的弱引用,connection.transmitters表示這個連接承載的所有數據流,即承載的所有請求。
好了,存 講完了,主要就是把連接存入隊列,同時開始循環嘗試清理過期連接。
取
//爲transmitter 從連接池 獲取 對應address的連接。若果routes不爲空,可能會因爲 連接合並(複用) 而獲取到HTTP/2連接。
boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
@Nullable List<Route> routes, boolean requireMultiplexed) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (requireMultiplexed && !connection.isMultiplexed()) continue;
if (!connection.isEligible(address, routes)) continue;
transmitter.acquireConnectionNoEvents(connection);
return true;
}
return false;
}
存的方法名是put,但你發現 取 的方法名卻不是get,transmitterAcquirePooledConnection意思是 爲transmitter 從連接池 獲取連接,實際上transmitter就代表一個數據流,也就是一個http請求。注意到,在遍歷中 經過判斷後也是transmitter的acquireConnectionNoEvents方法,即把匹配到的connection賦給transmitter。所以方法名還是很生動的。
繼續看是如何匹配的:如果requireMultiplexed爲false,即不是多路複用(不是http/2),那麼就要看Connection的isEligible方法了,isEligible方法返回true,就代表匹配成功:
//用於判斷 連接 是否 可以承載指向address的數據流
boolean isEligible(Address address, @Nullable List<Route> routes) {
//連接不再接受新的數據流,false
if (transmitters.size() >= allocationLimit || noNewExchanges) return false;
//匹配address中非host的部分
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
//匹配address的host,到這裏也匹配的話,就return true
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
//到這裏hostname是沒匹配的,但是還是有機會返回true:連接合並
// 1. 連接須是 HTTP/2.
if (http2Connection == null) return false;
// 2. IP 地址匹配
if (routes == null || !routeMatchesAny(routes)) return false;
// 3. 證書匹配
if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. 證書 pinning 匹配.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
private boolean routeMatchesAny(List<Route> candidates) {
for (int i = 0, size = candidates.size(); i < size; i++) {
Route candidate = candidates.get(i);
if (candidate.proxy().type() == Proxy.Type.DIRECT
&& route.proxy().type() == Proxy.Type.DIRECT
&& route.socketAddress().equals(candidate.socketAddress())) {
return true;
}
}
return false;
}
取的過程就是 遍歷連接池,進行地址等一系列匹配,到這裏第三個問號也解決了。
刪
//移除關閉空閒連接
public void evictAll() {
List<RealConnection> evictedConnections = new ArrayList<>();
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
if (connection.transmitters.isEmpty()) {
connection.noNewExchanges = true;
evictedConnections.add(connection);
i.remove();
}
}
}
for (RealConnection connection : evictedConnections) {
closeQuietly(connection.socket());
}
}
遍歷連接池,如果連接上的數據流是空,那麼就從連接池移除並且關閉。
我們回過頭看下Transmitter的releaseConnectionNoEvents方法,也就第一個問號,如果連接不再接受新的數據流,就會調用這個方法:
//從連接上移除transmitter.
@Nullable Socket releaseConnectionNoEvents() {
assert (Thread.holdsLock(connectionPool));
int index = -1;
//遍歷 此數據流依附的連接 上的所有數據流,找到index
for (int i = 0, size = this.connection.transmitters.size(); i < size; i++) {
Reference<Transmitter> reference = this.connection.transmitters.get(i);
if (reference.get() == this) {
index = i;
break;
}
}
if (index == -1) throw new IllegalStateException();
//transmitters移除此數據流
RealConnection released = this.connection;
released.transmitters.remove(index);
this.connection = null;
//如果連接上沒有有數據流了,就置爲空閒(等待清理),並返回待關閉的socket
if (released.transmitters.isEmpty()) {
released.idleAtNanos = System.nanoTime();
if (connectionPool.connectionBecameIdle(released)) {
return released.socket();
}
}
return null;
}
主要就是嘗試釋放連接,連接上沒有數據流就關閉socket等待被清理。
好了,到這裏連接池的管理就分析完了。
從連接的查找 到 連接池的管理,就是ConnectInterceptor的內容了。
CallServerInterceptor
哎呀,終於到最後一個攔截器了!
請求服務攔截器,也就是真正地去進行網絡IO讀寫了——寫入http請求的header和body數據、讀取響應的header和body。
上面ConnectInterceptor主要介紹瞭如何 尋找連接 以及 連接池如何管理連接。在獲取到連接後,調用了RealConnection的newCodec方法ExchangeCodec實例,然後使用ExchangeCodec實例創建了Exchange實例傳入CallServerInterceptor了。上面提到過ExchangeCodec負責請求和響應的IO讀寫,我們先來看看ExchangeCodec創建過程——RealConnection的newCodec方法:
ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
if (http2Connection != null) {
return new Http2ExchangeCodec(client, this, chain, http2Connection);
} else {
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1ExchangeCodec(client, this, source, sink);
}
}
http2Connection不爲空就創建Http2ExchangeCodec,否則是Http1ExchangeCodec。而http2Connection的創建是連接進行TCP、TLS握手的時候,即在RealConnection的connect方法中,具體就是connect方法中調用的establishProtocol方法:
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
//針對http請求,如果配置的協議包含Protocol.H2_PRIOR_KNOWLEDGE,則開啓Http2連接
if (route.address().sslSocketFactory() == null) {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
socket = rawSocket;
protocol = Protocol.H2_PRIOR_KNOWLEDGE;
startHttp2(pingIntervalMillis);
return;
}
socket = rawSocket;
protocol = Protocol.HTTP_1_1;
return;
}
//針對https請求,會在TLS握手後,根據平臺獲取協議(),如果協議是Protocol.HTTP_2,則開啓Http2連接
eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
startHttp2(pingIntervalMillis);
}
}
private void startHttp2(int pingIntervalMillis) throws IOException {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
http2Connection.start();
}
好了,到這裏不再深入了,繼續瞭解可以參考HTTP 2.0與OkHttp。那麼到這裏,ExchangeCodec已經創建了,然後又包裝成Exchange,最後傳入了CallServerInterceptor。
下面就來看看這最後一個攔截器:
public final class CallServerInterceptor implements Interceptor {
private final boolean forWebSocket;
public CallServerInterceptor(boolean forWebSocket) {
this.forWebSocket = forWebSocket;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();//上個攔截器傳入的exchange
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//寫請求頭
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
//含body的請求
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 若請求頭包含 "Expect: 100-continue" , 就會等服務端返回含有 "HTTP/1.1 100 Continue"的響應,然後再發送請求body.
//如果沒有收到這個響應(例如收到的響應是4xx),那就不發送body了。
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
//responseBuilder爲null說明服務端返回了100,也就是可以繼續發送body了
if (responseBuilder == null) {
if (request.body().isDuplex()) {//默認是false不會進入
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else {
// 滿足了 "Expect: 100-continue" ,寫請求body
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
} else {
//沒有滿足 "Expect: 100-continue" ,請求發送結束
exchange.noRequestBody();
if (!exchange.connection().isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
exchange.noNewExchangesOnConnection();
}
}
} else {
//沒有body,請求發送結束
exchange.noRequestBody();
}
//請求發送結束
if (request.body() == null || !request.body().isDuplex()) {
exchange.finishRequest();
}
//回調 讀響應頭開始事件(如果上面沒有)
if (!responseHeadersStarted) {
exchange.responseHeadersStart();
}
//讀響應頭(如果上面沒有)
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
//構建response
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
//這裏服務端又返回了個100,就再嘗試獲取真正的響應()
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
//回調讀響應頭結束
exchange.responseHeadersEnd(response);
//這裏就是獲取響應body了
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build();
}
//請求頭中Connection是close,表示請求完成後要關閉連接
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
exchange.noNewExchangesOnConnection();
}
//204(無內容)、205(充值內容),body應該是空
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
}
你會發現,整個內容就是前面說的一句話:寫入http請求的header和body、讀取響應的header和body。這裏就不再解釋了。
這裏我們可以看到,無論寫請求還是讀響應,都是使用Exchange對應的方法。上面也提到過Exchange理解上是對ExchangeCodec的包裝,這寫方法內部除了事件回調和一些參數獲取外,核心工作都由 ExchangeCodec 對象完成,而 ExchangeCodec實際上利用的是 Okio,而 Okio 實際上還是用的 Socket。
ExchangeCodec的實現類 有對應Http1.1的Http1ExchangeCodec 和 對應Http2.0的Http2ExchangeCodec。其中Http2ExchangeCodec是使用Http2.0中 數據幀 的概念完成請求響應的讀寫。關於Http1ExchangeCodec、Http2ExchangeCodec具體實現原理涉及okio這不再展開。
最後一點,CallServerInterceptor的intercept方法中沒有調用連接器鏈Chain的proceed方法,因爲這是最後一個攔截器啦!
好了,到這裏最後一個攔截器也分析完啦!
總結
本篇分析了ConnectInterceptor、CallServerInterceptor兩個攔截器的作用和原理。ConnectInterceptor負責連接的獲取,其中涉及到連接池的概念;CallServerInterceptor是真正的網絡IO讀寫。ConnectInterceptor涉及的內容較多,它是Okhttp的核心。
結合上一篇,我們已經分析完了Okhttp內部所有的攔截器,最後給出Okhttp的整體架構圖(圖片來源):
到這裏,Okhttp源碼解析部分就真的結束了,可真是一個漫長的過程!
從使用方式到工作流程,再到具體攔截器,掌握了這四篇文章的內容,應該說得上對Okhttp是比較熟悉了。這裏還計劃會出第五篇終章,來介紹一些Okhttp的常見問題和高級使用方式,敬請期待!
最後的最後,歡迎留言討論,如果你喜歡這一系列,或者覺得寫得還不錯,請幫忙 點贊、收藏和轉發,感謝!
.
感謝與參考:
okhttp源碼解析
通過ConnectInterceptor源碼掌握OKHttp3網絡連接原理 嘔心瀝血第十彈【十】
OkHttp源碼深度解析
OkHttp源碼解析 (三)——代理和路由
OkHttp 源碼學習筆記(三) 數據交換的流 HTTPCodec
歡迎關注我的 公 衆 號: