ConnectInterceptor作为CacheInterceptor的后一个缓存,已经没有缓存这条退路了,必须要真刀真枪的请求网络了。到了现在才真正开始请求网络获取数据。从名字可以看出这个类是负责连接的。
看下具体拦截intercept方法的实现:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// 创建HttpCodec和RealConnection
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
// 执行下一个拦截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
ConnectInterceptor的拦截逻辑比较简单,主要是准备RealConnection和HttpCodec,这两个是请求的核心类。这两个类都是通过StreamAllocation创建出来的,StreamAllocation在第一个拦截器RetryAndFollowUpInterceptor就已经创建出来了。
在ConnectInterceptor执行后,就达到了万事俱备的条件(已经连接成功),那么东风的到来就是到了最后一个拦截器CallServerInterceptor中,这个拦截器内部才是请求网络的主战场。这篇我们先分析ConnectInterceptor和StreamAllocation。
让我们先宏观看下这最后两个拦截器和他们之中组件的功能。
宏观分析
我们先宏观分析下OKHttp的网络请求部分,OKHttp的运行在网络的应用层之上,主要是实现了Http的逻辑,那么要使用网络请求,那么肯定需要往下层走,也就是TCP/UDP的网络层,在安卓的代码中,怎么使用这两种请求呢,其实安卓的操作系统已经给我们实现好了,我们只要简单的调用一下,传入我们需要传输的Http数据报,自然就完成了剩下的所有的工作。
OKHttp在这方面的工作也是一样的,单纯的使用系统函数进行网络请求,谁都会,但是还要考虑效率和其他的一些应用,这就比较难了。OKHttp在这方面的做法从上面介绍的最后两个拦截器中就可以看出,分为ConnectInterceptor负责连接,CallServerInterceptor负责传输数据。
连接过程
连接是什么,这是一个非常抽象的词。在网络中,就是指连接到服务器,建立一条通道,Http底层TCP是需要提前进行连接的,也就是三次握手,完成双发之间的约定,下面就可以正式的传递数据了。不建
立连接可否进行传输数据呢,UDP就是这个逻辑,但是不建立连接就不能保证可靠传输。连接需要进行三次握手,成功的建立是非常耗费资源的,所以能复用则复用,如果下次也是往这个服务器发送数据,当然是可以复用这个连接的。在OKHttp中,这个连接就是RealConnection,而负责复用连接的类就是ConnectInterceptor。
底层的连接时通过Socket#connect()进行得。
传输过程
在上一步通过Socket#connect()建立连接以后,我们就可以往输入流中输入数据了,就是这么简单的完成了网络数据的传输,等着输出流的数据到来。至于底层的数据怎么在链路上进行传送,我们一概不用知道。CallServerInterceptor就是负责这个功能的。在OkHttp中负责传送数据的就是HttpCodec,他内部通过okio封装了Socket的输入/输出流,可以简单的理解为一个流,一个OkHttp的流。
那StreamAllocation的作用是什么呢,因为连接过程的RealConnection需要和传输过程的HttpCodec进行通信,StreamAllocation相当于一个中介,使用到了中介者模式,这样连接和发送就能更专心的去完成自己的工作了。负责协调两者之间的工作,不但可以让它们各自的核心逻辑封装在各自的类内,不用关心和其他部分的通信部分。
经过上面对整体的OkHttp网络请求的具体分析,应该对大概有了了解。下面我们先分析连接部分。下一篇再分析传输部分。 本篇先跳过Http2.0和Https的处理代码,先主要分析整个流程。
ConnectInterceptor
这里我们先看下顶层ConnectInterceptor的代码,从外部使用角度分析下StreamAllocation 和 RealConnection两个类。ConnectInterceptor拦截方法中,对StreamAllocation的外部调用很简单。
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
先通过StreamAllocation的newSteam创建了HttpCodec,也就是数据传输的控件。后面直接调用了connection方法直接获取了连接类RealConnection。
简单的两部连接和传输的两大控件都已经创建好了。ConnectInterceptor就是这么的简单。下面我们先分析StreamAllocation。先通过newStream()和connection()为入口。
StreamAllocation
connection()的代码比较简单,直接获取了connection变量,可见在newStream()方法中就已经完成了连接和传输两个功能控件创建的工作。主要的逻辑都在newStream()中。
public synchronized RealConnection connection() {
return connection;
}
newStream()具体代码如下。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
。。。
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
直接调用了findHealthyConnection找到可以使用的RealConnection,再通过RealConnection的newCodec新建一个传输的控件HttpCodec。findHealthyConnection直接调用了findConnection方法,核心的逻辑都在findConnection中。也就到了ConnectionPool的连接缓存部分了,为什么这部分也有缓存的,因为连接需要经过3次握手,还要经过TCP的慢启动等阶段,如果每次都进行重新创建的话,是非常耗时的。
ConnectionPool缓存连接
构造函数
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
构造函数可以传入两个配置,一个就是最大的连接数,一个是最长等待时间。
缓存获取
findConnection的方法比较长,总览就是获取缓存Connection,和RecyclerView获取缓存有点像。获取分为三步。
- 看
StreamAllocation#connection是否可用 - 没有,从
ConnectionPool获取,这部分获取分为两步,首先不带router获取,再获取router再次获取 - 没有,创建一个新的Connection 是不是和RecyclerView获取ViewHolder缓存类似呢,只不过这里是两级缓存,下面我们逐步分析下每个获取步骤。
一级缓存:本地StreamAllocation#connection是否可用
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
result = this.connection;
releasedConnection = null;
}
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
route = connection.route();
return result;
}
result变量就是最终的目标,如果result不为空,那么表示我们已经找到了可用的连接,直接return即可。那这个连接是否可以复用,是通过releaseIfNoNewStreams这个方法判断的,如果没有被回收。那么就是可以进行复用的。
下面看下StreamAllocation#connection的赋值来源。
StreamAllocation#connection的来源
StreamAllocation中的connection对象,主要通过两个方法进行赋值,releaseAndAcquire 和acquire,前者是被Http2.0使用的,这里主要分析acquire。
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
acquire的逻辑比较简单,直接对connection进行赋值。并对allocations进行填充,因为一个连接可以有多个流,在Http1.1版本中,一个连接只可以有一个流,而Http2.0中一个连接可以有很多个流。如果在Http1.1中,connection.allocations这个集合只会有一个元素。
acquire调用又有两个地方,一个是从ConnectionPool获取成功后,一个是创建新的Connection后。我们发现这两个部分都是在findConnection的后面才进行的。
一级缓存生效条件
所以这个变量的重新利用,肯定是在第一次请求后,执行过findConnection后面的逻辑,找到真正可以使用的Connection后,并重复进行请求的场景下,进行的。比如在RetryAndFollowUpInterceptor拦截器中的重试逻辑等。也就是肯定是在第二次重试中才会复用这个连接。
一级缓存可用性判断
主要逻辑在releaseIfNoNewStreams方法中:
private Socket releaseIfNoNewStreams() {
assert (Thread.holdsLock(connectionPool));
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && allocatedConnection.noNewStreams) {
return deallocate(false, false, true);
}
return null;
}
主要逻辑就在noNewStreams这个变量中,如果这个变量为true,表示这个连接上不能创建新的流了,那么就会调用
deallocate进行回收,deallocate方法内部会置空connection变量,反之则不会进行回收。什么场景下这个值会为true呢?
比如服务器返回的数据中connection配置为close,那么这个连接就需要关闭了,noNewStreams就为true。当然还有其他很多场景。
总结是:这里指检查了noNewStreams,如果不为true,就可以进行复用。
二级缓存:ConnectionPool获取
如果上面的Connection不能使用,就会从ConnectionPool中获取,下面是获取的主要代码。
StreamAllocation.java
if (result == null) {
//只通过address进行匹配
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
route = connection.route();
return result;
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
//只通过address+router进行匹配
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
整体逻辑都是调用Internal.instance.get(connectionPool, address, this, route);从connectionPool获取可以复用的Connection。但是获取分为两部
- 只通过传入的address匹配
- 通过newRouteSelection判断是否根据router和adress进行匹配 整体逻辑就是这样,上面有两个点,需要分析下
什么是Router
Router翻译过来表示路由,通过RouteSelector进行获取,内部包含三个变量
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
| 字段 | 意义 |
|---|---|
| proxy | 代理,表示连接的代理服务器 |
| address | 表示连接到源服务器的规范,包括Url和协议和dns等 |
| inetSocketAddress | IP地址 |
while (hasNextProxy()) {
Proxy proxy = nextProxy();
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
} else {
routes.add(route);
}
}
}
router的创建主要通过上面的代码进行获取,inetSocketAddresses是从dns获取的IP地址列表,而nextProxy()获取下一个可用的代码。这里通过双层嵌套的循环,获取了代理proxy和可选IP地址的笛卡尔集。
可以得到:Router表示连接到代理服务器的途径,通过不同的IP地址或者不同的代理。当一条router不可用时,就可以使用下一条router。
ConnectionPool匹配规则
主要通过get方法进行获取
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
内部遍历了缓存的所有connections,之后通过connection.isEligible进行匹配,所有的匹配逻辑都在connection.isEligible中。如果匹配了,就通过上面谈过的acquire方法继续进行处理,
public boolean isEligible(Address address, @Nullable Route route) {
// 如果这个连接不接受新的流,不能复用
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// 如果地址的非主机字段不重叠,不能复用
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// 如果主机完全匹配,可以复用
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// 主机也不匹配,如果使用的是Http2.0协议 还有机会
if (http2Connection == null) return false;
// Http2.0协议下,如果复用需要满足的条件
if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
如果正在使用的协议是Http2.0,复用的协议就更加的灵活了。分两部分看下
协议共有判断
- 缓存的Connection,可以创建的流的数量不能超过
allocationLimit限制,上面也说过这个变量,Http1.1只能创建一个流 Internal.instance.equalsNonHost判读请求的address和缓存的address的变量,必须全部一致。包括以下变量。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(); }- 如果上面的两部通过了,判断url的
host()部分,如果一样,说明这两个Connection是可以进行复用的。 - 如果两个host不一样呢,还有补救的措施,就是使用的是Http2.0协议,并且满足一些条件。
Http2.0独有判断
- 外部必须通过router进行匹配了,并且缓存的代理和新请求的代理类型不能是直连接,也就是没有代理。说明这种情况需要有代理的情况下使用。
- 后面包括了Https的相关处理,这里先略过了,包括
certificatePinner和hostnameVerifier
上面就是从ConnectionPool获取缓存的全部逻辑,那么ConnectionPool什么时候插入缓存呢。
ConnectionPool插入时机
如果不是从ConnectionPool获取的连接就会加入ConnectionPool中。
插入的代码在通过Socket发送连接请求的下面,也就是说如果成功的进行了连接,才会插入到ConnectionPool。通过Internal.instance.put(connectionPool, result);进行插入。
如果连接失败了就不会插入了。
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
插入的时候,如果cleanupRunning为false,会提交一个线程到线程池,以做清理工作。
cleanupRunning是一种懒加载的模式。只有第一次才会进行。
ConnectionPool清除
清除的工作就是在加入的时候,在cleanupRunnable中完成的。
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
内部的逻辑先通过cleanup方法获取需要等待的时间。如果是-1,就表示没有可以回收的连接了。
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
内部的逻辑,整体分为两个部分,第一个就是遍历所有的连接获取当前的情况,得到三个数据
- 正在使用的连接
- 空闲的连接
- 最长的过期时间
得到上面的数据后,就会经过几个判断。
- 如果当前最大过期时间比传入的还大,那么就清除这个连接,并关闭socket
- 否则,如果有空闲的数量,就返回最近需要等待的时间
- 否则,如果有正在运行的连接,就返回最长的等待时间
- 否则,表示没有连接,返回-1,表示推出清除的线程池
获取缓存失败:新建Connection
上面两部都没有获取可用的连接,只能创建一个新的连接了。
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
这里选择了第一个router,并创建了一个新的RealConnection。下面调用了acquire进行赋值。
通过上面的三步获取,肯定拿到了合适的Connection了,下面就到了激动人心的时候了,正式开始连接。
连接过程
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
直接调用了RealConnection的connect方法。
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
。。。
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
。。。
}
省略了部分代码,核心逻辑就是这么几行 这里判断了是否需要隧道,OkHttp支持是使用Http隧道传输Https的数据。隧道的支持后面会说。 connectSocket内部直接调用了
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
调用安卓系统提供的rawSocket,直接使用Tcp请求网络。 establishProtocol方法内部判断了是否使用Https和Http2.0,并提供了支持,这些部分专门后面会分析。
总结
纵观整个连接的过程,底层的逻辑比较简单,就是调用Socket的connect方法,但是这里涉及到了连接的复用和路由等处理。通过上面的分析,应该对OKHttp怎么建立连接,有了了解吧。 OkHttp连接和传输的3个主要重要的类分别是StreamAllocation 、RealConnection、HttpCodec。他们之间的关系如下
graph TD
RealConnection --> StreamAllocation1
RealConnection --> StreamAllocation2
RealConnection --> StreamAllocation3
StreamAllocation1 --> HttpCodec1
StreamAllocation2 --> HttpCodec2
StreamAllocation3 --> HttpCodec3
也就是一个连接上可以建立多个流,当然只在Http2.0的情况下可以。StreamAllocation相当于一个中介,连接两者的关系,功能包括获取一个连接,在连接上新建一个流。RealConnection就是一个连接,包括隧道、TSL、Http1.1、HTTP2.0的不同配置连接等。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。