深入剖析OkHttp系列(五) 來自官方的事件機制

亓春傑發表於2018-08-16

Event

Events allow you to capture metrics on your application’s HTTP calls. Use events to monitor: The size and frequency of the HTTP calls your application makes. If you’re making too many calls, or your calls are too large, you should know about it! The performance of these calls on the underlying network. If the network’s performance isn’t sufficient, you need to either improve the network or use less of it.

Events允許你捕獲你的應用Http請求的呼叫監控(也就是生命週期)。 使用Events可以展示:
.應用Http請求發出的大小和頻率, 如果你發出了太多請求, 或者你的請求太大, 你應該瞭解它。
.底層網路呼叫的效能。如果效能不足, 你應該提高網路或減少網路使用。

EventListener

Subclass EventListener and override methods for the events you are interested in. In a successful HTTP call with no redirects or retries the sequence of events is described by this flow.

繼承EventListener, 重寫你感興趣的events方法。 在一次沒有重定向 或 重試的Http成功請求中, 事件序列執行如下。

深入剖析OkHttp系列(五) 來自官方的事件機制

Here’s a sample event listener that prints each event with a timestamp.

這是一個列印每個事件的簡單的eventListener。
class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}
複製程式碼
我們發幾個請求:
Request request = new Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build();

System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}
複製程式碼
EventListener會列印相應的事件:
REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd
複製程式碼

Notice how no connect events are fired for the second call. It reused the connection from the first request for dramatically better performance.

注意: 第二次呼叫沒有觸發連線事件。 它重用了第一次連線, 從而顯著提高了效能。

EventListener。Factory

In the preceding example we used a field, callStartNanos, to track the elapsed time of each event. This is handy, but it won’t work if multiple calls are executing concurrently. To accommodate this, use a Factory to create a new EventListener instance for each Call. This allows each listener to keep call-specific state. This sample factory creates a unique ID for each call and uses that ID to differentiate calls in log messages.

在前面的示例中,我們使用了一個欄位callStartNanos來跟蹤每個事件的已用時間。這很方便,但如果多個呼叫同時執行,它將無法工作。 為了適應這種情況,請使用Factory為每個Call建立一個新的EventListener例項。 這允許每個監聽器保持特定於呼叫的狀態。 此示例工廠為每個呼叫建立唯一ID,並使用該ID區分日誌訊息中的呼叫。
class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

  private void printEvent(String name) {
    long elapsedNanos = System.nanoTime() - callStartNanos;
    System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  ...
}
複製程式碼

We can use this listener to race a pair of concurrent HTTP requests

我們可以使用這個listener來同時追蹤一對HTTP請求:
Request washingtonPostRequest = new Request.Builder()
    .url("https://www.washingtonpost.com/")
    .build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
  ...
});

Request newYorkTimesRequest = new Request.Builder()
    .url("https://www.nytimes.com/")
    .build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
  ...
});
複製程式碼

Running this race over home WiFi shows the Times (0002) completes just slightly sooner than the Post (0001):

在WiFi上執行這次追蹤顯示, 0002的完成時間只比0001快一點。
0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd
複製程式碼

The EventListener.Factory also makes it possible to limit metrics to a subset of calls. This one captures metrics on a random 10%:

EventListener.Factory還可以限制一部分請求的指標。 這個隨機捕獲10%的指標:
class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };

  ...
}
複製程式碼

Events with Failures(有失敗的事件)

When an operation fails, a failure method is called. This is connectFailed() for failures while building a connection to the server, and callFailed() when the HTTP call fails permanently. When a failure happens it is possible that a start event won’t have a corresponding end event.

當一個操作失敗了, 失敗的方法會被呼叫。 這是一個在建立伺服器連線時的連線錯誤。 當失敗發生時, 可能一個開始事件就不會有對應的結束事件了。

深入剖析OkHttp系列(五) 來自官方的事件機制

Events with Retries and Follow-Ups(帶重試的事件和跟進)

OkHttp is resilient and can automatically recover from some connectivity failures. In this case, the connectFailed() event is not terminal and not followed by callFailed(). Event listeners will receive multiple events of the same type when retries are attempted. A single HTTP call may require follow-up requests to be made to handle authentication challenges, redirects, and HTTP-layer timeouts. In such cases multiple connections, requests, and responses may be attempted. Follow-ups are another reason a single call may trigger multiple events of the same type.

OkHttp具有彈性, 可以自動從連線故障中恢復。 這種情況下, connectFailed()不是終點, 且後面沒有callFailed()。 在嘗試重試時, EventListener將會接收多個同型別的事件。
單個請求呼叫可能需要後序的請求來詢問身份驗證, 重定向, 和Http層面的超時。 在這種情況下, 多個連線請求和響應會發生。後序請求是可能會收到多個同型別事件的原因。

深入剖析OkHttp系列(五) 來自官方的事件機制

適用性

事件機制可以在OkHttp 3.11版本公開使用。 未來可能會引入新的事件型別, 你需要重寫響應的方法來處理。

相關文章