Http請求過程
指標資料
1.入隊到請求結束耗時
2.dns查詢耗時
3.socket connect耗時
4.tls連線的耗時
5.請求傳送耗時
6.響應傳輸耗時
7.首包耗時
8.響應解析耗時
9.Http錯誤,區分業務錯誤和請求錯誤
採集到以上指標,結合資料視覺化的工具,可以對Http個階段的耗時和錯誤分佈有直觀的感受,同時對優化業務Http請求提供資料支援。
如何獲取指標
獲取指標資料首先需要找到產生指標資料的關鍵程式碼,然後插入收集程式碼即可。 如果業務中使用的框架沒有原始碼或者不重新打包原始碼的情況下,如何插入程式碼? 這個就需要使用到能夠實現AOP的工具,在前面分享的Monitor中的提供註解和配置檔案的方式,在指定函式中插入相關程式碼的功能。這樣的實現方式也可以使監控程式碼和業務程式碼分離。
OkHttp框架
OkHttp是Android上最常用的Http請求框架,OkHttp的最新版本已經升級到4.0.x,實現也全部由java替換到了Kotlin,API的一些使用也會有些不同。由於4.x的裝置不是預設支援TLSV1.2版本,OkHttp 3.13.x以上的版本需要在Android 5.0+(API level 21+)和Java 1.8的環境開發,不過OkHttp也為了支援4.x裝置單獨創立了3.12.x分支,本文中使用的OkHttp版本為3.12.3版本。
OkHttp整體流程
先引用個別人畫流程圖(原圖來自)
請求過程分析
1.建立 OkHttpClient
new OkHttpClient()中會呼叫new OkHttpClient.Builder()方法,Builder()會設定一些的預設值。OkHttpClient()會把OkHttpClient.Builder()產生的物件中欄位複製到對應OkHttpClient物件的欄位上,其中sslSocketFactory如果沒有在Builder中設定,OkHttp會獲取系統預設的sslSocketFactory。
public OkHttpClient.Builder(){
/**
*非同步請求的分發器,其中使用 不限制執行緒數,存活時間為60s的執行緒池來執行非同步請求
* 預設限制同時執行的非同步請求不操過64個,每個host的同時執行的非同步請求不操過5個
*超過限制新的請求需要等待。
* */
dispatcher = new Dispatcher();
//支援的協議型別 Http1.0/1.1/2,QUIC
protocols = DEFAULT_PROTOCOLS;
//支援TLS和ClearText
connectionSpecs = DEFAULT_CONNECTION_SPECS;
//請求事件通知器,大部分指標資料都可以度過EventListener來獲取
eventListenerFactory = EventListener.factory(EventListener.NONE);
//系統級代理伺服器
proxySelector = ProxySelector.getDefault();
if(proxySelector == null){
proxySelector = new NullProxySelector();
}
//預設不使用Cookie
cookieJar = CookieJar.NO_COOKIES;
//socket工廠
socketFactory = SocketFactory.getDefault();
//用於https主機名驗證
hostnameVerifier = OkHostnameVerifier.INSTANCE;
//用於約束哪些證照是可信的,可以用來證照固定
certificatePinner = CertificatePinner.DEFAULT;
//實現HTTP協議的資訊認證
proxyAuthenticator = Authenticator.NONE;
//實現HTTP協議的資訊認證
authenticator = Authenticator.NONE;
/**
*實現多路複用機制連線池,最多保持5個空閒連線
*每個空閒連線最多保持五分鐘
* */
connectionPool = new ConnectionPool();
//dns解析,預設使用系統InetAddress.getAllByName(hostname)
dns = Dns.SYSTEM;
//支援ssl重定向
followSslRedirects = true;
//支援重定向
followRedirects = true;
//連線失敗是否重試
retryOnConnectionFailure = true;
//請求超時時間,0為不超時
callTimeout = 0;
//連線超時時間
connectTimeout = 10_000;
//socket讀超時時間
readTimeout = 10_000;
//socket寫超時時間
writeTimeout = 10_000;
//websocket 心跳間隔
pingInterval = 0;
}
//獲取預設的sslSocketFactory
X509TrustManager trustManager = Util.platformTrustManager();
this.sslSocketFactory = newSslSocketFactory(trustManager);
this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
複製程式碼
2.Request執行過程
從上面的流程圖可以看出,不管同步請求還是非同步請求,最終都會呼叫到 RealCall.getResponseWithInterceptorChain(),getResponseWithInterceptorChain() 再呼叫RealInterceptorChain.proceed(Request request)方法發起最終請求,下面我們來分析一下這兩個方法的具體程式碼。
Response RealCall.getResponseWithInterceptorChain() throws IOException {
//組裝所有的Interceptors
List<Interceptor> interceptors = new ArrayList<>();
//業務自定的Interceptors,通過OkHttpClient.Builid.addInterceptor新增
interceptors.addAll(client.interceptors());
//其他功能interceptors
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
//如果不是forWebSocket請求,新增通過OkHttpClient.Builid.addNetworkInterceptor新增的Interceptor
if (!forWebSocket) {
interceptors.addAll([client.networkInterceptors](http://client.networkinterceptors/)());
}
//真正發起網路請求的Interceptor
interceptors.add(new CallServerInterceptor(forWebSocket));
//建立RealInterceptorChain
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
//開始執行請求
Response response = chain.proceed(originalRequest);
//如果請求被取消
if (retryAndFollowUpInterceptor.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
}
複製程式碼
接下來再看RealInterceptorChain.proceed(Request request)程式碼
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
/*
*index代表當前應該執行的Interceptor在Interceptor列表中的位置,如果超過
*Interceptor列表size,報錯
*在RealCall.getResponseWithInterceptorChain()第一次呼叫proceed方法時傳遞的index值為0
*/
if (index >= interceptors.size()) throw new AssertionError();
//執行次數+1
calls++;
//httpCodec時在ConnectInterceptor建立的,會對應一個socket連線
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
//建立新的RealInterceptorChain,通過改變index的值來實現呼叫Interceptor列表下一個位置的Interceptor
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
//獲取當前index位置的Interceptor
Interceptor interceptor = interceptors.get(index);
//執行當前位置的interceptor,同時傳遞新建立的RealInterceptorChain,新的RealInterceptorChain中的index值是當前Interceptor列表中下一個位置
//interceptor.intercept中會呼叫新的RealInterceptorChain的proceed方法,實現向後呼叫
Response response = interceptor.intercept(next);
// 如果當前RealInterceptorChain的httpCodec不為空,確保下一個位置的Interceptor只被呼叫一次,httpCodec是在ConnectInterceptor中被賦值
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// response為空報錯
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
// response.body為空報錯
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
//返回response
return response;
}
複製程式碼
從程式碼可以看出 Interceptor 是 OkHttp 最核心的功能類,Interceptor 把實際的網路請求、快取、透明壓縮等功能都統一了起來,每一個功能都實現為一個Interceptor,它們最終組成一個了Interceptor.Chain的責任鏈,其中每個 Interceptor 都可能完成 Request到 Response 轉變的任務,循著責任鏈讓每個 Interceptor 自行決定能否完成任務以及怎麼完成任務,完成網路請求這件事也從 RealCall 類中剝離了出來,簡化了各自的責任和邏輯,程式碼變得優雅。 這些Interceptor最為關鍵的兩個Interceptor是ConnectInterceptor和CallServerInterceptor,ConnectInterceptor的主要功能是在連線池裡找到可複用連線,如果沒有,就建立新的socket,進行tls握手,將socket用Okio進行包裹,建立HttpCodec。CallServerInterceptor使用HttpCodec進行相關協議的傳輸和解析。下面對ConnectInterceptor中findConnect過程和CallServerInterceptor請求過程做一個分析。
3.ConnectInterceptor findConnection過程分析
在ConnectInterceptor中,會為本次請求建立可用的RealConnection,首先會從連線池中找到能夠複用的連線,如果沒有就建立新的socket,然後使用RealConnection建立HttpCodec。建立RealConnection的方法呼叫鏈路為StreamAllocation.newStream()-> StreamAllocation.findHealthyConnection()->StreamAllocation.findConnection(),findConnection()是建立連線的主要程式碼。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
//最終找到的connection
RealConnection result = null;
Route selectedRoute = null;
//需要釋放的connection
Connection releasedConnection;
//需要關閉的socket
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
/**
*如果遇到重定向到同一個地址的情況下,在RetryAndFollowUpInterceptor中會使用已經分配的StreamAllocation
*進行重定向請求,這個時候的connection不為空,但是這個connection不一定時有效的連線。
* **/
releasedConnection = this.connection;
/**
*如果已經存在RealConnection,但是不能用來建立新的Stream
*就設定this.connection=null,同時返回要關閉的socket
*當前請求第一次執行時releaseIfNoNewStreams()不進行任何操作
* */
toClose = releaseIfNoNewStreams();
/**
*
* 這時候this.connection不為空
* 表示原來的connection可以用來建立新的Stream
* 當前請求第一次執行時this.connection=null
*
* */
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
/**
* reportedAcquired會在新建socket或者從連線池
* 獲取到有效RealConnection時賦值為true
* 當前請求第一次執行時reportedAcquired=fasle
* */
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
/**
* 如果還沒找到目標RealConnection
* 嘗試從連線池中獲取
* */
if (result == null) {
/**
* 對於非http/2協議,如果已經存在不超過過RealConnection複用的最大值且協議,證照都一致
* 這個RealConnection可以用來複用
* 如果從連線池中獲取RealConnection,會呼叫
* streamAllocation.acquire()設定connection為新值
* */
Internal.instance.get(connectionPool, address, this, null);
/**
* connection != null表示從連線池獲取到合適的
* RealConnection,設定foundPooledConnection = true;
* */
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
/**
* close當前的需要關閉的socket
* */
closeQuietly(toClose);
/**
* 如果當前的RealConnection需要釋放,呼叫eventListener
* */
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
/**
* 如果從連線池獲取到RealConnection,呼叫eventListener
* */
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
/**
* 當前RealConnection可以繼續使用或者從連線池中找到合適的RealConnection
* 返回這個RealConnection
* */
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.
/**
* routeSelector在StreamAllocation構造方法中被建立
* 連線池重新尋找
* 請求第一次執行時routeSelector.next()會進行域名解析工作
* */
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.
List<Route> 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.
/**
* 如果在連線池重新找到合適的RealConnection,返回
* */
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
/**
* 如果還沒有找到,就需要建立新的RealConnect
* 生成新的socket,建立Tls,並加入ConnectPool
* */
// Do TCP + TLS handshakes. This is a blocking operation.
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;
}
複製程式碼
4.CallServerInterceptor程分析
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
//Http解析器,在ConncetInterceptor中建立
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
/**
*請求的使用的連線,在ConncetInterceptor中產生
*連線可能是從ConnectPool中選擇或者重新建立出來
**/
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
/**
* 通過httpCodec中用Okio包裹的socket寫請求頭
**/
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
/**
* 如果請求有請求body,傳送body
**/
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
request body.
/**
* 請求頭是Expect: 100-continue,先讀響應頭
* httpCodec.readResponseHeaders方法讀取到狀態碼100時, 會返回null
**/
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
/**
* 如果正常傳送請求body部分
* 請求頭有Expect: 100-continue,但是伺服器沒有返回狀態碼100,且不是Http/2協議,關閉當前連線
**/
if (responseBuilder == null) {
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
/**
* 正常情況下,讀響應headers
**/
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
/**
* 讀響應headers結束
**/
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
/**
* 如果是forWebSocket,且 code == 101返回空響應
* 其他返回 RealResponseBody物件
**/
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 {
/**
*將
*無響應內容
* chunk內容,
* 存在contenlength內容
* 不存在contenlength內容
* 的響應body包裹成 RealResponseBody物件
*
**/
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
/**
* 服務端關閉要求連線
**/
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
/**
* code是204/205,contentLength()還大於0,丟擲協議錯誤
**/
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
複製程式碼
CallServerInterceptor執行完成後返回的是一個只讀取了響應headers,但是還沒有讀取body的Response,OkHttp網路請求部分的程式碼到此就結束了,後續的parseReponse都在更上層的框架中,比如Retrofit就是在OkHttpCall.parseReponse()方法中呼叫serviceMethod.toResponse(catchingBody)中呼叫GsonConvter或者其他Convertor來進行處理。
獲取指標具體實現
對於Http 請求耗時,異常,資料大小,狀態碼 的獲取,直接使用前面實現的MAOP,攔截OkHttpClient.Builder的build方法加入統計Interceptor ,DNSLookUp 耗時,連線耗時,ssl耗時,通過設定EventListener.Factory,可以直接收集。解析耗時需要攔截上層框架的parseReponse方法進行收集。 首包時間需要攔截OkHttp讀請求資料的方法來實現,OKHttpClient 最終呼叫CallServerInterceptor,關鍵程式碼就是讀取readResponseHeaders的時機。
MAOP 實現
使用前面提供的MAOP功能,在AOP配置檔案中加入,攔截OkHttpClient的builder方法和Http1Codec的readHeaderLine方法和okhttp3.internal.http2.Http2Stream的takeResponseHeaders方法的配置
在攔截OkHttpClient的Builder的build()方法中加入統計Interceptor和EventListenerFactory
首包的時間通過:認為第一次讀響應頭返回時為首包時間,攔截okhttp3.internal.http1.Http1Code.readHeaderLine的方法和okhttp3.internal.http2.Http2Stream.takeResponseHeaders計算首包時間
Retrofit parse耗時收集
AOP配置檔案中加入對retrofit2.OKHttp.parseResponse攔截的配置
Method回掉中處理相關的資料
綜上,這個方案基本能實現Http基本指標的獲取,但是有些細節還需完善。