OkHttp3连接池复用原理分析

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

源码分析

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

    public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
        this.maxIdleConnections = maxIdleConnections;
        this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

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

 

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

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

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

 

    /**
    保存连接以复用*/
    void put(RealConnection connection) {
        assert (Thread.holdsLock(this));
     
        if (!cleanupRunning) {
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }

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

 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 { 
                            //根据下次返回的时间间隔来释放wait锁  参数多一个纳秒,制更加精准 
                            ConnectionPool.this.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 = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();

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

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

cleanup主要逻辑是 

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

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

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

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

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

 

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

 

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

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

            references.remove(i);
            connection.noNewStreams = true;

            if (references.isEmpty()) {
                connection.idleAtNanos = now - keepAliveDurationNs;
                return 0;
            }
        }

        return references.size();
    }

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

 

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

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

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

 

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.
    }

   

    // 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.创建一个连接池

    创建连接池非常简单只需要使用new关键字创建一个对象向就行了。new ConnectionPool(maxIdleConnections,keepAliveDuration,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.如果主机地址完全匹配我们就重用该连接

 

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

相关文章: okhttp连接池复用机制

提高网络性能优化,很重要的一点就是降低延迟和提升响应速度。

通常我们在浏览器中发起请求的时候header部分往往是这样的

keep-alive 就是浏览器和服务端之间保持长连接,这个连接是可以复用的。在HTTP1.1中是默认开启的。

连接的复用为什么会提高性能呢? 
通常我们在发起http请求的时候首先要完成tcp的三次握手,然后传输数据,最后再释放连接。三次握手的过程可以参考这里 TCP三次握手详解及释放连接过程

一次响应的过程

在高并发的请求连接情况下或者同个客户端多次频繁的请求操作,无限制的创建会导致性能低下。

如果使用keep-alive

timeout空闲时间内,连接不会关闭,相同重复的request将复用原先的connection,减少握手的次数,大幅提高效率。

并非keep-alive的timeout设置时间越长,就越能提升性能。长久不关闭会造成过多的僵尸连接和泄露连接出现。

那么okttp在客户端是如果类似于浏览器客户端做到的keep-alive的机制呢?

在BridgeInterceptor的intercept()方法中可以看到:
 

 
  1. if (userRequest.header("Connection") == null) {

  2. requestBuilder.header("Connection", "Keep-Alive");

  3. }

  4.  
  5. public Builder header(String name, String value) {

  6. headers.set(name, value);

  7. return this;

  8. }

可以看到我们在request的请求头添加了("Connection", "Keep-Alive")的键值对。

 

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值