HTTP(HyperText Transfer Protocol): 超文本传输协议。 它是工作在应用层的 client-server 协议, 它定义了 client(客户端) 和 server(服务端) 之间通信的方式。 ref
HTTP 是由 Tim Berners-Lee 设计的。 大家可能不认识 Berners-Lee, 但大家一定知道 万维网(WWW, World Wide Web), 万维网就是由 Berners-Lee 发明的, 他是英国计算机科学家。 HTTP 最开始就是他为了万维网而设计出来的。 ref
HTTP 主要由以下几个版本: ref
版本 | 引入(正式使用)时间 | 说明 |
---|---|---|
HTTP/0.9 | 1991 | 已淘汰 |
HTTP/1.0 | 1996 | 已淘汰 |
HTTP/1.1 | 1997 | 还在使用 |
HTTP/2.0 | 2015 | 还在使用 |
HTTP/3.0 | 2022 | 还在使用 |
HTTP/0.9 和 HTTP/1 已经被淘汰了, 重点应该关注最新的三个版本。
🍕 HTTP/0.9
只支持 GET 请求, 并且该 GET 请求只能请求 HTML 文件。 HTTP/0.9 可以说是非常简单了!
🍕 HTTP/1.0
其实在 1992 年的时候, 就有了 HTTP/1.0-draft, 它对 GET 请求进行了一些完善。 所谓 draft(草案), 就是在正式(官网)版本发行之前使用的。
在 1995 年时, HTTP WG(HTTP 工作组) 为了规范化 HTTP 的设计, 同时让未来的 HTTP 更易扩展, 他们决定编写 RFC 文档。
RFC 系统用于收集一些非官方文档(memo), 不过在这其中有些文档会被 IETF 选为标准。 但大部分文档并不是标准。 ref
最终, 在 1996 年 5 月, HTTP/1.0 版本发布了。(RFC 1945)
HTTP/1.0 中每个文档都至少需要耗费两个 RTT 时间, 流程如下图所示: 建立 TCP 连接的前两个报文, 就耗费了一个 RTT; 在三报文握手的第三个报文中携带上 HTTP 请求数据, 然后收到响应报文时, 又耗费了一个 RTT。
可以看出, HTTP/1.0 的主要缺点是:
- 每请求一个资源(文档), 都需要耗费两个 RTT, 而访问一个网页, 可能需要请求很多个资源, 比如每张图片都是一次文档请求。每个 css 文件也是一次文档请求。
- 每次请求都需要建立 TCP 连接, 而每次建立 TCP 连接, 服务器都需要有对应的内存管理(传输控制块TCB), 服务器又是服务多个客户的, 这很容易导致服务器的负载过大。
对于 HTTP/1.0 这种非持续连接的连接方式, 可以借助浏览器提供的并行 TCP 连接来缩短一些响应时间, 但并行的数量是有限的, 改善的效果也是有限的。
实际上, HTTP/1.0 发行时, 大家就已经在使用 keep-alive 来解决非持续连接问题了, keep-alive 是 HTTP/1.1-draft 中的内容, 它能够较好的解决非持续连接问题。 ref
🍕 HTTP/1.1
虽然HTTP/1.1 的标准到了 1997 年才发行, 但实际上, 浏览器早就已经在使用 HTTP/1.1 中的相关特性了。 1997 年 HTTP/1.1 的标准 RFC 2068 发行, 不过它最著名的标准还是 RFC 2616。 RFC 2616 是 1999 年发行的, 它在 RFC 2068 的基础上进行了完善, 有时间的话, 可以读读 RFC 2616。
HTTP/1.1 使用了持续连接(persistent connection)。 它是通过 keep-alive 机制实现持续连接的, 有了持续连接后, server 在返回响应报文后, 不会再关闭 TCP 连接, 而是会将该连接继续保持一段时间(具体多长可自定义), 如果在这段时间内有新的请求, 则 server 可以直接返回响应, 不需要再次建立 TCP 连接。
HTTP/1.1 的持续连接有两种工作方式: 非流水线方式(without pipelining) 和流水线方式(with pipelining)。
- 非流水线: 客户需要在收到前一个响应后才能发出下一个请求。
- 流水线: 是客户可以在收到前一个响应报文之前, 连续地发送多个请求报文, 而服务器必须 按序返回 响应报文。
非持续连接和持续连接的区别见 下图:
注意⚠️, 虽然理论上流水线的方式可以有效的节省时间, 是实践表明流水线方式是不可行的。 ref
观察上面的工作方式, 我们会发现这么一个特点: 如果前一个请求还未返回响应报文, 那么后续的响应将需要等待。 这种现象叫做 HOLB(head of line blocking, 队头阻塞)。
HTTP/1.1 中解决 HOLB 的常用方法是开启多条 TCP 连接并行传输。 但这也带来了新的问题: ref
- 浏览器允许的 TCP 并行数量是有限的, 若超过额度, 那么仍然会被阻塞。
- TCP 的建立需要耗费资源, 而且如果是 HTTPS, 那么消耗的资源将会更多。
- 服务器的带宽是有限的, 建立多条 TCP, 每条 TCP 的带宽将会减少。 而且新的 TCP 建立会经过慢启动过程, 这会影响网络的吞吐量和延迟。
🍕 HTTP/2.0
HTTP/2.0 最知名的标准规范是 RFC 7540.
HTTP/1.1 通过持续连接一定程度上降低了多请求的响应延迟, 但它还存在队头阻塞问题。 并且 HTTP/1.1 的请求头字段经常是重复冗余的, 这会导致 TCP 的 "阻塞窗口" 很快就被填满, 从而出现网络延迟。 ref
HTTP/2.0 为了解决上述 HTTP/1.1 的问题, 提出了下面这些新特性:
- Frame (二进制分帧): 这是 HTTP/1.1 和 HTTP/2.0 的最大区别, 后面的几个新特征都是基于 Frame 才能实现的。
- 多路复用(stream and Multiplexing): 允许在同一 TCP 连接上交错发送请求和响应消息。
- 头部压缩(Header Compression): 使用 HPACK 算法对头部压缩。
- 响应优先级(Stream Priority): 允许对请求进行优先级排序, 可以更快地完成重要的请求。
- 服务器推送(server push): 不需要 client 发起请求, 服务器能自动向客户端推送浏览器渲染页面所需的资源
Frame
在 HTTP/1.1 中传输格式是文本形式, 而 HTTP/2.0 中是二进制形式。
HTTP/1.1 响应头案例:
HTTP/2.0 响应头案例:
帧的格式如下:
+-----------------------------------------------+
| Length (24) | 前三个字节是载荷长度
+---------------+---------------+---------------+
| Type (8) | Flags (8) | 第四个字节是帧的类型; 第五个字节是类型对应的 Flags, 一般是 0
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) | 接下来的 4 个字节中, 第一位是保留位, 默认置0, 剩余 31 位是流的 ID
+=+=============================================================+
| Frame Payload (0...) ... 帧的载荷取决于具体类型的帧的格式
+---------------------------------------------------------------+
HTTP/2.0 的有多种类型的帧:
- DATA (数据帧)
- HEADERS (头部帧)
- PRIORITY (优先级帧)
- RST_STREAM
- SETTINGS
- PUSH_PROMISE
- PING
- GOAWAY
- WINDOW_UPDATE
- CONTINUATION
这里只介绍一下 DATA 帧的格式:
+---------------+
|Pad Length? (8)| 该字段可选, 用于指示 DATA 帧后面的填充字节的长度
+---------------+-----------------------------------------------+
| Data (*) ... 实际要传输的数据
+---------------------------------------------------------------+
| Padding (*) ... 填充字节, 长度由 Pad Length 字段决定
+---------------------------------------------------------------+
想要了解其他类型帧的格式和含义, 可以查阅 Frame Definitions - RFC 7540
多路复用
HTTP/1.1 中, 无法实现多路复用, 下面是一个例子:
假设有两个文件 main.js
和 style.css
文件
// main.js
console.log('hello world')
/* style.css */
body {
color : blue;
}
我们想要实现多路复用, 那么会先对文件进行拆分, 比如按行拆分然后发送。具体如何拆分不重要, 重点是理解原理。 当我们交替发送时, HTTP/1.1 并不知道谁和谁是一起的, 所以收到的消息组成起来可能是这样的:
body {
color : blue;
console.log('hello world')
}
这就是为什么 HTTP/1.1 无法实现多路复用。
而 HTTP/2.0 采用的是帧传输, 这个帧传输的工作原理类似于链路层的帧, 但两个帧是不一样的。
还是上面的例子, 使用 HTTP/2.0 的情况下, 在一个 TCP 连接中同时发送 main.js
和 style.css
,
首先, 这个 TCP 连接分为两个流, 一个流是 main.js
, 一个流是 style.css
。
然后, 每个流中可以传输若干个 message, 每个 message 由若干个二进制帧组成。
这里为了方便讲解, 假定每个 message 包含以下信息: 每行都是一个 message, 它的结构实际上是若干个二进制帧
request=style.css, content='body {'
request=style.css, content=' color : blue;'
request=main.js, content='console.log('hello world')'
request=style.css, content='}'
这种情况下, 我们就可以知道每个行内容对应哪个文件, 从而实现了多路复用。
HTTP/2.0 中的队头阻塞
HTTP/2.0 中的多路复用确实能够解决 HTTP/1.1 中的队头阻塞, 但这并不代表 HTTP/2.0 中就没有队头阻塞了。 HTTP/2.0 中的同样有队头阻塞, 但这个队头阻塞体现在 TCP 上, 还是拿前面那两个代码举例:
假设有四个帧分别在四个 TCP 包中:
- TCP 包 1: 包含了 (包含了
style.css
的第 1 行内容)的 HTTP 2 的帧 - TCP 包 2: 包含了 (包含了
main.js
的全部内容)的 HTTP 2 的帧 - TCP 包 3: 包含了 (包含了
style.css
的第 2 行内容)的 HTTP 2 的帧 - TCP 包 4: 包含了 (包含了
style.css
的第 3 行内容)的 HTTP 2 的帧
假如 TCP 包2 丢失了, 那么 TCP 会要求重传 包2, 而不会将 包1, 包3, 包4 交付给 HTTP(这是 TCP 可靠传输机制)。
即使 style.css
总共就三行内容, 但 TCP 工作在运输层, HTTP 工作在应用层, TCP 无法获取上层的信息, 所以不会将 包1,3,4 交付给 HTTP。
这将导致了 HTTP 需等待 TCP 包2 的重传, 这就是 HTTP/2.0 在 TCP 上的队头阻塞。
🍕 HTTP/3.0
HTTP/3.0 的第一个草案是 2020 年才提出的, 最近的标准是 RFC 9114。
前面已经知道了 HTTP/2.0 中也存在队头阻塞, 该队头阻塞是由于 TCP 的机制引起的。
再来看看 HTTP/2.0 的其他问题, 现在的 HTTPS 是主流, 而 HTTPS 需要建立 TLS 连接, 在 HTTP/2.0 的模型下, client 请求资源前, 需要经过 3 个 RTT:
HTTP client 🧑
TCP client 👩🦰
TCP server 🧓
TLS client 👮♀️
TLS server 💂♀️
🧑: 我要和大哥说话
👩🦰 默默对 🧑 说: 我知道你很急, 但你先别急
👩🦰: 嗨! 🧓, 你在吗?
🧓: 嗨! 👩🦰, 我在, 你在吗?
👮♀️: Hello! 能给我把钥匙吗?
💂♀️: 给! 你的钥匙
🧑: 终于到我了, 我要 index.html!
注意: 1.2及1.2版本一下的 TLS 建立连接是需要 2 个 RTT; 1.3 版本的 TLS 第一次建立连接需要 1 个 RTT, 后面建立连接不需要 RTT。 这里考虑的是 1.2 版本。
上面的对话其实就是 下图的内容:
可以看的出来, HTTP/2.0 的瓶颈在 TCP 和 TLS, 所以 HTTP/3.0 对此的解决方法是, 不使用 TCP 和 TLS, 而是使用 QUIC。
QUIC(Quick UDP Internet Connection): QUIC 是基于 UDP 协议的, 但 QUIC 是可靠传输的, 并且提供加密, 多路复用等功能。
当使用了 QUIC 后, 之前的对话将变成下面这样了:
HTTP client 🧑
QUIC client 🤡
QUIC server 👽
第 1️⃣ 次访问, 需要 1 RTT
🧑: 我要和大哥说话
🤡: 嗨! 👽 你在吗? 在的话能给我把钥匙吗?
👽: 嗨! 🤡, 我在, 这是你的钥匙!
🧑: 今天这么快? 我要 index.html!
👽 (偷偷地告诉🤡): 这还有把钥匙, 下次找我可以不用问, 直接用
第 2️⃣ 次访问, 需要 0 RTT
🧑: 我要和大哥说话!
🤡: 嗨! 👽, 你在吗? 后面的话我已经用上次你给我的钥匙加密过了, HTTP 那小子肯定要 index.html!
👽: 嗨! 🤡, 我在, 我知道你要 index.html, 给你!
🧑: ❓, 6
🍕 总结
版本 | 关键词 |
---|---|
HTTP/0.9 | 只能 GET 请求 HTML 文件 |
HTTP/1.0 | 非持续连接 |
HTTP/1.1 | 通过 keep-alive 实现持续连接, 但存在队头阻塞 |
HTTP/2.0 | 帧, 多路复用, 但存在 TCP 上的队头阻塞 |
HTTP/3.0 | 使用 QUIC 实现加密和多路复用 |
最后, 即使是现在, HTTP/1.1 也还是很常见的, 主要是因为对于小型网站, HTTP/1.1 已经够用了。如果要使用 HTTP/2.0 需要花费额外的资源。 所以大部分情况下, 一个网站上请求的资源是既有 HTTP/1.1, 又有 HTTP/2.0, 甚至还有 HTTP/3.0。