一、概述
上一节讲了OkHttp3的从创建HttpClient到最后调用call.enqueue(callback)来执行一个网络请求并接收响应结果的源码分析流程。流程分析下来能够帮助我们理解这个框架,在理解整个执行流程的基础上我们分析一下上一节未分析到的遗留问题。比如:OkHttp3的连接池的复用。
二、连接池原理
多少了解点OkHttp3的同学都知道,OkHttp可以降低网络延时加快网络请求响应的速度。那么它是怎样做到的呢?在说这个之前,我们先简单回顾一下Http协议。Http协议是一个无连接的协议,客户端(请求头+请求体)发送请求给服务端,服务端收到(请求头+请求体)后响应数据(响应头和响应体)并返回。由于Http协议的底层实现是基于TCP协议的(保证数据准确到达),所以在请求+响应的过程中必然少不了Tcp的三次握手和释放资源时的四次挥手。我们假设有这么一种情况,客户端需要每隔10秒向服务端发送心跳包,如果按照无连接的状态每次客户端请求和服务端响应都需要经过Tcp的三次握手和四次挥手,这样高频率的发送重复的请求会严重影响网络的性能,就算除去头部字段在频繁三次握手和四次挥手的情况下网络性能也非常堪忧。那么有没有一种办法能够让,Http的链接保持一段时间,如果有形同请求时复用这个链接,在超时的时候把链接断掉,从而减少握手次数呢?答案是肯定的,OkHttp3已经帮我们设计好了。
OkHttp3连接池原理:OkHttp3使用ConnectionPool连接池来复用链接,其原理是:当用户发起请求是,首先在链接池中检查是否有符合要求的链接(复用就在这里发生),如果有就用该链接发起网络请求,如果没有就创建一个链接发起请求。这种复用机制可以极大的减少网络延时并加快网络的请求和响应速度。
三、源码分析
我们主要看下ConnectionPool连接池的源代码,看其是怎样实现的,我们一段一段拆分着看。
private final int maxIdleConnections;//每个地址最大的空闲连接数 private final long keepAliveDurationNs;
private final Deque<RealConnection> connections = new ArrayDeque<>();//连接池,其是一个双端链表结果,支持在头尾插入元素,且是一个后进先出的队列
final RouteDatabase routeDatabase = new RouteDatabase();//用来记录链接失败的路由
boolean cleanupRunning;
private static final Executor executor = new ThreadPoolExecutor(0 /* 核心线程数 */, Integer.MAX_VALUE /*线程池可容纳的最大线程数量 */, 60L /* 线程池中的线程最大闲置时间 */, TimeUnit.SECONDS,/*闲置时间的单位*/ new SynchronousQueue<Runnable>()/*线程池中的任务队列,通过线程池的execute方法提交的runnable会放入这个队列中*/, Util.threadFactory("OkHttp ConnectionPool", true)
/*工具类用来创建线程的,其原型是ThreadFactory*/);
通过上面的代码可知,ConnectionPool中会创建一个线程池,这个线程池的作用就是为了清理掉闲置的链接(Socket)。ConnectionPool利用自身的put方法向连接池中添加链接(每一个RealConnection都是一个链接)
void put(RealConnection connection) {
//java1.4中新增的关键字,如果为true无异常,如果为false则抛出一个异常 assert (Thread.holdsLock(this));
//利用线程池清除空闲的Socket if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); }
//向链接池中加入链接 connections.add(connection); }
通过以上代码我们发现向线程池中添加一个链接(RealConnection)其实是向连接池connections添加RealConnection。并且在添加之前需要调用线程池的execute方法区清理闲置的链接。
下面我们看下清理动作是如何实现的,直接看cleanupRunnable这个匿名内部类
private final Runnable cleanupRunnable = new Runnable() { @Override public void run() {
//死循环,不停的执行cleanup的清理工作 while (true) {
//返回下次清理的时间间隔 long waitNanos = cleanup(System.nanoTime());
//如果返回-1就直接停止 if (waitNanos == -1) return;
//如果下次清理的时间几个大于0 if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try {
//根据下次返回的时间间隔来释放wait锁 ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } };
在Runnable的内部会不停的执行死循环,调用cleanup来清理空闲的链接,并返回一个下次清理的时间间隔,根据这个时间间隔来释放wait锁。
接下来看下cleanup的具体执行步骤
long cleanup(long now) { int inUseConnectionCount = 0;//正在使用的链接数量 int idleConnectionCount = 0;//闲置的链接数量
//长时间闲置的链接 RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // 用for循环来遍历连接池 synchronized (this) { for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // 如果当前链接正在使用,就执行continue,进入下一次循环. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } //否则闲置链接+1 idleConnectionCount++; // 如果闲置时间间隔大于最大的闲置时间,那就把当前的链接赋值给最大闲置时间的链接. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //如果最大闲置时间间隔大于保持链接的最大时间间隔或者限制连接数大于连接池允许的最大闲置连接数,就把该链接从连接池中移除 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // 如果闲置链接数大于0,则返回允许保持链接的最大时间间隔-最长时间间隔,也就是下次返回的时间间隔 return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // 如果所有的链接都在使用则直接返回保持时间间隔的最大值 return keepAliveDurationNs; } else { // 如果以上条件都不满足,则清除事变,返回-1 cleanupRunning = false; return -1; } } //关闭闲置时间最长的那个socket closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }
cleanup(now)这个方法比较长,内容也比较多。我们把握大体逻辑就行。其核心逻辑是返回下次清理的时间间隔,其清理的核心是:链接的限制时间如果大于用户设置的最大限制时间或者闲置链接的数量已经超出了用户设置的最大数量,则就执行清除操作。其下次清理的时间间隔有四个值:
1.如果闲置的连接数大于0就返回用户设置的允许限制的时间-闲置时间最长的那个连接的闲置时间。
2.如果清理失败就返回-1,
3.如果清理成功就返回0,
4.如果没有闲置的链接就直接返回用户设置的最大清理时间间隔。
下面看一下系统是如何判断当前循环到的链接是正在使用的链接
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//编译StreamAllocation弱引用链表 List<Reference<StreamAllocation>> references = connection.allocations; for (int i = 0; i < references.size(); ) { Reference<StreamAllocation> reference = references.get(i); //如果StreamAllocation不为空则继续遍历,计数器+1; if (reference.get() != null) { i++; continue; } // We've discovered a leaked allocation. This is an application bug. StreamAllocation.StreamAllocationReference streamAllocRef = (StreamAllocation.StreamAllocationReference) reference; //移除链表中为空的引用 references.remove(i); connection.noNewStreams = true; // If this was the last allocation, the connection is eligible for immediate eviction.
//如果链表为空则返回0 if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; } } return references.size(); }
通过以上的代码我们可以看出,其遍历了弱引用列表,链表中为空的引用,最后返回一个链表数量。如果返回的数量>0表示RealConnection活跃,如果<=0则表示RealConnection空闲。也就是用这个来方法来判断当前的链接是不是空闲的链接。
我们再来看一下
closeQuietly(longestIdleConnection.socket());是如何关闭空闲时间最长的链接的。
public static void closeQuietly(Socket socket) { if (socket != null) { try { socket.close(); } catch (AssertionError e) { if (!isAndroidGetsocknameError(e)) throw e; } catch (RuntimeException rethrown) { throw rethrown; } catch (Exception ignored) { } } }
其实就一行核心代码socket.close()。socket的使用不再介绍,大家可以看专门类的文章。
我们已经分析了从连接池清理空闲链接,到向连接池中加入新的链接。下面看看连接的使用以及连接的复用是如何实现的
@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; }
获取连接池中的链接的逻辑非常的简单,利用for循环循环遍历连接池查看是否有符合要求的链接,如果有则直接返回该链接使用,如果没有就发挥null,然后会在另外的地方创建一个新的RealConnection放入连接池。这里的核心代码就是判断是否有符合条件的链接:connection.isEligible(address,route)
public boolean isEligible(Address address, @Nullable Route route) { //如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回false if (allocations.size() >= allocationLimit || noNewStreams) return false; // 如果地址主机字段不一致直接返回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. } ...... return true; }
分析到这里,连接池已经分析完毕了,下面来总结一下
总结:
1.创建一个连接池
创建连接池非常简单只需要使用new关键字创建一个对象向就行了。new ConnectionPool(maxIdleConnections,keepAliveDuration,timeUnit)
参数说明:
1.maxIdleConnections 连接池允许的最大闲置连接数
2.keepAliveDuration 连接池允许的保持链接超时时间
3.timeUnit 保持链接超时的时间的时间单位
2.向连接池中添加一个连接
a.通过ConnectionPool的put(realConnection)方法加入链接,在加入链接之前会先调用线程池执行cleanupRunnable匿名内部类来清理空闲的链接,然后再把链接加入Deque队列中,
b.在cleanupRunnable匿名内部类中执行死循环不停的调用cleanup来清理空闲的连接,并返回一个下次清理的时间间隔,调用ConnectionPool.wait方法根据下次清理的时间间隔
c.在cleanup的内部会遍历connections连接池队列,移除空闲时间最长的连接并返回下次清理的时间。
d.判断连接是否空闲是利用RealConnection内部的List<Reference<StreamAllocation> 的size。如果size>0就说明不空闲,如果size<=0就说明空闲。
3.获取一个链接
通过ConnectionPool的get方法来获取符合要求的RealConnection。如果有服务要求的就返回RealConnection,并用该链接发起请求,如果没有符合要求的就返回null,并在外部重新创建一个RealConnection,然后再发起链接。判断条件:1.如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回false 2.如果地址主机字段不一致直接返回false3.如果主机地址完全匹配我们就重用该连接