8.1 持久连接

8.1.1 目的

在持久连接之前,为了获取每个 URL,需要建立一个单独的 TCP 连接,这就增加了 HTTP 服务器的负载,导致互联网拥堵。内联图像和其他相关数据的使用往往需要客户端在短时间内向同一服务器发出多个请求。对这些性能问题的分析和一个原型实现的结果可以得到 [26] [30]。实际的 HTTP/1.1(RFC 2068)的实施经验和测量结果显示了良好的效果 [39]。其他的解决方案也被探索过,比如 T/TCP [27]。

持久化连接有许多优点:

  • 通过打开和关闭更少的 TCP 连接,路由器和主机(客户端、服务器、代理、网关、隧道或缓存)的 CPU 时间得以节省,主机中用于 TCP 协议控制块的内存也可以节省。

  • HTTP 请求和响应可以在一个连接上进行管道化。管道化允许客户端发出多个请求,而不需要等待每个响应,从而使单个 TCP 连接的使用效率大大提高,耗时也大大降低。

  • 通过减少由 TCP 打开引起的数据包数量,并允许 TCP 有足够的时间来确定网络的拥堵状态,从而减少网络拥堵。

  • 由于没有在 TCP 的连接开放握手中花费时间,因此后续请求的延迟会减少。

  • HTTP 可以更优雅地发展,因为错误可以被报告,而不需要关闭 TCP 连接来惩罚。使用未来版本的 HTTP 的客户可能会乐观地尝试新的功能,但如果与旧的服务器通信,在报告错误后会用旧的语义重试。

HTTP 实现应该实现持久连接。

8.1.2 整体操作(Overall Operation)

HTTP/1.1 和早期版本的 HTTP 之间的一个重要区别是,持久连接是任何 HTTP 连接的默认行为。也就是说,除非另有说明,客户端应该假定服务器将保持一个持久的连接,即使是在服务器的错误响应之后。

持久连接提供了一种机制,客户端和服务器可以通过这种机制发出关闭 TCP 连接的信号。这种信号是通过连接(Connection)头字段(第 14.10 节)进行的。一旦关闭信号发出,客户机必须不再在该连接上发送任何请求。

8.1.2.1 协商(Negotiation)

一个 HTTP/1.1 服务器可能会假定一个 HTTP/1.1 客户端打算保持一个持久的连接,除非在请求中发送了包括connection-token 为 “close” 的 Connection 头字段。如果服务器选择在发送响应后立即关闭连接,它应该发送一个包括 connection-token 为 “close” 的 Connection 头字段。

一个 HTTP/1.1 客户端可能期望一个连接保持开放,但会根据服务器的响应是否包括 connection-token 为 “close” 的 Connection 头字段来决定是否保持开放。如果客户端不想为超过该请求保持一个连接,它应该发送一个包括 connection-token 为 “close” 的 Connection 头字段。

如果客户端或服务器在 Connection 头中发送了close 标记,该请求就成为该连接的最后一个请求。

客户端和服务器不应该认为小于 1.1 版本的 HTTP 会保持一个持久的连接,除非它被明确地示意。关于与 HTTP/1.0 客户端的后向兼容性的更多信息,请参见第 19.6.2 节

为了保持持久性,连接上的所有消息必须有一个自我定义的消息长度(即,一个不由连接关闭定义的长度),如4.4 节所述。

8.1.2.2 管道化(Pipelining)

支持持久连接的客户端可以 “管道” 其请求(即,发送多个请求而不等待每个响应)。服务器必须按照收到请求的相同顺序发送其对这些请求的响应。

假设持久连接并在连接建立后立即进行流水线处理的客户端,应该准备在第一次管道化尝试失败时重试他们的连接。如果客户端做了这样的重试,那么在它知道连接是持久的之前,一定不要进行管道化。如果服务器在发送所有相应的响应之前关闭了连接,客户也必须准备好重新发送他们的请求。

客户端不应该使用非幂等的方法或非幂等的方法序列来处理请求(见第 9.1.2 节)。否则,传输连接的过早终止可能导致不确定的结果。希望发送非幂等请求的客户端应该等待发送该请求,直到它收到前一个请求的响应状态。

8.1.3 代理服务器

特别重要的是,代理机构要正确地实现第 14.10 节中规定的 Connection 头字段的属性。

代理服务器必须与它的客户端和它所连接的源服务器(或其他代理服务器)分别发出持久性连接的信号。每个持久性连接只适用于一个传输链路。

代理服务器不得与 HTTP/1.0 客户端建立 HTTP/1.1 持久连接(但请参见 RFC 2068 [33],以了解许多HTTP/1.0 客户端实现的 Keep-Alive 头的信息和讨论)。

8.1.4 实际考虑

服务器通常会有一些超时值,超过这个值就不再维持一个不活动的连接。代理服务器可能会使这个值更高,因为客户可能会通过同一个服务器进行更多的连接。持久连接的使用对客户端或服务器的超时长度(或存在)没有任何要求。

当一个客户或服务器希望超时时,它应该在传输连接上发出一个优雅的关闭。客户端和服务器都应该持续关注另一方的传输关闭,并在适当的时候对其做出响应。如果客户机或服务器没有及时发现对方的关闭,可能会对网络造成不必要的资源消耗。

客户端、服务器或代理可以在任何时候关闭传输连接。例如,在服务器决定关闭 “空闲” 连接的同时,客户端可能已经开始发送一个新的请求。从服务器的角度来看,连接是在空闲时被关闭的,但从客户的角度来看,一个请求正在进行中。

这意味着客户端、服务器和代理机构必须能够从异步关闭事件中恢复。只要请求序列是异步的,客户端软件应该重新打开传输连接并重新传送被中止的请求序列,而不需要用户交互(见第 9.1.2 节)。非幂等方法或序列不得自动重试,尽管用户代理可以向人类操作员提供重试请求的选择。具有对应用程序语义理解的用户代理软件的确认可以替代用户确认。如果第二个请求序列失败,自动重试不应该重复。

如果可能的话,服务器应该总是对每个连接至少响应一个请求。服务器不应该在传输响应的过程中关闭连接,除非怀疑是网络或客户端故障。

使用持久连接的客户端应该限制他们与特定服务器同时保持的连接数。一个单用户客户端不应该与任何服务器或代理保持超过 2 个连接。一个代理机构应该使用最多 2*N 个连接到另一个服务器或代理机构,其中 N 是同时活动的用户数量。这些准则旨在改善 HTTP 响应时间,避免拥堵。

8.2 消息传输要求

8.2.1 持久连接和流控制

HTTP/1.1 服务器应该保持持久的连接,并使用 TCP 的流量控制机制来解决暂时的过载问题,而不是终止连接,期望客户会重试。后者的技术会加剧网络拥堵。

8.2.2 监视连接以获取错误状态

一个发送消息体的 HTTP/1.1(或更高版本)客户端应该在传输请求时监控网络连接是否有错误状态。如果客户端看到一个错误状态,它应该立即停止传输主体。如果主体使用 “chunked” 编码发送(第 3.6 节),零长度的 chunked 和空 trailer 可能被用来过早地标记消息的结束。如果主体前面有一个 Content-Length 头,客户端必须关闭连接。

8.2.3 使用 100(Continue)状态

100(Continue)状态(见第 10.1.1 节)的目的是允许正在发送带有请求体的请求消息的客户端在客户端发送请求体之前确定源服务器是否愿意接受该请求(基于请求头)。在某些情况下,如果服务器不看正文就拒绝该消息,那么客户端发送正文可能是不合适的,或者效率很低。

对 HTTP/1.1 客户端的要求:

  • 如果客户端在发送请求正文之前将等待 100(Continue)响应,它必须发送一个带有 “100-continue “期望的 Expect 请求头字段(14.20 节)。

  • 如果客户端不打算发送请求体,那么它不应该发送带有 “100-continue” 期望的 Expect 请求头字段(第 14.20 节)。

由于旧的实现方式的存在,该协议允许模棱两可的情况,即客户端可能发送 “Expect: 100-continue” 而没有收到417(Expectation Failed)状态或100(Continue)状态。因此,当客户端向一个从未见过 100(继续)状态的源服务器(可能是通过代理)发送这个头域时,客户端不应该在发送请求体之前无限期地等待。

对 HTTP/1.1 源服务器的要求:

  • 当接收到一个包括有 “100-continue” 期望的 Expect 请求头字段的请求时,源服务器必须以 100(Continue)状态响应并继续从输入流中读取,或者以最终状态代码响应。源服务器在发送 100(Continue)响应之前不应等待请求正文。如果它以最终状态代码进行响应,它可以关闭传输连接,也可以继续读取并丢弃请求的其余部分。 如果它返回最终状态代码,它必须不执行所请求的方法。

  • 如果请求信息不包括带有 “100-continue” 期望的 Expect 请求头字段,源服务器不应发送 100(Continue)响应;如果这样的请求来自 HTTP/1.0(或更早)客户端,则必须不发送 100(Continue)响应。这条规则有一个例外:为了与 RFC 2068 兼容,服务器可以在响应 HTTP/1.1 PUT 或 POST 请求时发送一个100(Continue)状态,该请求不包括一个期望值为 “100-continue” 的请求头字段。这个例外的目的是为了尽量减少与未声明的等待 100(Continue)状态有关的任何客户端处理延迟,只适用于 HTTP/1.1 请求,而不是任何其他 HTTP-版本 的请求。

  • 如果源服务器已经收到了相应请求的部分或全部请求体,它可以省略 100(Continue)响应。

  • 发送 100(Continue)响应的源服务器必须最终发送一个最终状态代码,一旦收到并处理了请求主体,除非它过早地终止了传输连接。

  • 如果源服务器收到的请求不包括期望值为 “100-continue” 的 Expect 请求头字段,该请求包括一个请求体,并且服务器在从传输连接中读取整个请求体之前以最终状态代码进行响应,那么服务器在读取整个请求之前或在客户端关闭连接之前不应关闭传输连接。否则,客户端可能无法可靠地收到响应消息。然而,这一要求不能被理解为阻止服务器对拒绝服务攻击进行自我防御,或阻止糟糕的客户端实现。

对 HTTP/1.1 代理的要求:

  • 如果代理知道下一跳服务器的版本是 HTTP/1.0 或更低,它必须不转发该请求,并且必须以 417(Expectation Failed)状态响应。

  • 代理机构应该维护一个缓存,记录从最近参考的下一跳服务器收到的 HTTP 版本号。

  • 如果请求消息是从 HTTP/1.0(或更早的)客户端接收的,并且不包括带有 “100-continue” 期望的 Expect 请求头字段,则代理机构不得转发 100(Continue)响应。这一要求优先于转发 1xx 响应的一般规则(见第 10.1 节)。

8.2.4 服务器过早关闭连接时的客户端行为

如果一个 HTTP/1.1 客户端发送了一个包括请求体的请求,但不包括具有 “100-continue” 期望的 Expect 请求头字段,并且如果该客户端没有直接连接到 HTTP/1.1 源服务器,并且如果该客户端在收到服务器的任何状态之前看到连接关闭,该客户端应该重试该请求。 如果客户端重试这个请求,它可以使用下面的 “二进制指数后退” 算法来保证获得可靠的响应:

  1. 启动一个与服务器的新连接
  2. 传输请求头
  3. 将一个变量 R 初始化为到服务器的估计往返时间(例如,基于建立连接的时间),如果往返时间不可用,则初始化为 5 秒的常数。
  4. 计算 T = R (2*N),其中 N 是这个请求以前的重试次数。
  5. 等待来自服务器的错误响应,或等待 T 秒(以先到者为准)。
  6. 如果没有收到错误的响应,在 T 秒后传输请求的主体。
  7. 如果客户端看到连接被过早关闭,就从步骤 1 开始重复,直到请求被接受,收到错误响应,或者用户变得不耐烦,终止重试过程。

如果在任何时候收到一个错误状态,客户端

  • 不应该继续并且
  • 如果它没有完成发送请求消息,则关闭连接。