OkHttp 之 网络请求耗时统计

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 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");

    }

}

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 lookup(String hostname) throws UnknownHostException;

}

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 addresses = address.dns().lookup(socketHost);

        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 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);

      }

}

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

版权声明:本文为博主原创文章,转载请附上博文链接!

你可能感兴趣的:(OkHttp 之 网络请求耗时统计)