OkHttp 之 网络请求耗时统计

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

自定义EventListener实现和EventListener工厂实现,其中每个网络请求都对应一个EventListener监听。对于相同地址的请求,为了区分其对应的EventListener,需要通过EventListener工厂创建带有唯一标识的EventListener。这里是为每个EventListener分配唯一的自增编号。
在创建OkHttpClient时将EventListener工厂作为构建参数添加进去。

OkHttpClient.Builder builder = new OkHttpClient.Builder()
          .eventListenerFactory(HttpEventListener.FACTORY)
          .build();

下面详细说明每个回调的触发时机:

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

由于线程或事件流的限制,这里的请求开始并不是真正的去执行的这个请求。
如果发生重定向和多域名重试时,这个方法也仅被调用一次。

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

            }
        }
    }

}
//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方法,并由调用者负责关闭请求输出流和响应输入流。

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

这个解析过程会默认交给系统配置的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);
    }
}

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

因为创建的连接有两种类型(服务端直连和隧道代理),所以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);
            }
      }
}

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

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

}      

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

      }
}

找到合适的连接后,会在基于当前连接构建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);
        ...
    }
}

你可能感兴趣的:(Android)