1. 概述
HTTP 是建立在 TCP 协议上的无状态的应用层协议,我们假设这样的场景,一个页面需要请求服务器 N 次,如果每次请求都是无状态的,那么就需要进行 N 次 TCP 的连接建立与断开过程。我们都知道,TCP 连接的建立与断开会经历三次握手与四次挥手,所以这个过程的延迟与损耗是很大的。
因此,提供 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 条请求。
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支持管道化连接:
使用管道化的连接也有几处限制:
- 如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道。
- 必须按照与请求的相同顺序回送 HTTP 响应,因为 HTTP 没有序号这个概念,所以一旦响应失序,就没办法将其与请求匹配起来了。
- HTTP 客户端必须做好连接会在任何时刻关闭的准备,还要准备好重发所有未完成的管道化请求。
- 客户端不应该以管道化的方式发送任何非幂等请求,比如 POST,否则就会造成不确定的后果。
4. HTTP2.0 与 多路复用机制
多路复用将每一个请求被切割成了多个 ID 相同的帧,相同的帧会在缓存区拼接,拼接好后,通过多路复用机制就能复用 TCP 连接。
帧是一个数据单元,实现了对消息的封装。下面是HTTP/2的帧结构:
帧的字节中保存了不同的信息,前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 | 描述 |
|---|---|---|
| DATA | 0x0 | 传输流的核心内容 |
| HEADERS | 0x1 | 包含HTTP首部,和可选的优先级参数 |
| PRIORITY | 0x2 | 指示或者更改流的优先级和依赖 |
| RST_STREAM | 0x3 | 允许一端停止流(通常是由于错误导致的) |
| SETTINGS | 0x4 | 协商连接级参数 |
| PUSH_PROMISE | 0x5 | 提示客户端,服务端要推送些东西 |
| PING | 0x6 | 测试连接可用性和往返时延(RTT) |
| GOAWAY | 0x7 | 告诉另外一端,当前端已结束 |
| WINDOW_UPDATE | 0x8 | 协商一端要接收多少字节(用于流量控制) |
| CONTINUATION | 0x9 | 用以拓展HEADER数据块 |
消息帧通过复用 Stream 达到多路复用的效果。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。