目錄
一、提出問題
1.OkHttp底層也是通過Socket發送和接收請求,是如何支持http/https請求的?
2.連接池的實現原理,如何支持多路復用?怎樣從連接池選擇復用連接?
3.如何處理代理?
4.Route、ConnectionPool、RealConnection、steamAllocation、HttpCodec分別的作用,如何協作?
5.重定向請求或重試的處理流程?
6.如何支持http2協議?
如果剛開始學習OkHttp源碼或對代理不了解的,可以先忽略代理部分的邏輯,先搞清楚直連請求的流程。OkHttp源碼之所以復雜一部分原因是處理了代理和路由,但代理部分實際項目可能用不上。如果想深入了解OkHttp的代理,可以閱讀:OkHttp源碼解析 (三)——代理和路由(http://www.lxweimin.com/p/63ba15d8877a)。
二、網絡管理涉及的角色及作用
外部發起的一次請求封裝為一個RealCall,一個RealCall可能對應多個Request,如初始請求及后續的重定向請求,而每一個Request會創建一個StreamAllocation來管理連接,尋找合適的RealConnection,一個Call的所有Request偏向用同一個RealConnection,對于HTTP/1.x的請求,RealConnection同時只持有一個StreamAllocation,對于HTTP/2可以同時持有多個StreamAllocation。角色的對應關系如下圖。
三、各個角色的協作
1、在RetryAndFollowUpInterceptor攔截器中,為新請求創建流StreamAllocation,如果請求返回需要重定向,創建重定向Request及新的StreamAllocation,繼續上面的邏輯。
2、在ConnectInterceptor攔截器中,StreamAllocation選擇連接RealConnection:
第一步:優先從連接池(connectionPool)中尋找,有合適的則直接復用;
第二步:如果沒有則創建新的RealConnection,并加入到連接池中,新創建的連接通過connect方法完成socket的三次握手,與服務器建立連接;
第三步:建立連接后獲得網絡寫入流(BufferedSink,封裝了InputStream)和讀取流(BufferedSource,封裝了outputStream);
第四步:最后創建HttpCodec,持有BufferedSink和BufferedSource,后續寫入請求和讀取響應通過HttpCodec操作。
3、在CallServerInterceptor攔截器中,通過HttpCodec實現真正發送請求和讀取服務器響應,最后構造Response并沿鏈路返回給上一級的攔截器。
各攔截器的關鍵代碼:
RetryAndFollowUpInteceptor:
public final class RetryAndFollowUpInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
...
//原始請求
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
...
while (true) {
...
//原始請求返回
response = realChain.proceed(request, streamAllocation, null, null);
...
//創建重定向請求
Request followUp = followUpRequest(response, streamAllocation.route());
...
//重定向StreamAllocation
streamAllocation = new StreamAllocation(client.connectionPool(),createAddress(followUp.url()), call, eventListener, callStackTrace);
}
}
ConnectInterceptor:
public final class ConnectInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
...
//從攔截器鏈里得到StreamAllocation對象
StreamAllocation streamAllocation = realChain.streamAllocation();
//尋找合適的連接并返回讀寫流
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
//獲取realConnetion
RealConnection connection = streamAllocation.connection();
//執行下一個攔截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
CallServerInterceptor:
public final class CallServerInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
HttpCodec httpCodec = realChain.httpStream();
Request request = realChain.request();
//發送請求頭和請求行(寫入到緩沖區)
httpCodec.writeRequestHeaders(request);
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
...//發送body部分,post請求和"100-continue"請求
}
//flush正在發送
httpCodec.finishRequest();
//讀取響應
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
//讀取頭部
responseBuilder = httpCodec.readResponseHeaders(false);
}
//構造響應Response
Response response = responseBuilder.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
...
//返回Response
return response;
}
}
四、詳解各個角色的邏輯
(一) StreamAllocation
解釋:流分配器,主要功能是管理一次連接上的流,為Request尋找合適的Realconnection,并獲取網絡讀寫流。重定向會創建新的StreamAllocation。每個Connection 有個變量allocationLimit,用于定義可以承載的并發的 streams 的數量。HTTP/1.x 的 Connection 一次只能有一個stream, HTTP/2 一般可以有多個。
public final class StreamAllocation {
public final Address address;//請求地址
private RouteSelector.Selection routeSelection;//可選路由列表
private Route route;//選中的路由
private final ConnectionPool connectionPool;//連接池
public final Call call;//請求call
// State guarded by connectionPool.
private final RouteSelector routeSelector;//路由選擇器
private HttpCodec codec;//編碼網絡請求和響應
public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
EventListener eventListener, Object callStackTrace) {
this.connectionPool = connectionPool;
this.address = address;
this.call = call;
this.eventListener = eventListener;
this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
this.callStackTrace = callStackTrace;
}
/*
* 獲取流,通過findConnection得到連接,再獲取讀寫流
*/
public HttpCodec newStream(...) {}
/*
* 尋找可復用連接及判斷是否"健康",如果不“健康”則繼續循環直至找到“健康”連接
*/
private RealConnection findHealthyConnection(...) throws IOException {}
/*
* 為新stream尋找可復用連接,可能來自連接池,如果沒有則新建
*/
private RealConnection findConnection(...){}
/*
* 釋放當前持有的連接,如果連接是限制分配給新流的(noNewSteam為true),則返回socket進行關閉。
* 對于HTTP/2,多個請求共享一個連接,所以對于follow-up請求期間可能被限制分配新流
*/
private Socket releaseIfNoNewStreams(){}
/*
* 請求已經完成,從連接中移除正在當前執行的流,只有移除了連接才能被復用
*/
public void streamFinished(){}
/*
* 禁止在承載此分配的連接上創建新流
*/
public void noNewStreams(){}
/*
* 取消或拋異常,釋放連接
*/
public void release() {}
/*
* 釋放此流所持有的資源。如果分配了足夠的資源,連接將被分離或關閉。調用者必須在連接池上同步。
*/
private Socket deallocate(){}
}
(1)newStream方法
下面看下為新請求尋找連接獲取讀寫流的邏輯。
public HttpCodec newStream(
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);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
方法比較簡單,第一步尋找連接RealConnection,第二步獲取網絡讀寫流HttpCodec,有點小疑問,為什么要先同步連接池再返回HttpCodec。
(2)findHealthyConnection方法
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
* 尋找連接,如果是“健康”的則返回,如果不是繼續循環尋找。
*/
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, 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;
}
}
1、先調用findConnectoion方法返回連接對象RealConnection;
2、根據RealConnection的屬性successCount=0判斷連接是新創建的,新創建的連接不需要判斷是否“健康”,直接返回;
3、如果successCount大于0,表示連接早已經創建,是從連接池中獲取得到,這時需要判斷連接是否“健康”;
4、如果非“健康”連接,則設置該連接不允許承載新的流,繼續第一步;
findConnection及isHealthy的邏輯后面會分析。
(3)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,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;//是否從連接池中找到連接
RealConnection result = null;//需要返回的連接
Route selectedRoute = null;//找到路由
Connection releasedConnection;//可釋放的連接
Socket toClose;//需要關閉的socket
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
//??沒想懂什么場景這個地方已經分配了連接
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
//從連接池中獲取一個連接,通過傳入this對象,尋找到合適連接會賦值this.connection
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
//尋找到合適的連接
foundPooledConnection = true;
result = connection;
} else {
//連接池沒有合適的連接,可能已經有路由信息(什么場景)
selectedRoute = route;
}
}
}
//??什么場景
closeQuietly(toClose);
...
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
//如果已經分配連接或者從連接池中尋找到合適的,則返回
return result;
}
// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;//需要嘗試新的路由
routeSelection = routeSelector.next();//路由集合
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
// 現在已經有一個ip地址集合,再次嘗試重連接池中尋找可復用連接
List routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
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.
//創建新的連接并立即分配給當前的流,這樣允許異步調用取消方法來打斷即將進行的握手。
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);//分配連接給當前的流
}
}
// If we found a pooled connection on the 2nd time around, we're done.
// 如果從連接池找到合適的連接則返回。
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
//新創建的連接,建立與服務器的連接,方法內會根據平臺調用socket.connnect()
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
//路由庫記錄新連接的路由
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// Pool the connection.
//添加到連接池,添加前需要加鎖
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
//
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
總結findConnection的流程如下:
1、先判斷是否已經分配了連接,有則返回(沒想懂是什么場景);
2、沒有則根據address從連接池中找可重用的連接Internal.intance.get(connectionPool,address,this,null),找到則返回;
3、如果沒有確定路由,則需要嘗試新的路由,通過路由選擇器返回路由集合,這時得到一個ip地址集合;
4、遍歷路由集合再次從連接池中尋找可復用的連接,有則設為待返回的連接;
5、如最終從連接池中沒有找到合適的連接,則新建連接new RealConnection(connectionPool,selectedRoute),并馬上分配給當前的流;
6、新建立的連接,建立與服務器連接result.connect(即sockect.connect),并把路由記錄到路由庫中,把創建的連接添加到連接池Internal.intance.put(connectionPool ,result).
(二) RealConnection
解析:建立在Socket之上的物理通信信道,持有StreamAllocation隊列。
public final class RealConnection extends Http2Connection.Listenerimplements Connection {
private final ConnectionPool connectionPool;//連接池
private final Route route; //當前連接到路由
private Socket rawSocket; //底層socket
private Socket socket; //應用層socket
private Handshake handshake; //https的握手
private Protocol protocol; //協議
private Http2Connection http2Connection; //HTTP/2的鏈接
private BufferedSource source; //網絡讀取流
private BufferedSink sink; //網絡寫入流
public boolean noNewStreams; //標識是否能繼續添加流,一但設為true,則一直為true,不能再添加流
public int allocationLimit =1; //承載流(allocationStream)的最大的并發數
public final List> allocations =new ArrayList<>(); //當前承載流的集合
public RealConnection(ConnectionPool connectionPool, Route route){}
/*
* 連接,中創建新RealConnection對象后調用,完成socket連接
*/
public void connect(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled, Call call,EventListener eventListener){}
/*
* 連接隧道
*/
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,EventListener eventListener)throws IOException{}
/*
* socket連接,完成tcp三次握手
*/
private void connectSocket(int connectTimeout, int readTimeout, Call call,EventListener eventListener)throws IOException{}
/*
* 建立協議
*/
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,int pingIntervalMillis, Call call, EventListener eventListener)throws IOException{}
/*
*https請求,建立tls連接
*/
private void connectTls(ConnectionSpecSelector connectionSpecSelector)throws IOException{}
/*
* 創建通道
*/
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,HttpUrl url)throws IOException{}
/*
* 構造創建通道的請求
*/
private Request createTunnelRequest(){}
/*
* 是否符合條件,如果能分配新流則返回true,
* /
public boolean isEligible(Address address, @Nullable Route route){}
/*
* 將io流BufferedSource,BufferedSink封裝為HttpCodec
*/
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,StreamAllocation streamAllocation)throws SocketException{}
/*
* 是否“健康”,如果準備好建立新流則返回true
*/
public boolean isHealthy(boolean doExtensiveChecks){}
}
(1)connect方法
新創建RealConnection后,通過connect方法服務器建立連接,完成tcp三次握手,下面介紹下connect的方法。
public void connect(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled, Call call,EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");//只能調用一次
RouteException routeException = null;
List connectionSpecs = route.address().connectionSpecs();
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 (true) { //對應SSLHandshakeException/SSLProtocolException的拋錯,會重試
try {
//判斷是否需要隧道,如果是通過HTTP代理完成https請求,則返回true
//true的條件:address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP
if (route.requiresTunnel()) {
//建立隧道,與http代理之間建立socket連接
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
//無法與代理建立連接
break;
}
} else {
//建立socket連接,不需要代理
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
//創建協議,完成連接
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
...
//connectionRetryEnabled :是否運行重試連接,在okhttpclient的builder設置,默認true
//connectionSpecSelector.connectionFailed(e)針對不同的報錯有不同的策略,返回true則重試
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
...
if (http2Connection != null) { //http2運行一個RealConnection建立多個流
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
總結connect的方法如下:
1、判斷是否需要隧道(隧道代理),如果需要則建立與代理服務器的sockect連接;
2、不需要隧道則直接建立與服務器的sockect連接;
3、確定網絡協議,如果是https請求則進行tls握手;
4、如果是HTTP/2,則新建http2Connection來處理請求,完成HTTP/2的協議協商。
(2)connectSocket方法
解析了連接的整理流程,下面對其中調用的方法進行分析,首先看下connectSocket方法:
private void connectSocket(int connectTimeout, int readTimeout, Call call,EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
//如果是直連或者HTTP代理,則通過socketFactory創建socket,如果是socket代理,則直接new Socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket(): new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
//根據不同的平臺,完成socket連接,實際是socket.connect(address, connectTimeout);
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
try {
//okio封裝sockect讀寫流
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
流程比較簡單,總結如下:
1、對于直連及http代理請求,通過SocketFactory創建socket,對于SOCKET代理傳入proxy創建socket;
2、設置socket超時時間;
3、完成特定平臺的socket連接,實際是socket.connect(address, connectTimeout);
4、創建用于I/O的讀寫流source 、sink
這里可以發現,代理請求處理的不同:
· SOCKET代理:傳入代理對象proxy手動創建socket,其他沒有什么特別的處理,都交由java標準庫的socket去處理,route的socketAddress包含目標http服務器的域名,對外界而言,不需要做處理。
· HTTP代理:對于明文的HTTP代理, 也不需要特別的處理,route的socketAddress包含著代理服務器的IP地址,會自動建立與代理服務器的連接,代理服務器解析后再轉發請求內容。
(3)connectTunnel方法
通過HTTP代理發送https請求需要用到隧道代理,也是一種協定方式,總結建立隧道的流程:
1、客戶端發送CONNECT請求到代理服務器,請求建立通道;請求會包含目標服務器的主機名和端口
2、代理服務器與目標服務器建立TCP連接;
3、代理服務器回應客戶端;
4、客戶端向代理服務器發送請求,代理服務器原封不動轉發客戶端請求(原生TCP packet);
5、響應過程同請求過程。
了解隧道代理的基本流程,就好理解OkHttp中有關隧道代理的代碼邏輯了。下面介紹connectTunnel方法:
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,EventListener eventListener) throws IOException {
//構建連接請求
Request tunnelRequest = createTunnelRequest();
HttpUrl url = tunnelRequest.url();
for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
//建立與代理服務器的socket連接
connectSocket(connectTimeout, readTimeout, call, eventListener);
//建立隧道,發送不加密的代理請求并獲取返回結果
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
if (tunnelRequest == null) break; // 返回null表示已經與代理服務器建立隧道,退出循環
// The proxy decided to close the connection after an auth challenge. We need to create a new
// connection, but this time with the auth credentials.
//如果代理服務器因身份認證問題關閉了連接,需要創建新的連接,帶上身份憑證
closeQuietly(rawSocket);
rawSocket = null;
sink = null;
source = null;
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
}
}
先創建與代理服務器的socket連接,然后再發送代理請求建立隧道(按照隧道代理的協議方式)。
(4)createTunnelRequest方法
建立隧道的需要構建代理請求, 那代理的請求發了什么,下面介紹createTunnelRequest方法:
private Request createTunnelRequest() {
return new Request.Builder()
.url(route.address().url())
.header("Host", Util.hostHeader(route.address().url(), true))
.header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
.header("User-Agent", Version.userAgent())
.build();
}
從代碼可以看到通道請求包括很少的頭部信息,是因為與服務器代理之間的連接是不加密的,避免發生如cookies等敏感信息到代理服務器。
(5)createTunnel方法
下面看下再看下如何創建隧道:
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
HttpUrl url) throws IOException {
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
// 建立隧道的協議請求內容
String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
while (true) {//??swtich各種分支都會退出循環,沒想懂循環的場景
Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
source.timeout().timeout(readTimeout, MILLISECONDS);
sink.timeout().timeout(writeTimeout, MILLISECONDS);
// 發送代理請求
tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
tunnelConnection.finishRequest();
// 讀取響應
Response response = tunnelConnection.readResponseHeaders(false)
.request(tunnelRequest)
.build();
// The response body from a CONNECT should be empty, but if it is not then we should consume
// it before proceeding.
long contentLength = HttpHeaders.contentLength(response);
if (contentLength == -1L) {
contentLength = 0L;
}
Source body = tunnelConnection.newFixedLengthSource(contentLength);
Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
body.close();
switch (response.code()) {
case HTTP_OK:
// Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
// that happens, then we will have buffered bytes that are needed by the SSLSocket!
// This check is imperfect: it doesn't tell us whether a handshake will succeed, just
// that it will almost certainly fail because the proxy has sent unexpected data.
if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
throw new IOException("TLS tunnel buffered too many bytes!");
}
return null;
case HTTP_PROXY_AUTH:
tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");
if ("close".equalsIgnoreCase(response.header("Connection"))) {
return tunnelRequest;
}
break;
default:
throw new IOException(
"Unexpected response code for CONNECT: " + response.code());
}
}
}
(6)establishProtocol方法
上面的方法只是建立了socket連接,無論是與目標服務器的連接,還是與代理服務器的,下面的方法是確定網絡協議,如果是http請求,協議為http/1.1,可以直接返回,如果是https請求, 則建立ssl連接,如果HTTP/2則需要進行協議協商。這里就解析了OkHttp如何在socket連接之上實現http、https、HTTP/2等協議。
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
if (route.address().sslSocketFactory() == null) {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
return;
}
eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
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();
}
}
從代碼可以看出:
1、如果sslSocketFactory為空,說明是http請求,協議為HTTP_1_1,返回;
2、如果sslSocketFactory非空,需要進行TLS握手;
3、如果是協議是HTTP_2,則構建Http2Connection,完成與服務器的協商。
(7)connectTls方法
再看下如何建立TLS連接:
private void connectTls(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.
//在原來的已經 建立連接的socket上加一層ssl,java中傳入原始socket構造SSLSocket
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
//配置socket的加解密器 ,TLS版本及擴展內容
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
//ssl握手
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
if (!isValid(sslSocketSession)) {
throw new IOException("a valid ssl session was not established");
}
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// Verify that the socket's certificates are acceptable for the target host.
//驗證socket的證書是否被服務器接受
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
//獲取X509Certificate證書對象
X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
TLS連接是對原始的TCP連接的一個封裝,以提供TLS握手,及數據收發過程中的加密解密等功能。在Java中,用SSLSocket來描述。建立TLS連接的大致流程可總結為:
1、在原始已經建立連接的socket的基礎上,用SSLSocketFactory構建SSLSocket;
2、配置SSLSocket,包括加解密器,TLS協議版本,如果ConnectionSpec支持TLS擴展參數,配置TLS擴展參數;
3、開始TLS握手sslSocket.startHandshake();
4、握手完后,獲取服務器返回的證書信息SSLSession;
5、對握手過程返回證書新息SSLSession進行驗證hostnameVerifier().verify();
6、驗證遠程主機證書;
7、如果ConnectionSpec支持TLS擴展參數,獲取握手過程完成的協議協商所選擇的協議,主要用于http2的ALPN擴展;
8、獲取I/O操作的讀寫流,okio的BufferedSource和BufferedSink,保存協議及握手信息。
(8)isEligible方法
對于一個請求,優先從連接池尋找可復用的連接,如何判斷連接是否能否被復用,下面解釋判斷能否復用的方法:
/**
* Returns true if this connection can carry a stream allocation to {@code address}. If non-null
* {@code route} is the resolved route for a connection.
*/
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
// 當前分配的流已達到上限或者已經設為不允許再分配流
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
// host之外的配置要匹配,包括協議版本、代理、ssl、端口等
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
// 如果host也完全匹配,則可放心復用
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
// 運行到這里說明多個host指向一個ip的特殊情況,只允許HTTP/2復用,條件比較嚴格,要滿足后續4點
if (http2Connection == null) return false;
// 2. The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
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.
}
判斷連接是否可復用條件:
1、先要滿足流分配上限數(HTTP/1.x 1個,HTTP/2 多個);
2、Address的配置完全相同,如SSL、代理、端口、主機名都要匹配;
3、如果Address不匹配也可能有復用,主要是同一個主機配置了多個域名,且新請求已經選擇的路由,條件必須同時滿足:HTTP/2請求,新請求不是代理請求、當前連接也不是代理連接,路由ip、端口匹配,證書匹配。
(三) ConnectionPool
解析:我們都知道,在復雜的網絡環境下,頻繁創建和斷開Socket連接是非常浪費資源和耗時的(需要3次握手4次揮手),如果是https連接還要進行ssl握手,http協議的keepalive對于解決這一問題有重要的作用。
連接空閑后存活一段時間及連接復用需就要對連接進行管理,這里引入了連接池的概念。okhttp支持單個地址最多5個空閑連接(keepalive狀態),保活時間是5分鐘,超出時間的連接會被回收。okhttp用ConnectionPool實現連接池的功能,對連接進行管理和回收。
ConnectionPool內部維護一個隊列存放連接,一個線程池清理連接。
/*
* 連接池,實現連接的復用。通過一個隊列維護當前所有的連接(RealConnection)
* 最多同時持有5個空閑連接,保活時間為5分鐘
*/
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(), Util.threadFactory("OkHttp ConnectionPool", true));
/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;//每個地址最多空閑連接數
private final long keepAliveDurationNs;
//清理連接線程,在線程池executor中調用
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 connections = new ArrayDeque<>();//存放連接的隊列
final RouteDatabase routeDatabase = new RouteDatabase();//路由庫
boolean cleanupRunning;
/**
* 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);
}
}
/*
* 獲取可復用連接,如果沒有則返回null
*/
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {}
/*
* 將新創建的連接加入連接池
*/
void put(RealConnection connection){}
}
(1)get方法
/**
* Returns a recycled connection to {@code address}, or null if no such connection exists. The
* route is null if the address has not yet been routed.
* 根據address獲取可重用的連接,如果沒有返回null。
* 如果沒有選擇路由,入參route是null,StreamAllocation的findConnection方法,第一次調用get方法
* 的入參route就是null
*/
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
方法比較簡單,遍歷隊列中每個連接,調用isEligible方法判斷是否適合復用,能則分配給streamAllocation,判斷是否適合的方法isEligible前面有分析。
(2)put方法
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
新創建的連接需要通過put方法加入到連接池,先執行清理,再添加到隊列。
(3)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;
}
清理的邏輯不復雜,就是遍歷隊列中的連接,調用pruneAndGetAllocationCount方法返回引用數,判斷當前連接是否空閑,跳過正在被用的連接,對于空閑的連接,更新空閑持續的時間,通過遍歷得到空閑時間最長的連接,如果超過了設定的保活時間或者空閑連接超過最大數量,則移除并關閉該連接,繼續執行清除,如果沒有需要移除的,返回下次清理時間,即最快達到設定保活的時間。
(4)pruneAndGetAllocationCount方法
* 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();
}
RealConnection通過一個列表記錄當前建立的流List<Reference<StreamAllocation>>,這是一個弱引用列表,主要是為了防止內存泄漏,pruneAndGetAllocationCount方法主要是遍歷該列表,如果發現引用的StreamAllocatin已經為空(程序出現bug,正常是不會出現的),則將該引用移出列表,最后返回當前持有引用的計數。
(4)小結
由上面的分析可總結連接池復用的原理:
· OkHttp通過ConnectionPool維護線程池;
· ConnectionPool通過隊列Deque<RealConnection>持有當前所有的連接;
· 新創建的連接通過put方法加入到隊列,加入隊列前先執行一遍清理;
· get方法會根據傳入的Address和Route遍歷連接隊列,返回可以復用的連接,復用的條件既要滿足分配流的上限原則,也需protocol、ssl、host等配置匹配;
· ConnectionPool通過一個專門的線程清理失效的連接,該線程每執行完一次清理都會根據返回的等待時間阻塞等待;
· 清理的邏輯即遍歷每個連接,通過連接對StreamAlloction的弱引用計數器來判斷是否空閑(計數為0則說明空閑),通過遍歷隊列,找出空閑時長最長的連接,再根據已到保活的時長(keepalive)或空閑連接數的上限進行清理回收。
五、總結
至此基本解析了OkHttp網絡連接管理的流程,由于篇幅及時間有限的原因,中間有些細節沒有展開細分析。在分析的過程也解答了文章開頭提出的疑問。
參考
http://www.lxweimin.com/p/6166d28983a2
https://blog.csdn.net/yueaini10000/article/details/83305787
https://blog.csdn.net/FrancisHe/article/details/84667562#_HTTP__2