OkHttp 3.11.0版本提供了EventListener接口,可以让调用者接收一系列网络请求过程中的事件,例如DNS解析、TSL/SSL连接、Response接收等。通过继承此接口,调用者可以监视整个应用中网络请求次数、流量大小、耗时情况。
使用方法如下:
public class HttpEventListener extends EventListener {
/**
* 自定义EventListener工厂
*/
public static final Factory FACTORY = new Factory() {
final AtomicLong nextCallId = new AtomicLong(1L);
@Override
public EventListener create(Call call) {
long callId = nextCallId.getAndIncrement();
return new HttpEventListener(callId, call.request().url(), System.nanoTime());
}
};
/**
* 每次请求的标识
*/
private final long callId;
/**
* 每次请求的开始时间,单位纳秒
*/
private final long callStartNanos;
private StringBuilder sbLog;
public HttpEventListener(long callId, HttpUrl url, long callStartNanos) {
this.callId = callId;
this.callStartNanos = callStartNanos;
this.sbLog = new StringBuilder(url.toString()).append(" ").append(callId).append(":");
}
private void recordEventLog(String name) {
long elapseNanos = System.nanoTime() - callStartNanos;
sbLog.append(String.format(Locale.CHINA, "%.3f-%s", elapseNanos / 1000000000d, name)).append(";");
if (name.equalsIgnoreCase("callEnd") || name.equalsIgnoreCase("callFailed")) {
//打印出每个步骤的时间点
NearLogger.i(sbLog.toString());
}
}
@Override
public void callStart(Call call) {
super.callStart(call);
recordEventLog("callStart");
}
@Override
public void dnsStart(Call call, String domainName) {
super.dnsStart(call, domainName);
recordEventLog("dnsStart");
}
@Override
public void dnsEnd(Call call, String domainName, List inetAddressList) {
super.dnsEnd(call, domainName, inetAddressList);
recordEventLog("dnsEnd");
}
@Override
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
super.connectStart(call, inetSocketAddress, proxy);
recordEventLog("connectStart");
}
@Override
public void secureConnectStart(Call call) {
super.secureConnectStart(call);
recordEventLog("secureConnectStart");
}
@Override
public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
super.secureConnectEnd(call, handshake);
recordEventLog("secureConnectEnd");
}
@Override
public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {
super.connectEnd(call, inetSocketAddress, proxy, protocol);
recordEventLog("connectEnd");
}
@Override
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) {
super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe);
recordEventLog("connectFailed");
}
@Override
public void connectionAcquired(Call call, Connection connection) {
super.connectionAcquired(call, connection);
recordEventLog("connectionAcquired");
}
@Override
public void connectionReleased(Call call, Connection connection) {
super.connectionReleased(call, connection);
recordEventLog("connectionReleased");
}
@Override
public void requestHeadersStart(Call call) {
super.requestHeadersStart(call);
recordEventLog("requestHeadersStart");
}
@Override
public void requestHeadersEnd(Call call, Request request) {
super.requestHeadersEnd(call, request);
recordEventLog("requestHeadersEnd");
}
@Override
public void requestBodyStart(Call call) {
super.requestBodyStart(call);
recordEventLog("requestBodyStart");
}
@Override
public void requestBodyEnd(Call call, long byteCount) {
super.requestBodyEnd(call, byteCount);
recordEventLog("requestBodyEnd");
}
@Override
public void responseHeadersStart(Call call) {
super.responseHeadersStart(call);
recordEventLog("responseHeadersStart");
}
@Override
public void responseHeadersEnd(Call call, Response response) {
super.responseHeadersEnd(call, response);
recordEventLog("responseHeadersEnd");
}
@Override
public void responseBodyStart(Call call) {
super.responseBodyStart(call);
recordEventLog("responseBodyStart");
}
@Override
public void responseBodyEnd(Call call, long byteCount) {
super.responseBodyEnd(call, byteCount);
recordEventLog("responseBodyEnd");
}
@Override
public void callEnd(Call call) {
super.callEnd(call);
recordEventLog("callEnd");
}
@Override
public void callFailed(Call call, IOException ioe) {
super.callFailed(call, ioe);
recordEventLog("callFailed");
}
}
自定义EventListener实现和EventListener工厂实现,其中每个网络请求都对应一个EventListener监听。对于相同地址的请求,为了区分其对应的EventListener,需要通过EventListener工厂创建带有唯一标识的EventListener。这里是为每个EventListener分配唯一的自增编号。
在创建OkHttpClient时将EventListener工厂作为构建参数添加进去。
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.eventListenerFactory(HttpEventListener.FACTORY)
.build();
下面详细说明每个回调的触发时机:
当一个Call(代表一个请求)被同步执行或被添加异步队列中时。
//okhttp3
final class RealCall implements Call {
@Override
public Response execute() throws IOException {
eventListener.callStart(this);
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
}
@Override
public void enqueue(Callback responseCallback) {
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
}
由于线程或事件流的限制,这里的请求开始并不是真正的去执行的这个请求。
如果发生重定向和多域名重试时,这个方法也仅被调用一次。
每一个callStart都对应着一个callFailed或callEnd。
callFailed在两种情况下被调用
第一种是在请求执行的过程中发生异常时。
第二种是在请求结束后,关闭输入流时产生异常时。
//okhttp3
final class RealCall implements Call {
@Override
public Response execute() throws IOException {
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
}
}
final class AsyncCall extends NamedRunnable {
@Override
protected void execute() {
try {
Response response = getResponseWithInterceptorChain();
} catch (IOException e) {
eventListener.callFailed(RealCall.this, e);
}
}
}
}
//okhttp3.internal.connection
public final class StreamAllocation {
public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
...
if (e != null) {
eventListener.callFailed(call, e);
} else if (callEnd) {
eventListener.callEnd(call);
}
...
}
}
callEnd也有两种调用场景。
第一种也是在关闭流时。
第二种是在释放连接时。
//okhttp3.internal.connection
public final class StreamAllocation {
public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
...
if (e != null) {
eventListener.callFailed(call, e);
} else if (callEnd) {
eventListener.callEnd(call);
}
...
}
public void release() {
...
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
eventListener.callEnd(call);
}
}
}
为什么会将关闭流和关闭连接区分开?
在http2版本中,一个连接上允许打开多个流,OkHttp使用StreamAllocation来作为流和连接的桥梁。当一个流被关闭时,要检查这条连接上还有没有其他流,如果没有其他流了,则可以将连接关闭了。
streamFinished和release作用是一样的,都是关闭当前流,并检查是否需要关闭连接。
不同的是,当调用者手动取消请求时,调用的是release方法,并由调用者负责关闭请求输出流和响应输入流。
DNS解析是请求DNS(Domain Name System)服务器,将域名解析成ip的过程。
域名解析工作是由JDK中的InetAddress类完成的。
//java.net.InetAddress
public class InetAddress implements java.io.Serializable {
...
public static InetAddress[] getAllByName(String host)
throws UnknownHostException {
return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
}
}
这个解析过程会默认交给系统配置的DNS服务器完成。当然,我们也可以自定义DNS服务器的地址。
返回值InetAddress[]是一个数组,代表着一个域名可能对应着无数多个ip地址,像腾讯的域名对应了十几个ip地址。OkHttp会依次尝试连接这些ip地址,直到连接成功。
//okhttp3.Dns
public interface Dns {
List lookup(String hostname) throws UnknownHostException;
}
OkHttp中定义了一个Dns接口,其中的lookup(String hostname)方法代表了域名解析的过程。
dnsStart/dnsEnd就是在lookup前后被调用的。
//okhttp3.internal.connection.RouteSelector
public final class RouteSelector {
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
...
if (proxy.type() == Proxy.Type.SOCKS) {
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else {
eventListener.dnsStart(call, socketHost);
}
//dns解析
List addresses = address.dns().lookup(socketHost);
eventListener.dnsEnd(call, socketHost, addresses);
}
}
OkHttp是使用Socket接口建立Tcp连接的,所以这里的连接就是指Socket建立一个连接的过程。
//okhttp3.internal.connection.RealConnection
public final class RealConnection extends Http2Connection.Listener implements Connection {
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
...
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);
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
}
}
因为创建的连接有两种类型(服务端直连和隧道代理),所以callEnd有两处调用位置。为了在基于代理的连接上使用SSL,需要单独发送CONECT请求。
//okhttp3.internal.connection
public final class RealConnection extends Http2Connection.Listener implements Connection {
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
while (true) {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
//连接结束
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
}
}
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++) {
connectSocket(connectTimeout, readTimeout, call, eventListener);
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
if (tunnelRequest == null) break; // Tunnel successfully created.
// 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);
}
}
}
如果我们使用了HTTPS安全连接,在TCP连接成功后需要进行TLS安全协议通信,等TLS通讯结束后才能算是整个连接过程的结束,也就是说connectEnd在secureConnectEnd之后调用。
当存在重定向或连接重试的情况下,secureConnectStart/secureConnectEnd会被调用多次。
在上面看到,在Socket建立连接后,会执行一个establishProtocol方法,这个方法的作用就是TSL/SSL握手。
//okhttp3.internal.connection
public final class RealConnection extends Http2Connection.Listener implements Connection {
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
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;
}
//安全连接开始
eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);
//安全连接结束
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
startHttp2(pingIntervalMillis);
}
}
}
在连接过程中,无论是Socket连接失败,还是TSL/SSL握手失败,都会回调connectEnd。
//okhttp3.internal.connection
public final class RealConnection extends Http2Connection.Listener implements Connection {
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
...
while (true) {
try {
...
} catch (IOException e) {
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
}
}
}
因为OkHttp是基于连接复用的,当一次请求结束后并不会马上关闭当前连接,而是放到连接池中,当有相同域名的请求时,会从连接池中取出对应的连接使用,减少了连接的频繁创建和销毁。
当根据一个请求从连接池取连接时,并打开输入输出流就是acquired,用完释放流就是released。
connectionAcquired是在连接成功后被调用的。但是在连接复用的情况下没有连接步骤,connectAcquired会在获取缓存连接后被调用。由于StreamAllocation是连接“Stream”和“Connection”的桥梁,所以在StreamAllocation中会持有一个RealConnection引用。StreamAllocation在查找可用连接的顺序为:StreamAllocation.RealConnection -> ConnectionPool -> ConnectionPool -> new RealConnection
如果直接复用StreamAllocation中的连接,则不会调用connectionAcquired/connectReleased。
//okhttp3.internal.connection
public final class StreamAllocation {
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
// 第一次查缓存 Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
//第二次查缓存
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 we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
//如果缓存没有,则新建连接
result = new RealConnection(connectionPool, selectedRoute);
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
eventListener.connectionAcquired(call, result);
}
}
找到合适的连接后,会在基于当前连接构建Http的编解码器HttpCodec,来解析Http请求和响应。
当一个流被主动关闭或异常关闭时,就需要把这个流对应的资源释放(deallocate)掉。
资源释放的两个方面:
1. 将StreamAllocation的引用从RealConnection的队列中移除掉
2. 将RealConnection在连接池中变成空闲状态
在OkHttp中,HttpCodec负责对请求和响应按照Http协议进行编解码,包含发送请求头、发送请求体、读取响应头、读取响应体。
//okhttp3.internal.http
public final class CallServerInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
...
//发送请求头
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
//发送请求体
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);
//读取响应头
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) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
}
}
响应体的读取有些复杂,要根据不同类型的Content-Type决定如何读取响应体,例如固定长度的、基于块(chunk)数据的、未知长度的。同时Http1与Http2也有不同的解析方式。下面以Http1为例。
//okhttp3.internal.http1
public final class Http1Codec implements HttpCodec {
@Override
public ResponseBody openResponseBody(Response response) throws IOException {
//开始解析响应体
streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
...
}
}
//okhttp3.internal.connection
public final class StreamAllocation {
public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
//响应体解析结束
eventListener.responseBodyEnd(call, bytesRead);
...
}
}