开篇声明:由于本篇博文用到的一些观点或者结论在之前的博文中都已经分析过,所以本篇博文直接拿来用,建议读此博文的Monkey们按照下面的顺序读一下博主以下博文,以便于对此篇博文的理解:
《Okhttp源码简单解析(一)》
《OkHttp之BridgeInterceptor简单分析》
《 Okhttp之CacheInterceptor简单分析》
《OkHtp之ConnectInterceptor简单分析》
《Okhttp之RouteSelector简单解析》
闲言少叙,书归正传,在ConnectionInterceptor这篇博文的分析中我们可以得到一下结论:
1、在ConnectionInterceptor的intercept方法中通过调用StreamAllocation对象的newStream方法。
2、在newStream方法里面会有一个while循环,执行findConnection方法查找一个RealConnection.
findConnection的初步解释可以参考《OkHtp之ConnectInterceptor简单分析》
在从连接池获ConnectionPool获取RealConnection对象的时候,get方法会调用两次(对findConection剔除了与本博文无关的代码之后如下所示):
private RealConnection findConnection(。。。){
Route selectedRoute;
synchronized (connectionPool) {
。。。。
//从连接池获取一个连接,此时最后一个参数route传的为null
Internal.instance.get(connectionPool, address, this, null);
//成功从连接池中获取一个连接,返回之
if (connection != null) {
return connection;
}
//当前对象使用的路由对象
selectedRoute = route;
}
//选中一个路由
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
}
RealConnection result;
synchronized (connectionPool) {
//从缓冲池中获取对象
Internal.instance.get(connectionPool, address, this, selectedRoute);
//缓存池中有此连接
。。。。。
}
。。。。。
return result;
}
很清晰的可以看出第一次尝试从连接池中获取 RealConnection对象的时候route传null,如果此时没有获取到可用的链接,则选中一个路由后(routeSelector.next()),再次重连接池中获取RealConnection,(注意在调用连接池的时候将自身(this)也就是StreamAllocation对象也传了过去)。 (注:关于路由选择这块详情参考《Okhttp之RouteSelector简单解析》)
先看看是怎么从连接池中获取链接的:
//此方法为ConnectionPool对象的方法
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
//遍历连接池
for (RealConnection connection : connections) {
//判断链接是否合格
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}
上面的代码也很简单:
1、循环遍历连接池connections(本质是一个ArrayDeque),从中获取一个可用链接。
2、如果为可用链接,则调用streamAllocation的acquire方法。
在这里有两个重要的方法:一个是acquire方法;一个是isEligible,它根据地址address和路由route判断链接是否为可用(或可复用)。先分析分析StreamAllocation对象的acquire方法:
private RealConnection connection;
public void acquire(RealConnection connection) {
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
该方法很简单:就是把从连接池中获取到的RealConnection对象赋值给StreamAllocation的connection属性,然后把StreamAllocation对象的弱引用添加到RealConnection对象的allocations集合中去,所以根据RealConnection对象的allocations很容易判断出当前链接对象所持有的StreamAllocation数目,该数组的大小用来判定一个链接的负载量是否超过指定的次数。
通过前面几篇关于Okhttp的的分析我们知道,每一次http请求会在RetryAndFollowUpInterceptor这个拦截器里创建一个StreamAllocation对象,或者简单的说每一次http请求都会产生一个StreamAllocation对象。这样说来如果若干个请求从链接池中获取的是同一的RealConnection对象,那么此时他们的关系可以简单的如下图所示:
从上图其实也可以发现说每次请求生成StreamAllocation对象请求链接时,首先要做的不是new 一个新的RealConnection对象,而是从链接池中获取已经存在的并且可以复用的RealConnection,如果找不到可用的链接,则才new 一个新的RealConnection(当然要把新创建的RealConnection放入链接池,以待别的请求复用之)。
上面简单的描述了下怎么从连接池获取连接,至于怎么判断链接池的某个连接是否可以复用,就是isEligible的作用了。在分析这个方法之前,先简单说下RealConnection:
RealConnection是Okhttp正式发起网络请求所使用的对象,该对象含有了一个通过RouteSelector选中Route对象,route对象正好持有了当前请求所访问的目标主机信息(InetSocketAddress)。事实上在上面所讲的findConnection方法中就是如果在ConnectionPool中找不到可用链接就根据选中的路由链初始化一个RealConnection交给StreamAllocation使用,当然此时会把新创建的链接放入ConnectionPool中,要不然链接池中的链接对象从何而来(这句算是没有bug的废话):
result = new RealConnection(connectionPool, selectedRoute);
//放入链接池
Internal.instance.put(connectionPool, result);
return result
那么到底是如何判断一个RealConnection是否可以让StreamAllocation使用呢?其实一个链接能否复用的条件我们能想到无外乎地址一样、端口一样、一条链接线路的负载不超过指定负载数等等这些条件。Okhttp对复用条件做了更多的限制,详见isEligible代码:
public boolean isEligible(Address address, @Nullable Route route) {
//1、负载超过指定最大负载,不可复用
if (allocations.size() >= allocationLimit || noNewStreams) return false;
//2、Address对象的非主机部分不相等,不可复用
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
//3、非主机部分不相等,不可复用
if(address.url().host().equals(this.route().address().url().host())) {
//这个链接完美的匹配
return true; // This connection is a perfect match.
}
//
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 4. This connection must be HTTP/2.
if (http2Connection == null) return false;
// The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
//5
if (route == null) return false;
//6
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
//7
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
//8
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 9
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 10. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
//最终可以复用
return true;
}
上面的代码可以发现基本上可以分成两大部分:
1、用StreamAllocation携带的Address对象跟RealConnection的Address相匹配;2、1不满足的情况下用StreamAllocation携带的Route(由RouteSelecor选择而来)与RealConnection的Route相匹配(具体的说是SocketAddress所表的信息)。
结合本文开头说的获取链接两次调用get方法的作用,StreamAllocation获取连接池中的链接的工作方式可以简单的如下表示:
现在为止再来分析分析isEligible的判定一个连接是否可复用的标准吧(对照上面源码的1、2、3.。。10的注释来说明):
1、如果此链接的负载数目超过指定数目(表现为RealConnection的allocations集合的数量超过该链接指定的数量)或者noNewStreams为true时,此链接不可复用。
2、StreamAllocation 所持有的Address对象和RealConnection的Address非主机部分不同,则此链接不可复用。至于非主机部分的判定是在Address的equalsNonHost方法来体现,代码如下:
boolean equalsNonHost(Address that) {
return this.dns.equals(that.dns)
&& this.proxyAuthenticator.equals(that.proxyAuthenticator)
&& this.protocols.equals(that.protocols)
&& this.connectionSpecs.equals(that.connectionSpecs)
&& this.proxySelector.equals(that.proxySelector)
&& equal(this.proxy, that.proxy)
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
&& equal(this.certificatePinner, that.certificatePinner)
&& this.url().port() == that.url().port();
}
两者Adress对象的非主机部分相等的标准就是dns,Authenticator对象、协议、CA授权验证标准、端口等信息全部相等。
3、在1、2判定条件都为true的话,如果两个Address对象的host或者说url中的host一样,则此链接可复用,正如注释说说,添加1、2、3都满足的话,那么此时这个链接就是This connection is a perfect match。
以上三点是Address对象的比较,如果步骤三能成功的话(地址的主机部分和非主机部分都一样),则就不需要route对象进行匹配验证了。否则则需要用route做进一步的验证来判断此链接是否可以复用。既然主机名都不想等了怎么还有复用的可能呢?由于牵扯到Http2等概念及博客篇幅问题,这个分析将在下一篇中详细分析,敬请期待《Okhttp之连接池ConnectionPool简单分析(二)》,如有不当之处,欢迎批评指正。