基於OkHttp的Http監控

山魷魚說發表於2019-09-01

Http請求過程

image

指標資料

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整體流程

先引用個別人畫流程圖(原圖來自

image

請求過程分析

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的時機。

image

MAOP 實現

使用前面提供的MAOP功能,在AOP配置檔案中加入,攔截OkHttpClient的builder方法和Http1Codec的readHeaderLine方法和okhttp3.internal.http2.Http2Stream的takeResponseHeaders方法的配置

image

image

在攔截OkHttpClient的Builder的build()方法中加入統計Interceptor和EventListenerFactory

image

首包的時間通過:認為第一次讀響應頭返回時為首包時間,攔截okhttp3.internal.http1.Http1Code.readHeaderLine的方法和okhttp3.internal.http2.Http2Stream.takeResponseHeaders計算首包時間

image

Retrofit parse耗時收集

AOP配置檔案中加入對retrofit2.OKHttp.parseResponse攔截的配置

20_32_45__09_01_2019.jpg

Method回掉中處理相關的資料

20_35_17__09_01_2019.jpg

綜上,這個方案基本能實現Http基本指標的獲取,但是有些細節還需完善。

基於OkHttp的Http監控

相關文章