比较老的 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 连接。 下图 显示了 keep-alive 连接的一些性能优点,图中将在串行连接上实 现 4 个 HTTP 事务的时间线与在一条持久连接上实现同样事务所需的时 间线进行了比较。 由于去除了进行连接和关闭连接的开销,所以时间 线有所缩减。(由于去除了慢启动阶段,请求和响应时间可能也有缩减。这种性能收益在图中没有显示出 来)
[图片上传失败...(image-ee5ac6-1607570892332)]
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 首部请求将一条连接保持在打开状态。 如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含 相同的首部(参见图 4-14)。如果响应中没有 Connection: KeepAlive 首部,客户端就认为服务器不支持 keep-alive,会在发回响应报 文之后关闭连接。
[图片上传失败...(image-d396bb-1607570892332)]
Keep-Alive 选项
注意,keep-Alive 首部只是请求将连接保持在活跃状态。发出 keepalive 请求之后,客户端和服务器并不一定会同意进行 keep-alive 会话。 它们可以在任意时刻关闭空闲的 keep-alive 连接,并可随意限制 keepalive 连接所处理事务的数量。 可以用 Keep-Alive 通用首部中指定的、由逗号分隔的选项来调节 keep-alive 的行为。 参数 timeout 是在 Keep-Alive 响应首部发送的。它估计了服务 器希望将连接保持在活跃状态的时间。这并不是一个承诺值。 参数 max 是在 Keep-Alive 响应首部发送的。它估计了服务器还希 望为多少个事务保持此连接的活跃状态。这并不是一个承诺值。 Keep-Alive 首部还可支持任意未经处理的属性,这些属性主要用 于诊断和调试。语法为 name [=value]。 Keep-Alive 首部完全是可选的,但只有在提供 Connection: KeepAlive 时才能使用它。这里有个 Keep-Alive 响应首部的例子,这个例 子说明服务器最多还会为另外 5 个事务保持连接的打开状态,或者将打 开状态保持到连接空闲了 2 分钟之后。
[图片上传失败...(image-2e172d-1607570892332)]
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: KeepAlive 首部来告知服务器它希望保持连接的活跃状态。如果服务器支持 keep-alive,就回送一个 Connection: Keep-Alive 首部,否则就不回 送。
1. Connection 首部和盲中继 问题出在代理上——尤其是那些不理解 Connection 首部,而且不 知道在沿着转发链路将其发送出去之前,应该将该首部删除的代 理。很多老的或简单的代理都是盲中继(blind relay),它们只是 将字节从一个连接转发到另一个连接中去,不对 Connection 首部 进行特殊的处理。 假设有一个 Web 客户端正通过一个作为盲中继使用的哑代理与 Web 服务器进行对话。下图 显示的就是这种情形。
[图片上传失败...(image-4f85ad-1607570892332)]
这幅图中发生的情况如下所示。
1. 在图 4-15a 中,Web 客户端向代理发送了一条报文,其中包含 了 Connection: Keep-Alive 首部,如果可能的话请求建立 一条 keep-alive 连接。客户端等待响应,以确定对方是否认可 它对 keep-alive 信道的请求。
2. 哑代理收到了这条 HTTP 请求,但它并不理解 Connection 首 部(只是将其作为一个扩展首部对待)。代理不知道 keepalive 是什么意思,因此只是沿着转发链路将报文一字不漏地发 送给服务器(图 4-15b)。但 Connection 首部是个逐跳首 部,只适用于单条传输链路,不应该沿着传输链路向下传输。 接下来,就要发生一些很糟糕的事情了。
3. 在图 4-15b 中,经过中继的 HTTP 请求抵达了 Web 服务器。 当 Web 服务器收到经过代理转发的 Connection: KeepAlive 首部时,会误以为代理(对服务器来说,这个代理看起 来就和所有其他客户端一样)希望进行 keep-alive 对话!对 Web 服务器来说这没什么问题——它同意进行 keep-alive 对 话,并在图 4-15c 中回送了一个 Connection: Keep-Alive 响应首部。所以,此时 Web 服务器认为它在与代理进行 keepalive 对话,会遵循 keep-alive 的规则。但代理却对 keep-alive 一无所知。不妙。
4. 在图 4-15d 中,哑代理将 Web 服务器的响应报文回送给客户 端,并将来自 Web 服务器的 Connection: Keep-Alive 首部 一起传送过去。客户端看到这个首部,就会认为代理同意进行 keep-alive 对话。所以,此时客户端和服务器都认为它们在进 行 keep-alive 对话,但与它们进行对话的代理却对 keep-alive 一无所知。
5. 由于代理对 keep-alive 一无所知,所以会将收到的所有数据都 回送给客户端,然后等待源端服务器关闭连接。但源端服务器 会认为代理已经显式地请求它将连接保持在打开状态了,所以 不会去关闭连接。这样,代理就会挂在那里等待连接的关闭。
6. 客户端在图 4-15d 中收到了回送的响应报文时,会立即转向下 一条请求,在 keep-alive 连接上向代理发送另一条请求(参见 图 4-15e)。而代理并不认为同一条连接上会有其他请求到 来,请求被忽略,浏览器就在这里转圈,不会有任何进展了。
7. 这种错误的通信方式会使浏览器一直处于挂起状态,直到客户 端或服务器将连接超时,并将其关闭为止。
2. 代理和逐跳首部 为避免此类代理通信问题的发生,现代的代理都绝不能转发 Connection 首部和所有名字出现在 Connection 值中的首部。因 此,如果一个代理收到了一个 Connection: Keep-Alive 首部, 是不应该转发 Connection 首部,或所有名为 Keep-Alive 的首部 的。 另外,还有几个不能作为 Connection 首部值列出,也不能被代理 转发或作为缓存响应使用的首部。其中包括 ProxyAuthenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade。
插入Proxy-Connection
Netscape 的浏览器及代理实现者们提出了一个对盲中继问题的变通做 法,这种做法并不要求所有的 Web 应用程序支持高版本的 HTTP。这种 变通做法引入了一个名为 Proxy-Connection 的新首部,解决了在客 户端后面紧跟着一个盲中继所带来的问题——但并没有解决所有其他情 况下存在的问题。在显式配置了代理的情况下,现代浏览器都实现了 Proxy-Connection,很多代理都能够理解它。 问题是哑代理盲目地转发 Connection: Keep-Alive 之类的逐跳首部 惹出了麻烦。逐跳首部只与一条特定的连接有关,不能被转发。当下游 服务器误将转发来的首部作为来自代理自身的请求解释,用它来控制自 己的连接时,就会引发问题。 在网景的变通做法是,浏览器会向代理发送非标准的 ProxyConnection 扩展首部,而不是官方支持的著名的 Connection 首部。 如果代理是盲中继,它会将无意义的 Proxy-Connection 首部转发给 Web 服务器,服务器会忽略此首部,不会带来任何问题。但如果代理是 个聪明的代理(能够理解持久连接的握手动作),就用一个 Connection 首部取代无意义的 Proxy-Connection 首部,然后将其发 送给服务器,以收到预期的效果。
下图 a ~ d显示了盲中继是如何向Wb服务器转发 ProxyConnection 首部,而不带来任何问题的,Web 服务器忽略了这个首 部,这样在客户端和代理,或者代理和服务器之间就不会建立起 keepalive 连接了。下图 e ~h 中那个聪明的代理知道 ProxyConnection 首部是对 keep-alive 对话的请求,它会发送自己的 Connection: Keep-Alive 首部来建立 keep-alive 连接。
[图片上传失败...(image-b31141-1607570892332)]
在客户端和服务器之间只有一个代理时可以用这种方案来解决问题。但 下图 所示,如果在哑代理的任意一侧还有一个聪明的代理,这个 问题就会再次露头了
[图片上传失败...(image-31c043-1607570892332)]
而且,网络中出现“不可见”代理的情况现在变得很常见了,这些代理可 以是防火墙、拦截缓存,或者是反向代理服务器的加速器。这些设备对 浏览器是不可见的,所以浏览器不会向它们发送 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 个用户试图访问服务器的话,代理最多 要维持 2_N_ 条到任意服务器或父代理的连接。
管道化连接
HTTP/1.1 允许在持久连接上可选地使用请求管道。这是相对于 keepalive 连接的又一性能优化。在响应到达之前,可以将多条请求放入队 列。当第一条请求通过网络流向地球另一端的服务器时,第二条和第三 条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络 的环回时间,提高性能。
下图 a-c 显示了持久连接是怎样消除 TCP 连接时延,以及管道化请求 (参见下图 c)是如何消除传输时延的。
对管道化连接有几条限制。
如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道。
必须按照与请求相同的顺序回送 HTTP 响应。HTTP 报文中没有序 列号标签,因此如果收到的响应失序了,就没办法将其与请求匹配 起来了。
HTTP 客户端必须做好连接会在任意时刻关闭的准备,还要准备好 重发所有未完成的管道化请求。如果客户端打开了一条持久连接, 并立即发出了 10 条请求,服务器可能在只处理了,比方说,5 条 请求之后关闭连接。剩下的 5 条请求会失败,客户端必须能够应对 这些过早关闭连接的情况,重新发出这些请求。
HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比 如 POST)。总之,出错的时候,管道化方式会阻碍客户端了解服 务器执行的是一系列管道化请求中的哪一些。由于无法安全地重试 POST 这样的非幂等请求,所以出错时,就存在某些方法永远不会 被执行的风险。
[图片上传失败...(image-61a08f-1607570892332)]
关闭连接的奥秘
连接管理——尤其是知道在什么时候以及如何去关闭连接——是 HTTP 的实用魔法之一。这个问题比很多开发者起初意识到的复杂一些,而且 没有多少资料涉及这个问题。
“任意”解除连接
所有 HTTP 客户端、服务器或代理都可以在任意时刻关闭一条 TCP 传 输连接。通常会在一条报文结束时关闭连接,(除非服务器怀疑出现了客户端或网络故障,否则就不应该在请求的中间关闭连接。)但出错的时候,也可能 在首部行的中间,或其他奇怪的地方关闭连接。
对管道化持久连接来说,这种情形是很常见的。HTTP 应用程序可以在 经过任意一段时间之后,关闭持久连接。比如,在持久连接空闲一段时 间之后,服务器可能会决定将其关闭。
但是,服务器永远都无法确定在它关闭“空闲”连接的那一刻,在线路那 一头的客户端有没有数据要发送。如果出现这种情况,客户端就会在写 入半截请求报文时发现出现了连接错误。
4.7.2 Content-Length 及截尾操作
每条 HTTP 响应都应该有精确的 Content-Length 首部,用以描述响应 主体的尺寸。一些老的 HTTP 服务器会省略 Content-Length 首部,或 者包含错误的长度指示,这样就要依赖服务器发出的连接关闭来说明数 据的真实末尾。
客户端或代理收到一条随连接关闭而结束的 HTTP 响应,且实际传输的 实体长度与 Content-Length 并不匹配(或没有 Content-Length) 时,接收端就应该质疑长度的正确性。
如果接收端是个缓存代理,接收端就不应该缓存这条响应(以降低今后 将潜在的错误报文混合起来的可能)。代理应该将有问题的报文原封不 动地转发出去,而不应该试图去“校正”Content-Length,以维护语义 的透明性。
连接关闭容限、重试以及幂等性
即使在非错误情况下,连接也可以在任意时刻关闭。HTTP 应用程序要 做好正确处理非预期关闭的准备。如果在客户端执行事务的过程中,传 输连接关闭了,那么,除非事务处理会带来一些副作用,否则客户端就 应该重新打开连接,并重试一次。对管道化连接来说,这种情况更加严 重一些。客户端可以将大量请求放入队列中排队,但源端服务器可以关 闭连接,这样就会留下大量未处理的请求,需要重新调度。
副作用是很重要的问题。如果在发送出一些请求数据之后,收到返回结 果之前,连接关闭了,客户端就无法百分之百地确定服务器端实际激活 了多少事务。有些事务,比如 GET 一个静态的 HTML 页面,可以反复 执行多次,也不会有什么变化。而其他一些事务,比如向一个在线书店 POST 一张订单,就不能重复执行,不然会有下多张订单的危险。
如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个 事务就是幂等的。实现者们可以认为 GET、HEAD、PUT、DELETE、 TRACE 和 OPTIONS 方法都共享这一特性。(基于 GET 构建动态表单的管理者们要确保这些表单是幂等的。)客户端不应该以管道化方 式传送非幂等请求(比如 POST)。否则,传输连接的过早终止就会造 成一些不确定的后果。要发送一条非幂等请求,就需要等待来自前一条 请求的响应状态。
尽管用户 Agent 代理可能会让操作员来选择是否对请求进行重试,但一 定不能自动重试非幂等方法或序列。比如,大多数浏览器都会在重载一 个缓存的 POST 响应时提供一个对话框,询问用户是否希望再次发起事 务处理。
正常关闭连接
如下图所示,TCP 连接是双向的。TCP 连接的每一端都有一个输入 队列和一个输出队列,用于数据的读或写。放入一端输出队列中的数据 最终会出现在另一端的输入队列中。
[图片上传失败...(image-b48ad7-1607570892332)]
1. 完全关闭与半关闭
应用程序可以关闭 TCP 输入和输出信道中的任意一个,或者将两 者都关闭了。套接字调用 close() 会将 TCP 连接的输入和输出信 道都关闭了。这被称作“完全关闭”,如图 a 所示。还可以用套 接字调用 shutdown() 单独关闭输入或输出信道。这被称为“半关 闭”,如图 b 所示。
[图片上传失败...(image-11ca89-1607570892332)]
2. TCP关闭及重置错误
简单的 HTTP 应用程序可以只使用完全关闭。但当应用程序开始与 很多其他类型的 HTTP 客户端、服务器和代理进行对话且开始使用 管道化持久连接时,使用半关闭来防止对等实体收到非预期的写入 错误就变得很重要了。
总之,关闭连接的输出信道总是很安全的。连接另一端的对等实体 会在从其缓冲区中读出所有数据之后收到一条通知,说明流结束 了,这样它就知道你将连接关闭了。
关闭连接的输入信道比较危险,除非你知道另一端不打算再发送其 他数据了。如果另一端向你已关闭的输入信道发送数据,操作系统 就会向另一端的机器回送一条 TCP“连接被对端重置”的报文,如下图所示。大部分操作系统都会将这种情况作为很严重的错误来处 理,删除对端还未读取的所有缓存数据。对管道化连接来说,这是 非常糟糕的事情。[图片上传失败...(image-5026-1607570892332)]
比如你已经在一条持久连接上发送了 10 条管道式请求了,响应也 已经收到了,正在操作系统的缓冲区中存着呢(但应用程序还未将 其读走)。现在,假设你发送了第 11 条请求,但服务器认为你使 用这条连接的时间已经够长了,决定将其关闭。那么你的第 11 条 请求就会被发送到一条已关闭的连接上去,并会向你回送一条重置 信息。这个重置信息会清空你的输入缓冲区。
当你最终要去读取数据的时候,会得到一个连接被对端重置的错 误,已缓存的未读响应数据都丢失了,尽管其中的大部分都已经成 功抵达你的机器了。
正常关闭
HTTP 规范建议,当客户端或服务器突然要关闭一条连接时,应 该“正常地关闭传输连接”,但它并没有说明应该如何去做。
总之,实现正常关闭的应用程序首先应该关闭它们的输出信道,然 后等待连接另一端的对等实体关闭它的输出信道。当两端都告诉对 方它们不会再发送任何数据(比如关闭输出信道)之后,连接就会 被完全关闭,而不会有重置的危险。
但不幸的是,无法确保对等实体会实现半关闭,或对其进行检查。 因此,想要正常关闭连接的应用程序应该先半关闭其输出信道,然 后周期性地检查其输入信道的状态(查找数据,或流的末尾)。如 果在一定的时间区间内对端没有关闭输入信道,应用程序可以强制 关闭连接,以节省资源。