https://blog.csdn.net/joye123/article/details/82115562?utm_source=blogxgwz9
OkHttp 之 网络请求耗时统计
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
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");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
自定义EventListener实现和EventListener工厂实现,其中每个网络请求都对应一个EventListener监听。对于相同地址的请求,为了区分其对应的EventListener,需要通过EventListener工厂创建带有唯一标识的EventListener。这里是为每个EventListener分配唯一的自增编号。
在创建OkHttpClient时将EventListener工厂作为构建参数添加进去。
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.eventListenerFactory(HttpEventListener.FACTORY)
.build();
1
2
3
下面详细说明每个回调的触发时机:
callStart(Call call) 请求开始
当一个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));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
由于线程或事件流的限制,这里的请求开始并不是真正的去执行的这个请求。
如果发生重定向和多域名重试时,这个方法也仅被调用一次。
callFailed/callEnd 请求异常和请求结束
每一个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);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//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);
}
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
为什么会将关闭流和关闭连接区分开?
在http2版本中,一个连接上允许打开多个流,OkHttp使用StreamAllocation来作为流和连接的桥梁。当一个流被关闭时,要检查这条连接上还有没有其他流,如果没有其他流了,则可以将连接关闭了。
streamFinished和release作用是一样的,都是关闭当前流,并检查是否需要关闭连接。
不同的是,当调用者手动取消请求时,调用的是release方法,并由调用者负责关闭请求输出流和响应输入流。
dnsStart/dnsEnd dns解析开始/结束
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();
}
}
1
2
3
4
5
6
7
8
9
这个解析过程会默认交给系统配置的DNS服务器完成。当然,我们也可以自定义DNS服务器的地址。
返回值InetAddress[]是一个数组,代表着一个域名可能对应着无数多个ip地址,像腾讯的域名对应了十几个ip地址。OkHttp会依次尝试连接这些ip地址,直到连接成功。
//okhttp3.Dns
public interface Dns {
List
}
1
2
3
4
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
eventListener.dnsEnd(call, socketHost, addresses);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
connectStart/connectEnd 连接开始结束
OkHttp是使用Socket接口建立Tcp连接的,所以这里的连接就是指Socket建立一个连接的过程。
当连接被重用时,connectStart/connectEnd不会被调用。
当请求被重定向到新的域名后,connectStart/connectEnd会被调用多次。
//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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
因为创建的连接有两种类型(服务端直连和隧道代理),所以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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
secureConnectStart/secureConnectEnd TLS安全连接开始和结束
如果我们使用了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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
connectEnd 连接失败
在连接过程中,无论是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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
connectionAcquired/connectReleased 连接绑定和释放
因为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
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
找到合适的连接后,会在基于当前连接构建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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
响应体的读取有些复杂,要根据不同类型的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);
...
}
}
1
2
3
4
5
6
7
8
9
10
11
//okhttp3.internal.connection
public final class StreamAllocation {
public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
//响应体解析结束
eventListener.responseBodyEnd(call, bytesRead);
...
}
}
---------------------
作者:joye123
来源:CSDN
原文:https://blog.csdn.net/joye123/article/details/82115562
版权声明:本文为博主原创文章,转载请附上博文链接!