OkHttp3连接池复用原理分析

时间:2025-01-14 14:44:17

OkHttp3连接池原理:OkHttp3使用ConnectionPool连接池来复用链接,其原理是:当用户发起请求是,首先在链接池中检查是否有符合要求的链接(复用就在这里发生),如果有就用该链接发起网络请求,如果没有就创建一个链接发起请求。这种复用机制可以极大的减少网络延时并加快网络的请求和响应速度。

源码分析

   // 最多保存 5个 处于空闲状态的连接,连接的默认保活时间为 5分钟
    public ConnectionPool() {
        this(5, 5, );
    }

    public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
         = maxIdleConnections;
         = (keepAliveDuration);

        if (keepAliveDuration <= 0) {
            throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
        }
    }

 

//连接池,其是一个双端链表结果,支持在头尾插入元素,且是一个后进先出的队列
    private static final Executor executor = new ThreadPoolExecutor(0  /* 核心线程数 */,
            Integer.MAX_VALUE /* 线程池可容纳的最大线程数量 */, 60L /* 线程池中的线程最大闲置时间 */, ,
            /* 闲置时间的单位 */
            new SynchronousQueue<Runnable>()
            /*线程池中的任务队列,通过线程池的execute方法提交的runnable会放入这个队列中*/
            , ("OkHttp ConnectionPool", true));

    //每个地址最大的空闲连接数
    private final int maxIdleConnections;
    private final long keepAliveDurationNs;

 ConnectionPool中会创建一个线程池,这个线程池的作用就是为了清理掉闲置的链接(Socket)。ConnectionPool利用自身的put方法向连接池中添加链接(每一个RealConnection都是一个链接)

 

    /**
    保存连接以复用*/
    void put(RealConnection connection) {
        assert ((this));
     
        if (!cleanupRunning) {
            cleanupRunning = true;
            (cleanupRunnable);
        }
        (connection);
    }

向线程池中添加一个链接(RealConnection)其实是向连接池connections添加RealConnection。并且在添加之前需要调用线程池的execute方法区清理闲置的链接。并且在添加之前需要调用线程池的execute方法区清理闲置的链接 ,在来看cleanup 如何做清理闲置的链接

 private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                // 最快多久后需要再清理
                long waitNanos = cleanup(());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                  
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized () {
                        try { 
                            //根据下次返回的时间间隔来释放wait锁  参数多一个纳秒,制更加精准 
                            (waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

这里面具体就是GC回收算法,类似于标记清除算法,顾名思义,就是先标记处最不活跃的连接,然后清除。

 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 = (); (); ) {
                RealConnection connection = ();

                //检查连接是否正在被使用
                
                if (pruneAndGetAllocationCount(connection, now) > 0) {
                    inUseConnectionCount++;
                    continue;
                }
                //否则记录闲置连接数
                idleConnectionCount++;

    
                //获得这个连接已经闲置多久
                long idleDurationNs = now - ;
                if (idleDurationNs > longestIdleDurationNs) {
                    longestIdleDurationNs = idleDurationNs;
                    longestIdleConnection = connection;
                }
            }
            // 超过了保活时间(5分钟) 或者池内数量超过了(5个) 马上移除,然后返回0,表示不等待,马上再次检查清理
            if (longestIdleDurationNs >= 
                    || idleConnectionCount > ) {
       
                (longestIdleConnection);
            } else if (idleConnectionCount > 0) {
                // 
                //池内存在闲置连接,就等待 保活时间(5分钟)-最长闲置时间 =还能闲置多久 再检查
                return keepAliveDurationNs - longestIdleDurationNs;
            } else if (inUseConnectionCount > 0) {
              
                //有使用中的连接,就等 5分钟 再检查
                return keepAliveDurationNs;
            } else {
                .
                //都不满足,可能池内没任何连接,直接停止清理(put后会再次启动)
                cleanupRunning = false;
                return -1;
            }
        }
         //关闭闲置时间最长的那个socket
        closeQuietly(());
        return 0;
    }

cleanup主要逻辑是 

链接的限制时间如果大于用户设置的最大限制时间或者闲置链接的数量已经超出了用户设置的最大数量,则就执行清除操作。其下次清理的时间间隔有四个值:

1.如果闲置的连接数大于0就返回用户设置的允许限制的时间-闲置时间最长的那个连接的闲置时间。

2.如果清理失败就返回-1,

3.如果清理成功就返回0,

4.如果没有闲置的链接就直接返回用户设置的最大清理时间间隔。

 

现在看pruneAndGetAllocationCount是如何判断当前循环到的链接是正在使用的链接

 

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
        // 这个连接被使用就会创建一个弱引用放入集合,这个集合不为空就表示这个连接正在被使用
    
        List<Reference<StreamAllocation>> references = ;
        for (int i = 0; i < (); ) {
            Reference<StreamAllocation> reference = (i);
            if (() != null) {
                i++;
                continue;
            }

         
             streamAllocRef =
                    () reference;
            String message = "A connection to " + ().address().url()
                    + " was leaked. Did you forget to close a response body?";
            ().logCloseableLeak(message, );

            (i);
             = true;

            if (()) {
                 = now - keepAliveDurationNs;
                return 0;
            }
        }

        return ();
    }

 通过返回的 references的数量>0表示RealConnection活跃,如果<=0则表示RealConnection空闲。也就是用这个来方法来判断当前的链接是不是空闲的链接。

 

下面看看连接的使用以及连接的复用是如何实现的

  RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert ((this));
        for (RealConnection connection : connections) {
            // 要拿到的连接与连接池中的连接  连接的配置(dns/代理/域名等等)一致 就可以复用
    
            if ((address, route)) {
                (connection, true);
                return connection;
            }
        }
        return null;
    }

获取连接池中的链接的逻辑非常的简单,利用for循环循环遍历连接池查看是否有符合要求的链接,如果有则直接返回该链接使用.判断是否有符合条件的链接:(address,route)

 

public boolean isEligible(Address address, @Nullable Route route) {
    //1、负载超过指定最大负载,不可复用 
    if (() >= allocationLimit || noNewStreams) return false;

    //2、Address对象的非主机部分不相等,不可复用 
    if (!((), address)) return false;

    //3、非主机部分不相等,不可复用 
 if(().host().equals(().address().url().host())) {
     //这个链接完美的匹配
      return true; // This connection is a perfect match.
    }

   

    // 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 (().type() != ) return false;
    //7
    if (().type() != ) return false;
    //8
    if (!().equals(())) return false;

    // 9
    if (().hostnameVerifier() != ) return false;
    if (!supportsUrl(())) return false;

    // 10. Certificate pinning must match the host.
    try {
      ().check(().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }
    //最终可以复用
    return true;
  }

连接池已经分析完毕了,下面来总结一下

1.创建一个连接池

创建连接池非常简单只需要使用new关键字创建一个对象向就行了。new ConnectionPool(maxIdleConnections,keepAliveDuration,timeUnit)

2.向连接池中添加一个连接

a.通过ConnectionPool的put(realConnection)方法加入链接,在加入链接之前会先调用线程池执行cleanupRunnable匿名内部类来清理空闲的链接,然后再把链接加入Deque队列中,

b.在cleanupRunnable匿名内部类中执行死循环不停的调用cleanup来清理空闲的连接,并返回一个下次清理的时间间隔,调用方法根据下次清理的时间间隔

c.在cleanup的内部会遍历connections连接池队列,移除空闲时间最长的连接并返回下次清理的时间。

d.判断连接是否空闲是利用RealConnection内部的List<Reference<StreamAllocation> 的size。如果size>0就说明不空闲,如果size<=0就说明空闲。

3.获取一个链接

通过ConnectionPool的get方法来获取符合要求的RealConnection。如果有服务要求的就返回RealConnection,并用该链接发起请求,如果没有符合要求的就返回null,并在外部重新创建一个RealConnection,然后再发起链接。判断条件:1.如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回false 2.如果地址主机字段不一致直接返回false3.如果主机地址完全匹配我们就重用该连接

 

我们知道okhttp是可以通过连接池来减少请求延时的,那么这一点是怎么实现的呢?