前言
面试官:OkHttp如何进行请求日志统计,各个请求环节的耗时统计呢?
答:OkHttp提供了一个网络事件的回掉EventListener,可以统一处理各个环节进行耗时统计。
OkHttp#EventListener 回调原理
public abstract class EventListener {
// 按照请求顺序回调
public void callStart(Call call) {}
// 释放当前Transmitter的RealConnection
public void connectionReleased(Call call, Connection connection) {}
// 只要拿到了RealConnection就会回调 分为三种情况分别是从当前Transmitter中的RealConnection复用,连接池中获取RealConnection,和新创建一个RealConnection会回调一次。
public void connectionAcquired(call, result){};
// 域名解析
public void dnsStart(Call call, String domainName) {}
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {}
// 开始连接
public void connectStart(call, route.socketAddress(), proxy){}
public void callEnd(Call call) {}
public void callFailed(Call call, IOException ioe) {}
}
其实探索EventListener各个回调的位置,基本上将OkHttp的请求环节都走了一遍,挺有价值的一次学习。
先看这三个回调的触发位置,我们知道OkHttp通过Dispatcher调度器(两个异步队列,一个同步队列)进行网络请求的任务调度,最终的请求过程执行在RealCall类中。所以以上三个回调将在RealCall中体现。
RealCall
// 同步
@Override public Response execute() throws IOException {
//... 省略代码
transmitter.callStart();
}
// 异步
@Override public void enqueue(Callback responseCallback) {
//... 省略代码
transmitter.callStart();
}
callStart()没的说,在调用同步异步请求的时候就会回调这个方法,而且每次请求只会调用一次。
我们了解无论执行同步异步都会最终走OkHttp提供的5个拦截器,第一个重定向拦截器只有当一次请求结束,责任链反向回传Response才能知道当前的请求地址是否被重定向,所以在这里只有可能回调callEnd()、callFailed()这里之后说。第二个拦截器是BridgeInterceptor用于将客户端的请求转换为服务端需要的请求,也就是配置请求报文头相关的信息,这里也不涉及请求的过程只是配置请求。第三个缓存拦截器,处理请求缓存的,一旦该次请求被判断满足直接使用缓存的条件,直接就返回缓存Response,因此后续的EnventListener回调事件就不会再回调 直接会回调callEnd()或者callFailed() ,这里我们为了全面就忽略缓存了。第四个拦截器连接拦截器,就涉及很多了,其主要目的将Transmitter与ExchangeFinder构建出来,此类公开了高级应用程序层原语:连接,请求,响应和流,它持有okhttpclient对象以及RealCall对象。ExchangeFinder从连接池中找到一个健康可用的连接进行复用,没有找到的话,就创建一个路由列表,并创建一个新连接,因此我们的dnsStart()回调就在这个创建新连接的过程中体现。第五个CallServerInterceptor拦截器作用就是由客户端向服务端拿数据的过程,ConnectInterceptor中建立连接后通过责任链将Exchange与最终的Request传递给当前拦截器,并发起最终的请求,获取Response返回。
ConnectInterceptor
获取构建Http1ExchangeCodec路径
ConnectInterceptor ->
Transmitter.newExchange(xxx,xxx) ->
ExchangeFinder.find(xxx,xxx,xxx) ->
ExchangeFinder.findHealthyConnection() ->
ExchangeFinder.findConnection()
ok 以上是层层调用,最终到达ExchangeFinder.findConnect()的方法,这里进行了是否能找到一个健康可用的连接通道,也就是keepalive状态的Http请求,dns解析,挥手,等等过程都建立好了,没有找到就要重新走各个建立连接的流程。
ExchangeFinder
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
//... 省略
// 是否要断开Socket
Socket toClose;
// 最终请求的连接通路
RealConnection result = null;
// 要释放掉的连接通路
RealConnection releasedConnection;
releasedConnection = transmitter.connection;
// 是否需要断开 条件是当前的RealConnection不为空,并且该RealConnection的noNewExchanges是否还可以进行创建连接
toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
? transmitter.releaseConnectionNoEvents()
: null;
if (transmitter.connection != null) {
// 如果当前已经有了一个满足使用的连接通路就直接用这个通道
result = transmitter.connection;
// 因为找到了满足通路的connection,所以要被释放的RealConnect设置为空
releasedConnection = null;
}
// 若没有
if (result == null) {
// 去连接池中找一个满足条件的连接通路
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
foundPooledConnection = true;
result = transmitter.connection;
} else if (nextRouteToTry != null) {
selectedRoute = nextRouteToTry;
nextRouteToTry = null;
} else if (retryCurrentRoute()) {
selectedRoute = transmitter.connection.route();
}
}
// 断开socket
closeQuietly(toClose);
// 若希望被释放掉Kill掉的RealConnection不为空
if (releasedConnection != null) {
// 直接回调
eventListener.connectionReleased(call, releasedConnection);
}
// 若从连接池中找到了一个可用健康的连接通路,就回调这个Event
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
// 若找到了RealConnection连接通路就返回
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result;
}
// 下方的代码将重新构建一个RealConnection
// 从连接池中若没有找到,则要重新创建一个
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
// 1
routeSelection = routeSelector.next();
}
List<Route> routes = null;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
if (newRouteSelection) {
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(
address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
// 在连接池中没有找到可用连接
if (!foundPooledConnection) {
if (selectedRoute == null) {
// 从上方1处 刚刚创建的
selectedRoute = routeSelection.next();
}
// 再次创建一个RealConnection
result = new RealConnection(connectionPool, selectedRoute);
connectingConnection = result;
}
}
// TCP + TLS 握手过程...
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
// 关闭socket
closeQuietly(socket);
// 回调获得了新的Connection
eventListener.connectionAcquired(call, result);
return result;
}
上述代码中描述流程很清楚了,先找到当前的RealConnection中正在建立Socket连接的RealConnection,然后若当前的连接通路不能够再次创建新的数据交换的连接,并且从连接池中也找不到一个健康可用的连接,就最终会调用closeQuietly(toClose)去关闭Socket,并且去创建一个新的连接路由, 与新的RealConnection进行绑定。我们来看下代码1处的流程,其中包含DNS解析的过程。简单总结下,就是找一个可复用的连接,如果没有找到,就创建一个连接并放入连接池中返回,之后拿到构造好的ExchageCodec。
RouteSelector#resetNextInetSocketAddress(Proxy proxy)
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// 域名解析后的存放IP的集合
inetSocketAddresses = new ArrayList<>();
String socketHost;
int socketPort;
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
// 获取 host 与 port
socketHost = address.url().host();
socketPort = address.url().port();
} else {
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException(
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
if (proxy.type() == Proxy.Type.SOCKS) {
// 若是WebSocket,则跳过DNS解析
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else {
// Event dns回调 开始解析DNS
eventListener.dnsStart(call, socketHost);
// Try each address for best behavior in mixed IPv4/IPv6 environments.
// 默认使用底层Java提供的解析DNS 实现类 Inet6AddressImpl
List<InetAddress> addresses = address.dns().lookup(socketHost);
if (addresses.isEmpty()) {
throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost);
}
// Event dns回调 解析DNS完成
eventListener.dnsEnd(call, socketHost, addresses);
for (int i = 0, size = addresses.size(); i < size; i++) {
InetAddress inetAddress = addresses.get(i);
inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}
}
Transmitter
private @Nullable IOException maybeReleaseConnection(@Nullable IOException e, boolean force) {
Socket socket;
Connection releasedConnection;
boolean callEnd;
synchronized (connectionPool) {
if (force && exchange != null) {
throw new IllegalStateException("cannot release connection while it is in use");
}
releasedConnection = this.connection;
socket = this.connection != null && exchange == null && (force || noMoreExchanges)
? releaseConnectionNoEvents()
: null;
if (this.connection != null) releasedConnection = null;
callEnd = noMoreExchanges && exchange == null;
}
closeQuietly(socket);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (callEnd) {
boolean callFailed = (e != null);
e = timeoutExit(e);
if (callFailed) {
eventListener.callFailed(call, e);
} else {
eventListener.callEnd(call);
}
}
return e;
}
未完待续....
网友评论