OkHttp3源码分析[复用连接池]

2024-09-06 01:18

本文主要是介绍OkHttp3源码分析[复用连接池],希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OkHttp系列文章如下

  • OkHttp3源码分析[综述]
  • OkHttp3源码分析[复用连接池]
  • OkHttp3源码分析[缓存策略]
  • OkHttp3源码分析[DiskLruCache]
  • OkHttp3源码分析[任务队列]

1. 概述

HTTP中的keepalive连接在网络性能优化中,对于延迟降低与速度提升的有非常重要的作用。

通常我们进行http连接时,首先进行tcp握手,然后传输数据,最后释放


图源: Nginx closed

这种方法的确简单,但是在复杂的网络内容中就不够用了,创建socket需要进行3次握手,而释放socket需要2次握手(或者是4次)。重复的连接与释放tcp连接就像每次仅仅挤1mm的牙膏就合上牙膏盖子接着再打开接着挤一样。而每次连接大概是TTL一次的时间(也就是ping一次),甚至在TLS环境下消耗的时间就更多了。很明显,当访问复杂网络时,延时(而不是带宽)将成为非常重要的因素。

当然,上面的问题早已经解决了,在http中有一种叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手


图源: Nginx keep_alive

在现代浏览器中,一般同时开启6~8个keepalive connections的socket连接,并保持一定的链路生命,当不需要时再关闭;而在服务器中,一般是由软件根据负载情况进行配置。

Okhttp支持5个并发,默认链路生命为5分钟(链路空闲后,保持存活的时间)

当然keepalive也有缺点,在提高了单个客户端性能的同时,复用却阻碍了其他客户端的链路速度,具体来说如下

  1. 根据TCP的拥塞机制,当总水管大小固定时,如果存在大量空闲的keepalive connections(我们可以称作僵尸连接或者泄漏连接),其它客户端们的正常连接速度也会受到影响
  2. 服务器/防火墙上有并发限制,比如apache服务器只支持150个并发连接(数据来源于nginx官网),不过这个瓶颈随着高并发server软硬件的发展(golang/分布式)将会越来越少
  3. 大量的DDOS产生的僵尸连接可能被用于恶意攻击服务器,耗尽资源

好了,以上科普完毕,本文主要是写客户端的,服务端不再介绍。

下文假设服务器是经过专业的运维配置好的,它默认开启了keep-alive,并不主动关闭连接

2. 连接池的使用与分析

首先先说下源码中关键的对象:

  • Call: 对http的请求封装,属于上层高级代码
  • Connection: 对jdk的socket物理连接的包装,它内部有List<WeakReference<StreamAllocation>>的引用
  • StreamAllocation: 表示Connection被上层高级代码的引用次数
  • ConnectionPool: Socket连接池,对连接缓存进行回收与管理
  • Deque: Deque也就是双端队列,双端队列同时具有队列和栈性质,经常在缓存中被使用,这个是java基础

在okhttp中,连接池对用户,甚至开发者都是透明的。它自动创建连接池,自动进行泄漏连接回收,自动帮你管理线程池,提供了put/get/clear的接口,甚至调用都帮你写好了。

在以前的内存泄露分析文章中我写到,我们知道在socket连接中,也就是Connection中,本质是封装好的流操作,除非手动close,基本不会被gc掉,非常容易引发内存泄露。所以当涉及到并发socket编程时,我们就会非常紧张,往往写出来的代码都是try/catch/finally的迷之缩进,却又对这样的代码无可奈何。

在okhttp中,在高层代码的调用中,使用了类似于引用计数的方式跟踪流的调用,这里的计数对象是StreamAllocation,它被反复执行aquirerelease操作(点击函数可以进入github查看),这两个函数其实是在改变Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的数量也就是物理socket被引用的计数(Refference Count),如果计数为0的话,说明此连接没有被使用,是空闲的,需要通过下文的算法实现回收;如果上层代码仍然引用,就不需要关闭连接。

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用。它不能处理循环引用的问题。

2.1. 实例化

在源码中,我们先找ConnectionPool实例化的位置,它是直接new出来的,而它的各种操作却在OkHttpClient的static区实现了Internal.instance接口作为ConnectionPool的包装。

至于为什么需要这么多此一举的分层包装,主要是为了让外部包的成员访问非public方法,详见这里注释

2.2. 构造
  1. 连接池内部维护了一个叫做OkHttp ConnectionPoolThreadPool,专门用来淘汰末位的socket,当满足以下条件时,就会进行末位淘汰,非常像GC

     1. 并发socket空闲连接超过52. 某个socket的keepalive时间大于5分钟
  2. 维护着一个Deque<Connection>,提供get/put/remove等数据结构的功能

  3. 维护着一个RouteDatabase,它用来记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去(本文不讨论)

2.3 put/get操作

在连接池中,提供如下的操作,这里可以看成是对deque的一个简单的包装

//从连接池中获取
get
//放入连接池
put
//线程变成空闲,并调用清理线程池
connectionBecameIdle
//关闭所有连接
evictAll

随着上述操作被更高级的对象调用,Connection中的StreamAllocation被不断的aquirerelease,也就是List<WeakReference<StreamAllocation>>的大小将时刻变化

2.4 Connection自动回收的实现

java内部有垃圾回收GC,okhttp有socket回收SocketClean;垃圾回收是根据对象的引用树实现的,而okhttp是根据RealConnection的虚引用StreamAllocation引用计数是否为0实现的。

cleanupRunnable:

当用户socket连接成功,向连接池中put新的socket时,回收函数会被主动调用,线程池就会执行cleanupRunnable,如下

//Socket清理的Runnable,每当put操作时,就会被调用
//注意put操作是在网络线程
//而Socket清理是在`OkHttp ConnectionPool`线程池中调用
while (true) {//执行清理并返回下场需要清理的时间long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {synchronized (ConnectionPool.this) {try {//在timeout内释放锁与时间片ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));} catch (InterruptedException ignored) {}}}
}

这段死循环实际上是一个阻塞的清理任务,首先进行清理(clean),并返回下次需要清理的间隔时间,然后调用wait(timeout)进行等待以释放锁与时间片,当等待时间到了后,再次进行清理,并返回下次要清理的间隔时间...

Cleanup:

cleanup使用了类似于GC的标记-清除算法,也就是首先标记出最不活跃的连接(我们可以叫做泄漏连接,或者空闲连接),接着进行清除,流程如下:

long cleanup(long now) {int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;//遍历`Deque`中所有的`RealConnection`,标记泄漏的连接synchronized (this) {for (RealConnection connection : connections) {// 查询此连接内部StreamAllocation的引用数量if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}idleConnectionCount++;//选择排序法,标记出空闲连接long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {//如果(`空闲socket连接超过5个`//且`keepalive时间大于5分钟`)//就将此泄漏连接从`Deque`中移除connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {//返回此连接即将到期的时间,供下次清理//这里依据是在上文`connectionBecameIdle`中设定的计时return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {//全部都是活跃的连接,5分钟后再次清理return keepAliveDurationNs;} else {//没有任何连接,跳出循环cleanupRunning = false;return -1;}}//关闭连接,返回`0`,也就是立刻再次清理closeQuietly(longestIdleConnection.socket());return 0;
}
  1. 遍历Deque中所有的RealConnection,标记泄漏的连接
  2. 如果被标记的连接(空闲socket连接超过5个&&keepalive时间大于5分钟),就将此泄漏连接从Deque中移除,并关闭连接,返回0,也就是将要执行wait(0),提醒立刻再次扫描
  3. 如果(目前还可以塞得下5个连接,但是有可能泄漏的连接(即空闲时间即将达到5分钟)),就返回此连接即将到期的时间,供下次清理
  4. 如果(全部都是活跃的连接),就返回默认的keep-alive时间,也就是5分钟后再执行清理
  5. 如果(没有任何连接),就返回-1,跳出清理的死循环

再次注意:这里的“并发”==(“空闲”+“活跃”)==5,而不是说并发连接就一定是活跃的连接

pruneAndGetAllocationCount:

如何找到最不活跃的连接呢,这里使用了pruneAndGetAllocationCount的方法,它主要依据弱引用是否为null而判断这个连接是否泄漏

//类似于引用计数法,如果引用全部为空,返回立刻清理
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);//如果正在被使用,跳过,接着循环//是否置空是在上文`connectionBecameIdle`的`release`手动控制的if (reference.get() != null) {//非常明显的引用计数i++;continue;}//否则移除引用references.remove(i);connection.noNewStreams = true;//如果所有分配的流均没了,标记为已经距离现在空闲了5分钟if (references.isEmpty()) {connection.idleAtNanos = now - keepAliveDurationNs;return 0;}}return references.size();
}
  1. 遍历RealConnection连接中的StreamAllocationList,它维护着一个弱应用列表
  2. 查看此StreamAllocation是否为空(它是在线程池的put/remove手动控制的),如果为空,说明已经没有代码引用这个对象了,需要在List中删除
  3. 遍历结束,如果List中维护的StreamAllocation删空了,就返回0,表示这个连接已经没有代码引用了,是泄漏的连接;否则返回非0的值,表示这个仍然被引用,是活跃的连接。

总结

通过上面的分析,我们可以总结,okhttp使用了类似于引用计数法与标记擦除法的混合使用,当连接空闲或者释放时,StreamAllocation的数量会渐渐变成0,从而被线程池监测到并回收,这样就可以保持多个健康的keep-alive连接,Okhttp的黑科技就是这样实现的。

如果你期待更多高质量的文章,不妨关注我或者点赞吧!

Ref

  1. https://www.nginx.com/blog/http-keepalives-and-web-performance


文/BlackSwift(简书作者)
原文链接:http://www.jianshu.com/p/92a61357164b
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

这篇关于OkHttp3源码分析[复用连接池]的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140583

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。