
如果你把一个普通网页在开发者工具里的请求列表拉出来少则几十个、多则上百个资源请求这背后有一个经常被忽略却又极其关键的问题每一个资源都是用新的连接去取还是复用同一条连接这正是HTTP持续连接与非持续连接要回答的核心问题。作为搞过几年Web开发和网络性能排查的人我可以负责任地说理解了这两种连接模式的区别你才能真正看懂浏览器Network面板里那些Queueing、Stalled时间是从哪来的也才能想明白为什么HTTP/1.1要引入Keep-Alive、为什么会有队头阻塞、为什么HTTP/2和HTTP/3要彻底改造连接模型。这篇文章不罗列教科书定义而是把两种模式的交互逻辑、时间开销、实际观测方法和调优思路一次性讲透适合刚学计算机网络的学生也适合写接口、做前端、维护服务器的工程师参考。1. HTTP为什么把连接分成“持续”和“非持续”两种1.1 先搞清楚HTTP请求和TCP连接之间的关系很多人刚开始学时把HTTP和TCP搅在一起其实两者的关系很清晰HTTP是应用层协议负责定义请求和响应的格式TCP是传输层协议负责在客户端和服务端之间建立一条可靠的数据通道。HTTP请求要发出去必须先在这条TCP通道上跑。类比一下TCP连接是水管HTTP请求是水管里流的水。你可以打开水管放一杯水就关掉也可以一直开着接连放很多杯水。前者就是非持续连接后者就是持续连接。这里有个容易混淆的点一次HTTP请求只取一个资源。哪怕一个网页里有20个图片浏览器也得发20个HTTP请求。区别只在于这20个请求是分别用20条TCP连接去发还是复用同一条TCP连接去发。这个选择和HTTP协议版本、浏览器策略、服务器配置都有关系。1.2 两种模式的直观差异重新排队还是接着排队用食堂打饭来比喻最好理解。非持续连接模式你每打一个菜都要重新排一次队刷一次卡然后端着盘子走。下一个菜重新去队尾排着。HTTP/1.0默认就是这个模式每个请求都是独立的连接、独立的“排队”过程。持续连接模式你打完第一个菜之后不用走直接在同一窗口接着点下一个菜打完之后再点下一个直到你觉得够了或者食堂规定你不能再点了才离开。HTTP/1.1默认就是这种模式一个TCP连接上可以连续处理多个HTTP请求。这两种模式的差异听起来只是“省了一次排队”但在网络世界里一次“排队”意味着一次TCP三次握手可能要多花几十甚至几百毫秒。一个网页几十个资源累加起来差距就非常可观了。2. 非持续连接HTTP/1.0时代的“一次性买卖”2.1 非持续连接的完整交互过程非持续连接也叫短连接流程可以用四步概括客户端发起TCP三次握手建立连接。客户端在已建立的连接上发送HTTP请求报文。服务端处理请求通过同一条连接返回HTTP响应报文。响应接收完毕后连接关闭。每一步看起来都很自然但请注意第四条连接用一次就关。下一次请求任何资源哪怕这个资源和刚才那个在同一个服务器上也要重新执行这四步。补充一个细节HTTP/1.1虽然是默认持续连接但客户端可以在请求头里加Connection: close服务端也可以在响应头里加Connection: close双方都同意“发完这个就断开”那就是非持续连接。所以非持续连接并没有被淘汰它依然在最简单、最不适合复用的场景中存在。2.2 算一算非持续连接要多久2RTT起步网络性能里最常用的一个指标是RTT即往返时延指的是一个数据包从客户端出发到达服务端再返回客户端所花的时间。假设RTT是50毫秒这是一个比较接近跨省网络或者小型移动网络的数值。非持续连接传输一个很小的对象需要多长时间TCP三次握手1个RTT。客户端发SYN服务端回SYNACK客户端再回ACK这三个报文本质上需要完成一次完整的往返。请求和响应1个RTT。客户端发HTTP请求服务端收到后处理并返回响应响应第一个字节到达客户端。所以传输一个对象至少2个RTT。如果考虑响应体完整到达传输时间还要加上文件大小 / 带宽。但为了聚焦连接开销教科书上通常按2个RTT估算。现在看真实场景一个页面包含1个HTML文件加10个图片样式脚本共11个对象。非持续连接下每个对象都要经历“建连传输”也就是每个对象2个RTT。总共11 × 2 22个RTT。每个RTT 50毫秒总耗时大约22 × 50 1100毫秒。也就是说仅仅连接和请求响应的往返开销就超过了一秒钟。这还没有算DNS解析、服务器处理时间、带宽传输时间。这就是为什么HTTP/1.0时代打开一个很多图片的网页会觉得明显卡顿的原因之一。2.3 非持续连接真正的代价不止是慢除了慢非持续连接还有三个看不见的代价。第一个是服务器资源消耗。每条TCP连接都需要服务器分配文件描述符、socket缓冲区等资源。大量短连接频繁建立和释放会让服务器CPU忙于处理握手包真正用于业务处理的能力被挤占。第二个是TIME_WAIT问题。TCP连接在主动关闭的一方会进入TIME_WAIT状态并且要等待2MSL最大报文段生存时间通常约2到4分钟才会彻底消失。如果服务器每处理一个请求就主动关闭连接大量连接堆积在TIME_WAIT状态会导致连接表被占满新连接无法建立。第三个是移动弱网场景下的加倍损耗。在Wi-Fi环境下50毫秒RTT可能觉得还好但在弱网环境RTT可能飙升到几百毫秒。非持续连接下每次请求都要付出完整握手代价用户体验会断崖式下降。3. 持续连接HTTP/1.1如何把“重复握手”省掉3.1 Keep-Alive在HTTP层面是怎么工作的持续连接在HTTP/1.1中成为默认行为也就是说只要双方没有明确说要关闭TCP连接会保持下去供后续请求复用。它的实现机制就是Keep-Alive。工作流程是这样的第一次请求正常经过TCP三次握手。请求头和响应头里出现Connection: keep-alive双方都知道这个连接不要急着关。后续请求可以直接复用这条TCP连接不需要再握手。连接会在空闲一段时间后由服务器或者客户端主动关闭也可以通过Connection: close显式关闭。这里要分清两层Keep-Alive。HTTP层的Keep-Alive是复用连接传输多个请求。TCP层的Keep-Alive则是一个探测机制系统会定时发送探测包确认连接是否仍然有效。两者名字相似职责完全不同排查问题时别搞混了。3.2 持续连接到底快多少一次完整计算还是用刚才的例子11个对象RTT为50毫秒。持续连接下第一个对象仍然需要2个RTT建连加请求响应。但后面10个对象因为连接已经建立每个只需要1个RTT就能完成请求和响应。总耗时大约2 10 12个RTT也就是12 × 50 600毫秒。和非持续连接的1100毫秒对比节省了接近一半。如果页面有100个资源那差距会更大非持续连接200个RTT持续连接101个RTT接近一倍的差距。节省的本质是TCP三次握手的1个RTT被摊薄到后续N个请求上。连接建立一次收益很多次。提示这里说的是理论理想情况。实际运行中TCP慢启动机制会让新建连接初期的传输速率较低持续连接因为已经经历过慢启动速率也更容易达到稳定值。所以持续连接带来的收益往往比理论计算的还要多。3.3 持续连接的边界超时、上限和Connection头持续连接不是“永远持续”它有边界。服务器一般会配置空闲超时时间比如连接空闲10秒、30秒或60秒后关闭。这样做是防止长期不用的连接占用服务器资源。一些Web服务器的响应头里会带Keep-Alive: timeout5, max100意思是这条连接最多空闲5秒、最多处理100个请求。连接还有一个处理请求次数的上限。即使是持续连接也不允许无限请求下去因为长时间运行的连接可能出现内存碎片、中间设备状态过期等问题。达到次数上限后服务端会主动关闭。这里有一个知识点很多人忽略HTTP/1.1的请求和响应是严格按顺序的。在同一条持续连接上必须发一个请求、收一个响应然后才能发下一个请求不能跳着来也不能并行。这个“一进一出”的顺序约束成为了后面队头阻塞问题的根源。4. 持续连接下的新瓶颈队头阻塞与连接并发4.1 流水线Pipelining为什么没能普及既然持续连接上请求必须一个一个来那能不能一次把多个请求全发出去再统一接收响应HTTP/1.1确实设计了流水线机制客户端不用等上一个响应可以直接连续发送多个请求。理论上这可以大幅减少等待时间。但实际落地有问题。第一服务端必须严格按照请求顺序返回响应。如果第一个请求是慢接口要处理5秒钟那排它后面的所有响应都得等它完成才能开始返回。这种情况就是典型的队头阻塞一个慢请求堵住了后面所有请求。第二中间设备和服务器对流水线的支持非常混乱。有些代理服务器不支持流水线或者处理方式不符合规范导致请求错乱、连接重置。第三改造成本高。浏览器厂商需要处理各种兼容性问题而收益却不一定保证。最终结果就是流水线在实践层面被废弃了。浏览器默认关闭流水线开发者仍然只能用“并发连接”来缓解性能问题。4.2 浏览器“6个连接上限”是怎么回事浏览器为了在持续连接模式下还能有点并发能力会对同一域名下的TCP连接数量做限制。主流浏览器的典型限制是6条左右也就是同一时间最多有6个HTTP请求在同一条域名连接池里并行执行。看起来6条连接也不少了但网页资源动辄几十上百个。当6条连接都在传输资源时第7个请求只能排队等待空闲连接。这其实是一种变相的队头阻塞一个资源慢就会拖住一条连接而这条连接上后面排队的资源只能等着。所以HTTP/1.1时代的优化手段就是减少请求数量合并CSS、合并JS、图片雪碧图、内联小资源。这些做法的本质都是在绕开连接数和队头阻塞的限制。4.3 从Keep-Alive到多路复用HTTP/2带来的变化HTTP/2针对队头阻塞给出的方案是多路复用。在HTTP/2中同一条TCP连接上可以同时存在多个“流”每个流承载一个HTTP请求和响应。数据被拆分成二进制帧帧可以在多个流之间交错发送到对端再按流重组。这就解决了HTTP层的队头阻塞一个慢请求只影响它自己的流不会堵住其他流。连接复用的效率也更高通常一条连接就能满足整个页面的资源请求浏览器不再需要开6条连接。但HTTP/2没有解决TCP层的队头阻塞。因为TCP本身是按序传输的如果底层网络丢包TCP要重传并保证顺序排在后面的所有数据都得等。所以一条连接在丢包率较高的网络环境下整体吞吐还是会受影响。HTTP/3就是为了解决这个问题把传输层换成基于UDP的QUIC协议实现了无序交付和连接迁移。当然HTTP/3已经超出了“持续连接与非持续连接”的经典讨论范围但它解决的问题正是从连接复用这条线一直演化过来的。5. 实操在浏览器和服务器视角观察HTTP连接5.1 浏览器开发者工具怎么看连接复用打开浏览器的开发者工具切到Network面板默认能看到Name、Status、Size、Time等列。要观察连接状态需要把Connection ID列显示出来。操作方式很简单在Network面板的列标题上右键勾选Connection ID项。刷新页面后就能看到每个请求对应一条数字ID。如果多个不同请求的Connection ID是同一个数字说明它们复用了同一条TCP连接。这是持续连接生效的直接证据。如果每个请求的Connection ID都不同那就说明连接没有被复用一定有什么地方在关闭连接或者禁止复用。同时可以看看请求头里的Connection字段显示keep-alive说明客户端希望复用如果显示close说明这个请求是“一次性”的。另外观察请求瀑布图也很有用。Timeline里持续连接下的请求是一个接一个的“竖条”第二个请求的开始时间紧跟着第一个请求的结束时间看不到什么建连过程。非持续连接下每个请求前面会多出一段“等待建立连接”的空白段。5.2 服务器端的连接参数怎么调以常见的开源Web服务器为例连接相关配置里最核心的两个参数是空闲超时时间和单连接最大请求数。空闲超时时间建议设置在10到30秒之间。如果站点是图片静态资源为主的页面用户会在短时间内大量请求资源空闲超时设长一点有助于复用。如果是API服务器请求分散且每个请求间隔时间较长长时间保持空闲连接反而是浪费可以把空闲超时调短比如3到5秒。单连接最大请求数这块默认值往往是100或者1000。对于静态资源站点来说偏小因为一个页面几十个资源一次访问可能就消耗掉几十个请求配额下个页面又要重新建连。可以适当调高比如调到1000甚至10000。但也要注意大得太离谱会让单条连接存活过久内存占用和异常状态积累的风险都会增加。注意这些参数的作用原理在主流Web服务器中是相通的但具体配置项名称和语法各有不同。动手之前先查一下所用软件的文档别把不同软件的写法生搬硬套。5.3 连接复用率与TTFB两个值得盯的性能指标排查连接问题时我会重点盯两个数据。连接复用率的大致算法是看Network面板中有多少请求的Connection ID与被复用的ID相同。更精确的统计可以靠监控服务端连接建立频率如果每秒新建连接数非常高说明Keep-Alive没有奏效如果新建连接数远低于请求数说明复用是有效的。TTFB也就是Time To First Byte表示从发起请求到收到响应第一个字节的时间。这个时间包含了DNS、TCP建连、发送请求、服务器处理等环节。如果TTFB偏高同时观察到每个请求前都有建连操作问题多半出在连接复用。当然TTFB高也可能是服务端业务处理慢需要配合服务端日志和监控进一步区分。6. 常见问题与排查技巧实录6.1 页面加载慢先确认是不是连接没复用上我遇到过一个典型的案例一个业务系统页面静态资源不多但每次打开都要好几秒。当时大家怀疑是接口慢各种优化都没效果。后来我打开Network面板一看发现几十个请求的Connection ID全都不一样每条请求都在重新建连。往下查才发现服务器配置里写明了Connection: close任何一个请求过来响应完就断。浏览器不得不为每个资源重新走一次TCP握手延迟自然就上去了。排查此类问题最快的办法就是按5.1节的方法看Connection ID。确认是连接没复用时优先检查三个位置服务器是否配置了关闭Keep-Alive反向代理或网关层有没有改写Connection头客户端请求里是否强制带了Connection: close。6.2 TIME_WAIT和CLOSE_WAIT别傻傻分不清在服务器上执行连接状态统计经常能看到TIME_WAIT和CLOSE_WAIT这两个状态它们含义完全不同。TIME_WAIT是主动关闭连接的一方在发送最后一个ACK后进入的状态。它存在的意义是防止迟到的数据包干扰新连接。如果服务器TIME_WAIT数量特别多通常是服务器频繁主动关闭短连接属于非持续连接或连接复用失效的典型症状。此时优先调整连接超时和Keep-Alive配置让连接尽量复用而不是急着去调内核参数强制回收TIME_WAIT。CLOSE_WAIT是收到对方关闭请求后本地还没有关闭连接时进入的状态。如果服务器上大量连接堆在CLOSE_WAIT说明应用程序没有正确执行关闭操作多半是代码里socket或连接对象没有正确释放。这种问题调内核参数没有用要去查业务代码的连接生命周期管理。6.3 我反复踩过的三个HTTP连接管理的坑第一个坑是反向代理层破坏了长连接。应用服务器明明支持Keep-Alive但客户端到反向代理的请求却被代理层重写了头部长连接在代理层就断了。排查时要在浏览器的响应头里看清是谁返回的Connection: close。第二个坑是心跳与超时设置不匹配。有些连接池会定期发心跳保活但服务器端的空闲超时时间比心跳间隔还短结果连接在两次心跳之间就被服务器关了。客户端自己却不知道继续往已失效的连接上发请求等来的就是一个完整的建连重试。调这个问题的思路很简单服务器空闲超时务必大于客户端的空闲检测或心跳周期。第三个坑是静态资源域名拆得太碎。为了提升并发有人把资源分散到多个子域名这在HTTP/1.1时代是合理优化。但在HTTP/2时代反而有害因为不同域名之间无法复用多路复用连接浏览器反而要为每个域名维护连接状态。域名收敛、连接合并才是更符合当前协议特性的做法。从这些坑里走一圈之后我的体会是HTTP连接管理从来不是“开开关关”这么简单它是一场权衡复用多了连接占着资源不做事复用少了握手开销拖慢一切。理解持续连接与非持续连接说到底就是在理解这套权衡的边界在哪里。能把浏览器Network面板里的每一段等待时间解释清楚能把服务器上每一个异常连接状态定位到具体配置或代码你对HTTP的理解就真正过关了。