HTTP持久连接

聊一聊TCP/IP下的HTTP持久连接方案

Posted by Jeremy Song on 2021-08-03
Estimated Reading Time 18 Minutes
Words 5.2k In Total
Viewed Times

持久连接

Web客户端经常会打开到同一个站点的连接。比如,一个Web页面上的大部分内嵌图片通常都来自同一个Web站点,而且相当一部分指向其他对象的超链通常都指向同一个站点。因此,初始化了对某服务器HTTP请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求(比如,获取在线图片)。这种性质被称为站点局部性(site locality)。

因此,HTTP/1.1(以及HTTP/1.0的各种增强版本)允许HTTP设备在事务处理结束之后将TCP连接保持在打开状态,以便为未来的HTTP请求重用现存的连接。在事务处理结束之后仍然保持在打开状态的TCP连接被称为持久连接。非持久连接会在每个事务结束之后关闭。持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。

重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据的传输。

持久以及并行连接

并行连接可以提高复合页面的传输速度。但并行连接也有一些缺点。

  • 每个事务都会打开/关闭一条新的连接,会耗费时间和带宽。
  • 由于TCP慢启动特性的存在,每条新连接的性能都会有所降低。
  • 可打开的并行连接数量实际上是有限的。

持久连接有一些比并行连接更好的地方。持久连接降低了时延和连接建立的开销,将连接保持在已调谐状态,而且减少了打开连接的潜在数量。但是,管理持久连接时要特别小心,不然就会累积出大量的空闲连接,耗费本地以及远程客户端和服务器上的资源。

持久连接与并行连接配合使用可能是最高效的方式。现在,很多Web应用程序都会打开少量的并行连接,其中的每一个都是持久连接。持久连接有两种类型:比较老的HTTP/1.0+“keep-alive”连接,以及现代的HTTP/1.1“persistent”连接。在接下来的几节中我们将对这两种类型进行介绍。

HTTP/1.0+ keep-alive连接

大约从1996年开始,很多HTTP/1.0浏览器和服务器都进行了扩展,以支持一种被称为keep-alive连接的早期实验型持久连接。这些早期的持久连接受到了一些互操作性设计方面问题的困扰,这些问题在后期的HTTP/1.1版本中都得到了修正,但很多客户端和服务器仍然在使用这些早期的keep-alive连接。

图1显示了keep-alive连接的一些性能优点,图中将在串行连接上实现4个HTTP事务的时间线与在一条持久连接上实现同样事务所需的时间线进行了比较。由于去除了进行连接和关闭连接的开销,所以时间线有所缩减。
图1 四个事务 (串行与持久连接)

Keep-Alive操作

keep-alive已经不再使用了,而且在当前的HTTP/1.1规范中也没有对它的说明了。但浏览器和服务器对keep-alive握手的使用仍然相当广泛,因此,HTTP的实现者应该做好与之进行交互操作的准备。现在我们来快速浏览一下keep-alive的操作。对keep-alive握手更详细的解释请参见较早的HTTP/1.1规范版本(比如RFC 2068)。

实现HTTP/1.0 keep-alive连接的客户端可以通过包含Connection: Keep-Alive首部请求将一条连接保持在打开状态。

如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部(参见图2)。如果响应中没有Connection: Keep-Alive首部,客户端就认为服务器不支持keep-alive,会在发回响应报文之后关闭连接。
图2 HTTP/1.0 keep-alive事务首部的握手过程

Keep-Alive选项

注意,keep-Alive首部只是请求将连接保持在活跃状态。发出keep-alive请求之后,客户端和服务器并不一定会同意进行keep-alive会话。它们可以在任意时刻关闭空闲的keep-alive连接,并可随意限制keep-alive连接所处理事务的数量。

可以用Keep-Alive通用首部中指定的、由逗号分隔的选项来调节keep-alive的行为。

  • 参数timeout是在Keep-Alive响应首部发送的。它估计了服务器希望将连接保持在活跃状态的时间。这并不是一个承诺值。
  • 参数max是在Keep-Alive响应首部发送的。它估计了服务器还希望为多少个事务保持此连接的活跃状态。这并不是一个承诺值。
  • Keep-Alive首部还可支持任意未经处理的属性,这些属性主要用于诊断和调试。语法为 name[=value]

Keep-Alive首部完全是可选的,但只有在提供Connection: Keep-Alive时才能使用它。这里有个Keep-Alive响应首部的例子,这个例子说明服务器最多还会为另外5个事务保持连接的打开状态,或者将打开状态保持到连接空闲了2分钟之后。

1
2
Connection: Keep-Alive
Keep-Alive: max=5, timeout=120

Keep-Alive连接的限制和规则

使用keep-alive连接时有一些限制和一些需要澄清的地方。

  • 在HTTP/1.0中,keep-alive并不是默认使用的。客户端必须发送一个Connection: Keep-Alive请求首部来激活keep-alive连接。
  • Connection: Keep-Alive首部必须随所有希望保持持久连接的报文一起发送。如果客户端没有发送Connection: Keep-Alive首部,服务器就会在那条请求之后关闭连接。
  • 通过检测响应中是否包含Connection: Keep-Alive响应首部,客户端可以判断服务器是否会在发出响应之后关闭连接。
  • 只有在无需检测到连接的关闭即可确定报文实体主体部分长度的情况下,才能将连接保持在打开状态——也就是说实体的主体部分必须有正确的Content-Length,有多部件媒体类型,或者用分块传输编码的方式进行了编码。在一条keep-alive信道中回送错误的Content-Length是很糟糕的事,这样的话,事务处理的另一端就无法精确地检测出一条报文的结束和另一条报文的开始了。
  • 代理和网关必须执行Connection首部的规则。代理或网关必须在将报文转发出去或将其高速缓存之前,删除在Connection首部中命名的所有首部字段以及Connection首部自身。
  • 严格来说,不应该与无法确定是否支持Connection首部的代理服务器建立keep-alive连接,以防止出现下面要介绍的哑代理问题。在实际应用中不是总能做到这一点的。
  • 从技术上来讲,应该忽略所有来自HTTP/1.0设备的Connection首部字段(包括Connection:Keep-Alive),因为它们可能是由比较老的代理服务器误转发的。但实际上,尽管可能会有在老代理上挂起的危险,有些客户端和服务器还是会违反这条规则。
  • 除非重复发送请求会产生其他一些副作用,否则如果在客户端收到完整的响应之前连接就关闭了,客户端就一定要做好重试请求的准备。

Keep-Alive和哑代理

我们来仔细看看keep-alive和哑代理中一些比较微妙的问题。Web客户端的Connection: Keep-Alive首部应该只会对这条离开客户端的TCP链路产生影响。这就是将其称作“连接”首部的原因。如果客户端正在与一台Web服务器对话,客户端可以发送一个Connection: Keep-Alive首部来告知服务器它希望保持连接的活跃状态。如果服务器支持keep-alive,就回送一个Connection: Keep-Alive首部,否则就不回送。

Connection首部和盲中继

问题出在代理上——尤其是那些不理解Connection首部,而且不知道在沿着转发链路将其发送出去之前,应该将该首部删除的代理。很多老的或简单的代理都是盲中继(blind relay),它们只是将字节从一个连接转发到另一个连接中去,不对Connection首部进行特殊的处理。

假设有一个Web客户端正通过一个作为盲中继使用的哑代理与Web服务器进行对话。图3显示的就是这种情形。
图3 keep-alive无法与不支持Connection首部的代理进行互操作

这幅图中发生的情况如下所示。

  1. 在图3 a中,Web客户端向代理发送了一条报文,其中包含了Connection:Keep-Alive首部,如果可能的话请求建立一条keep-alive连接。客户端等待响应,以确定对方是否认可它对keep-alive信道的请求。
  2. 哑代理收到了这条HTTP请求,但它并不理解Connection首部(只是将其作为一个扩展首部对待)。代理不知道keep-alive是什么意思,因此只是沿着转发链路将报文一字不漏地发送给服务器(图3 b)。但Connection首部是个逐跳首部,只适用于单条传输链路,不应该沿着传输链路向下传输。接下来,就要发生一些很糟糕的事情了。
  3. 在图3 b中,经过中继的HTTP请求抵达了Web服务器。当Web服务器收到经过代理转发的Connection: Keep-Alive首部时,会误以为代理(对服务器来说,这个代理看起来就和所有其他客户端一样)希望进行keep-alive对话!对Web服务器来说这没什么问题——它同意进行keep-alive对话,并在图3 c中回送了一个Connection: Keep-Alive响应首部。所以,此时Web服务器认为它在与代理进行keep-alive对话,会遵循keep-alive的规则。但代理却对keep-alive一无所知。不妙。
  4. 在图3 d中,哑代理将Web服务器的响应报文回送给客户端,并将来自Web服务器的Connection: Keep-Alive首部一起传送过去。客户端看到这个首部,就会认为代理同意进行keep-alive对话。所以,此时客户端和服务器都认为它们在进行keep-alive对话,但与它们进行对话的代理却对keep-alive一无所知。
  5. 由于代理对keep-alive一无所知,所以会将收到的所有数据都回送给客户端,然后等待源端服务器关闭连接。但源端服务器会认为代理已经显式地请求它将连接保持在打开状态了,所以不会去关闭连接。这样,代理就会挂在那里等待连接的关闭。
  6. 客户端在图3 d中收到了回送的响应报文时,会立即转向下一条请求,在keep-alive连接上向代理发送另一条请求(参见图3 e)。而代理并不认为同一条连接上会有其他请求到来,请求被忽略,浏览器就在这里转圈,不会有任何进展了。
  7. 这种错误的通信方式会使浏览器一直处于挂起状态,直到客户端或服务器将连接超时,并将其关闭为止。

代理和逐跳首部

为避免此类代理通信问题的发生,现代的代理都绝不能转发Connection首部和所有名字出现在Connection值中的首部。因此,如果一个代理收到了一个Connection: Keep-Alive首部,是不应该转发Connection首部,或所有名为Keep-Alive的首部的。

另外,还有几个不能作为Connection首部值列出,也不能被代理转发或作为缓存响应使用的首部。其中包括Proxy-Authenticate、Proxy-Connection、Transfer-Encoding和Upgrade。

插入Proxy-Connection

Netscape的浏览器及代理实现者们提出了一个对盲中继问题的变通做法,这种做法并不要求所有的Web应用程序支持高版本的HTTP。这种变通做法引入了一个名为Proxy-Connection的新首部,解决了在客户端后面紧跟着一个盲中继所带来的问题——但并没有解决所有其他情况下存在的问题。在显式配置了代理的情况下,现代浏览器都实现了Proxy-Connection,很多代理都能够理解它。

问题是哑代理盲目地转发Connection: Keep-Alive之类的逐跳首部惹出了麻烦。逐跳首部只与一条特定的连接有关,不能被转发。当下游服务器误将转发来的首部作为来自代理自身的请求解释,用它来控制自己的连接时,就会引发问题。

在网景的变通做法是,浏览器会向代理发送非标准的Proxy-Connection扩展首部,而不是官方支持的著名的Connection首部。如果代理是盲中继,它会将无意义的Proxy-Connection首部转发给Web服务器,服务器会忽略此首部,不会带来任何问题。但如果代理是个聪明的代理(能够理解持久连接的握手动作),就用一个Connection首部取代无意义的Proxy-Connection首部,然后将其发送给服务器,以收到预期的效果。

图4 a~图4 d显示了盲中继是如何向Web服务器转发Proxy-Connection首部,而不带来任何问题的,Web服务器忽略了这个首部,这样在客户端和代理,或者代理和服务器之间就不会建立起keep-alive连接了。图4 e~图4 h中那个聪明的代理知道Proxy-Connection首部是对keep-alive对话的请求,它会发送自己的Connection: Keep-Alive首部来建立keep-alive连接。
图4 Proxy-Connection首部修正了单个盲中继带来的问题

在客户端和服务器之间只有一个代理时可以用这种方案来解决问题。但如图5所示,如果在哑代理的任意一侧还有一个聪明的代理,这个问题就会再次露头了。
图5 对有多层次代理的情况,Proxy-Connection仍然无法解决问题

而且,网络中出现“不可见”代理的情况现在变得很常见了,这些代理可以是防火墙、拦截缓存,或者是反向代理服务器的加速器。这些设备对浏览器是不可见的,所以浏览器不会向它们发送Proxy-Connection首部。透明的Web应用程序正确地实现持久连接是非常重要的。

HTTP/1.1持久连接

HTTP/1.1逐渐停止了对keep-alive连接的支持,用一种名为持久连接(persistent connection)的改进型设计取代了它。持久连接的目的与keep-alive连接的目的相同,但工作机制更优一些。

与HTTP/1.0+的keep-alive连接不同,HTTP/1.1持久连接在默认情况下是激活的。除非特别指明,否则HTTP/1.1假定所有连接都是持久的。要在事务处理结束之后将连接关闭,HTTP/1.1应用程序必须向报文中显式地添加一个Connection:close首部。这是与以前的HTTP协议版本很重要的区别,在以前的版本中,keep-alive连接要么是可选的,要么根本就不支持。

HTTP/1.1客户端假定在收到响应后,除非响应中包含了Connection: close首部,不然HTTP/1.1连接就仍维持在打开状态。但是,客户端和服务器仍然可以随时关闭空闲的连接。不发送Connection:close并不意味着服务器承诺永远将连接保持在打开状态。

持久连接的限制和规则

在持久连接的使用中有以下限制和需要澄清的问题。

  • 发送了Connection: close请求首部之后,客户端就无法在那条连接上发送更多的请求了。
  • 如果客户端不想在连接上发送其他请求了,就应该在最后一条请求中发送一个Connection:close请求首部。
  • 只有当连接上所有的报文都有正确的、自定义报文长度时——也就是说,实体主体部分的长度都和相应的Content-Length一致,或者是用分块传输编码方式编码的——连接才能持久保持。
  • HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接——每个持久连接都只适用于一跳传输。
  • (由于较老的代理会转发Connection首部,所以)HTTP/1.1的代理服务器不应该与HTTP/1.0客户端建立持久连接,除非它们了解客户端的处理能力。实际上,这一点是很难做到的,很多厂商都违背了这一原则。
  • 尽管服务器不应该试图在传输报文的过程中关闭连接,而且在关闭连接之前至少应该响应一条请求,但不管Connection首部取了什么值,HTTP/1.1设备都可以在任意时刻关闭连接。
  • HTTP/1.1应用程序必须能够从异步的关闭中恢复出来。只要不存在可能会累积起来的副作用,客户端都应该重试这条请求。
  • 除非重复发起请求会产生副作用,否则如果在客户端收到整条响应之前连接关闭了,客户端就必须要重新发起请求。
  • 一个用户客户端对任何服务器或代理最多只能维护两条持久连接,以防服务器过载。代理可能需要更多到服务器的连接来支持并发用户的通信,所以,如果有N个用户试图访问服务器的话,代理最多要维持2N条到任意服务器或父代理的连接。

管道化连接

HTTP/1.1允许在持久连接上可选地使用请求管道。这是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列。当第一条请求通过网络流向地球另一端的服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。

图6 a-c显示了持久连接是怎样消除TCP连接时延,以及管道化请求(参见图6 c)是如何消除传输时延的。
图6 4个事务(管道化连接)

对管道化连接有几条限制。

  • 如果HTTP客户端无法确认连接是持久的,就不应该使用管道。
  • 必须按照与请求相同的顺序回送HTTP响应。HTTP报文中没有序列号标签,因此如果收到的响应失序了,就没办法将其与请求匹配起来了。
  • HTTP客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成的管道化请求。如果客户端打开了一条持久连接,并立即发出了10条请求,服务器可能在只处理了,比方说,5条请求之后关闭连接。剩下的5条请求会失败,客户端必须能够应对这些过早关闭连接的情况,重新发出这些请求。
  • HTTP客户端不应该用管道化的方式发送会产生副作用的请求(比如POST)。总之,出错的时候,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪一些。由于无法安全地重试POST这样的非幂等请求,所以出错时,就存在某些方法永远不会被执行的风险。

最后

本文节选自《HTTP权威指南》 - David Gourley Brian Totty等。


欢迎关注我的公众号 须弥零一,跟我一起学习IT知识。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !