HTTP 连接管理:keep-alived、持久连接与多路复用详解

3,823 阅读7分钟

1. 概述

HTTP 是建立在 TCP 协议上的无状态的应用层协议,我们假设这样的场景,一个页面需要请求服务器 N 次,如果每次请求都是无状态的,那么就需要进行 N 次 TCP 的连接建立与断开过程。我们都知道,TCP 连接的建立与断开会经历三次握手与四次挥手,所以这个过程的延迟与损耗是很大的。

image.png

因此,提供 HTTP 的连接管理机制,在需要的场景下,复用 TCP 的连接,就显得很有必要。

2. HTTP 1.0 keep-alived 机制

HTTP1.0 的 keep-alived 是在 TCP 的基础上实现的对 socket 套接字的复用,TCP 的 keep-alived 保活机制底层是靠心跳检测,当 HTTP 开启了 keep-alived 时,就能服用保活的 socket。但是由于 keep-alived 默认不开启,所以可能会出现哑代理的问题,即代理服务器不能识别keep-alived 导致的问题。并且 keep-alived 只能在完成一个请求之后才能接受另一个请求。

在 HTTP 1.0 + Keep-Alived 的方式下,客户端可以通过在请求首部添加 Connection:Keep-Alive 来打开连接管理开关(默认关闭)。

在 Keep-Alived 机制下,通信双方会通过心跳检验复用 TCP 的 keep-alived 连接。这样就能在一个连接中,处理 N 条请求。

image.png

Keep-Alived 的哑代理问题:

  • 首先,Web 客户端向代理发送了一条报文,其中包含了 Connection: Keep-Alive 首部,希望在这次 HTTP 事务之后继续保持活跃状态,然后客户端等待响应,已确定对方是否允许持久连接。
  • 哑代理(这里先界定为哑代理是不妥的,我们往往先看做的事,再给这件事定性,现在这个服务器还没做出哑代理行为呢,就给他定性了)收到了这条 HTTP 请求,但它不理解 Connection 首部,它也不知道 Keep-Alive 是什么意思,因此只是沿着转发链路将报文发送给服务器,但 Connection 首部是个 Hop-by-Hop 首部,只适用于单条链路传输,所以这个代理服务器不应该再将其发送给服务器了,但是它还是发送了,后面就会发生一些难顶的事情。
  • 经过转发的 HTTP 请求到达服务器后,会误以为对方希望保持 Keep-Alive 持久连接,经过评估后,服务器作出响应,它同意进行 Keep-Alive 对话,所以它回送了一个 Connection:Keep-Alive 响应并到达了哑代理服务器。
  • 哑代理服务器会直接将响应发送给客户端,客户端收到响应后,就知道服务器可以使用持久连接。然而,此时客户端和服务器都知道要使用 Keep-Alive 持久连接,但是哑代理服务器却对 Keep-Alive 一无所知。
  • 由于代理对 Keep-Alive 一无所知,所以会收到的所有数据都会发送给客户端,然后等待服务器关闭连接,但是代理服务器却认为应该保持打开状态,所以不会去关闭连接。这样,哑代理服务器就一直挂在那里等待连接的关闭。
  • 等到客户端发送下一个 HTTP 事务后,哑代理会直接忽视新的 HTTP 事务,因为它并不认为一条连接上还会有其他请求的到来,所以会直接忽略新的请求。

3. HTTP1.1 持久连接机制

持久连接是默认开启的,支持持久连接的服务器克服了哑代理问题,并且不需要等待上一个请求结束,下一个请求再发送,但是请求仍然是顺序执行的,后面的请求会被前面的耗时请求阻塞

与 HTTP/1.0 的 Keep-Alive 连接不同,HTTP/1.1 在默认情况下使用的就是持久连接。除非特别指明,否则 HTTP/1.1 会假定所有连接都是持久连接。如果想要在事务结束后关闭连接的话,就需要在报文中显示添加一个 Connection:close 首部。

使用 persistent connection 也会有一些限制和规则

  • 首先,发送了 Connection: close 请求后,客户端就无法在这条连接上发送更多的请求。这同时也可以说,如果客户端不想发送其他请求,就可以使用 Connection:close 关闭连接。
  • HTTP/1.1 的代理必须能够分别管理客户端和服务器的持久连接 ,每个持久连接都只适用于单次传输。
  • 客户端对任何服务器或者代理最好只维护两条持久连接,以防止服务器过载。
  • 只有实体部分的长度和相应的 Content-Length保持一致时,或者使用分块传输编码的方式时,连接才能保持长久。

持久连接机制虽然解决了哑代理问题,但是其请求形式与keep-alived一样,仍然是串行的,会出现大任务阻塞小任务的情况,为了解决这个问题,HTTP1.1支持管道化连接

image.png

使用管道化的连接也有几处限制:

  • 如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道。
  • 必须按照与请求的相同顺序回送 HTTP 响应,因为 HTTP 没有序号这个概念,所以一旦响应失序,就没办法将其与请求匹配起来了。
  • HTTP 客户端必须做好连接会在任何时刻关闭的准备,还要准备好重发所有未完成的管道化请求。
  • 客户端不应该以管道化的方式发送任何非幂等请求,比如 POST,否则就会造成不确定的后果。

4. HTTP2.0 与 多路复用机制

多路复用将每一个请求被切割成了多个 ID 相同的帧,相同的帧会在缓存区拼接,拼接好后,通过多路复用机制就能复用 TCP 连接。

帧是一个数据单元,实现了对消息的封装。下面是HTTP/2的帧结构:

image.png

帧的字节中保存了不同的信息,前9个字节对于每个帧都是一致的,“服务器”解析HTTP/2的数据帧时只需要解析这些字节,就能准确的知道整个帧期望多少字节数来进行处理信息。我们先来了解一下帧中每个字段保存的信息:

  • Length 3 字节 表示帧负载的长度,默认最大帧大小2^14
  • Type 1 字节 当前帧的类型
  • Flags 1 字节 具体帧的标识
  • R 1 字节 保留位,不需要设置,否则可能带来严重后果
  • Stream Identifier 31 位 每个流的唯一ID
  • Frame Payload 不固定 真实帧的长度,真实长度在Length中设置

为了能够发送不同的“数据信息”,通过帧数据传递不同的内容,HTTP/2中定义了10种不同类型的帧,在上面表格的Type字段中可对“帧”类型进行设置:

名称ID描述
DATA0x0传输流的核心内容
HEADERS0x1包含HTTP首部,和可选的优先级参数
PRIORITY0x2指示或者更改流的优先级和依赖
RST_STREAM0x3允许一端停止流(通常是由于错误导致的)
SETTINGS0x4协商连接级参数
PUSH_PROMISE0x5提示客户端,服务端要推送些东西
PING0x6测试连接可用性和往返时延(RTT)
GOAWAY0x7告诉另外一端,当前端已结束
WINDOW_UPDATE0x8协商一端要接收多少字节(用于流量控制)
CONTINUATION0x9用以拓展HEADER数据块

消息帧通过复用 Stream 达到多路复用的效果。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿