HTTP(Hypertext Transfer Protocol,超文本传输协议)是用于在 Web 浏览器与服务器之间传输数据的基础协议。自 1991 年发明以来,HTTP 经历了多次版本更新,随着互联网的快速发展,它也不断演进以满足新的需求
HTTP/0.9
HTTP/0.9 是最早期的 HTTP 协议版本,它由 Tim Berners-Lee
在 1991 年设计并实现,作为万维网(World Wide Web)的基础协议之一。HTTP/0.9 的设计非常简单,是 Web 早期的通信基础,它标志着 Web 浏览器和服务器之间的第一次通信。
HTTP/0.9 的设计非常简单,仅支持 GET 方法,用于获取静态 HTML 文档。请求只需要一行内容,而响应仅包含 HTML 文件的正文部分,没有任何头部信息、状态码等附加内容。
这种协议不支持多媒体文件或动态内容,仅适用于传输静态超文本内容。由于其无状态特性,每个请求和响应都是独立的,服务器不会保存任何上下文,也不支持会话管理。
HTTP/0.9 基于 TCP 实现数据传输,客户端通过 TCP 连接发送请求,服务器返回响应后立即关闭连接。整个流程简洁而高效,但功能极为有限。
总的来说,它的主要特点如下所示:
-
仅支持 GET 方法
-
只能传输 HTML 文档
-
没有 HTTP 头部信息
-
没有状态码和错误处理
-
每次请求后立即关闭连接
HTTP/0.9 的通信过程非常简单。客户端通过 TCP 建立连接后发送一行 GET 请求,例如:
GET /index.html
服务器接收请求后返回 HTML 文件的内容,类似以下示例:
<html>
<head>
<title>Welcome to HTTP/0.9</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
响应内容没有状态码(如 200 或 404)或头部信息,仅包含 HTML 文件的正文。
它的基础同学流程如下图代码所示:
const net = require("net");
const server = net.createServer((socket) => {
socket.on("data", (data) => {
const request = data.toString();
if (request.startsWith("GET ")) {
const html = "<html><body>Hello World</body></html>";
socket.write(html);
}
socket.end();
});
});
server.listen(8080);
它的整个连接处理流程有这些步骤:
-
客户端发起 TCP 连接
-
发送 GET 请求
-
服务器返回 HTML 文档
-
连接关闭
HTTP/0.9 是 HTTP 协议的开端,虽然功能简单,但在当时满足了传输静态 HTML 文档的需求。它为后续的 HTTP 协议(如 HTTP/1.0、HTTP/1.1)的发展奠定了坚实的基础,也反映了互联网早期的技术现状。尽管 HTTP/0.9 早已被淘汰,但它的简洁性和历史价值,仍是理解现代 HTTP 协议不可或缺的一部分。
HTTP/1.0
HTTP/1.0 作为 HTTP/0.9 的继任者,于 1996 年发布。它引入了许多重要的功能特性,为现代 Web 通信奠定了更坚实的基础。
在 HTTP/0.9 的基础上,HTTP/1.0 增加了两个新的请求方法:
-
POST:向服务器提交数据(如表单)。
-
HEAD:获取文档的元数据,不返回文档主体。
还增加了 HTTP 状态码,用于标识服务器对客户端请求的处理结果。常见的状态码如下所示:
-
2xx 成功:如 200 OK 表示请求成功。
-
3xx 重定向:如 301 Moved Permanently 表示资源永久转移。
-
4xx 客户端错误:如 404 Not Found 表示资源未找到。
-
5xx 服务端错误:如 500 Internal Server Error 表示服务器内部错误。
增加了 HTTP 头部信息,用于描述请求和响应的元数据。客户端可以发送更多的元信息,比如请求的内容类型(Content-Type)、语言(Accept-Language)等。服务端可以附加额外的信息,比如返回的内容类型、内容长度等。
如下所示:
HTTP/1.0 200 OK
Server: Apache/1.3.0
Date: Mon, 15 Nov 2023 12:00:00 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 217
Last-Modified: Sun, 14 Nov 2023 12:00:00 GMT
Expires: Tue, 16 Nov 2023 12:00:00 GMT
Cache-Control: max-age=3600
X-Powered-By: PHP/5.2.1
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP/1.0 Response Example</title>
<meta charset="utf-8">
</head>
<body>
<h1>Welcome to HTTP/1.0</h1>
<p>This is a sample response from an HTTP/1.0 server.</p>
</body>
</html>
HTTP/1.0 通过 Content-Type 支持传输除 HTML 外的多种内容类型(如 JSON、图片、视频等)。
例如返回一个图片:
HTTP/1.0 200 OK
Server: Apache/1.3.0
Date: Mon, 15 Nov 2023 12:00:00 GMT
Content-Type: image/jpeg
Content-Length: 123456
Last-Modified: Sun, 14 Nov 2023 12:00:00 GMT
Cache-Control: max-age=86400
[二进制图片数据...]
在 HTTP/1.0 中,默认情况下是没有持久连接(persistent connection)的,连接是短连接,即请求完成后会立即关闭 TCP 连接。但通过非标准的扩展,HTTP/1.0 实际上可以实现持久连接,这依赖于一个非标准的头部:Connection: keep-alive
。它默认的情况下是 Connection: close
,需要手动开启。而在 HTTP/1.1 中,只有当 Connection 被设置为 close 时才会用到这个模型。
HTTP1.0 在 0.9 的基础上增加了一些新的特性,但是它仍然存在一些问题,比如:
-
不支持虚拟主机:没有 Host 头部时,服务器无法区分不同的虚拟主机(同一服务器运行多个网站)
-
缓存机制不完善:HTTP/1.0 引入了简单的缓存机制,通过以下响应头实现:
-
Expires:指定资源的过期时间。
-
Last-Modified:资源的最后修改时间,用于判断缓存是否需要更新。
-
-
队头阻塞:资源加载是串行的,无法并行处理多个请求。
HTTP/1.0 是互联网发展早期的基础协议,它显著扩展了 HTTP/0.9 的功能,增加了状态码、请求头、响应头以及多种方法支持,并首次引入了缓存机制。然而,由于它是短连接协议,存在性能低下、队头阻塞等问题,这些不足促使了 HTTP/1.1 的诞生。
HTTP/1.1
HTTP/1.1(HyperText Transfer Protocol 1.1)是目前互联网通信中最常用的协议之一。它是在 HTTP/1.0 的基础上进行了改进,增加了很多重要的功能,使得 Web 的性能和可扩展性得到了改善。
在传统的 HTTP/1.0 中,每个请求和响应都是通过独立的连接进行的,且每个请求都必须等待前一个请求的响应返回之后才能继续发起下一个请求。这就导致了很大的延迟,尤其是在加载多个资源时。
HTTP/1.1 引入了 管道化(Pipelining),允许客户端在一个 TCP 连接上连续发送多个请求,而无需等待每个请求的响应返回。这意味着客户端可以通过管道化提高请求的并发性,从而减少延迟和连接的数量。
管道化的主要工作原理是:客户端在同一个连接上 连续发送多个请求,而不等待前一个请求的响应。所有请求在 TCP 连接上发送,服务器会按顺序逐一处理这些请求,并按顺序返回响应。
管道化的流程:
-
客户端发送多个请求:客户端可以一次性发送多个请求,不必等前一个请求的响应完成。例如,客户端可以请求多个页面资源,如 HTML 文件、CSS 文件、JS 文件等。
-
服务器接收并顺序处理请求:服务器收到客户端的请求后,逐个处理这些请求。每个请求的处理顺序和发送顺序一致。
-
服务器按顺序返回响应:尽管客户端可能发送多个请求,服务器仍然会按发送顺序依次返回响应。即使某个请求的响应较慢,后续请求的响应也会受到影响,因为响应是顺序返回的。
假设客户端需要请求 3 个资源:index.html
,style.css
,和 script.js
。在 HTTP/1.0
中,客户端必须等待每个请求的响应才会发起下一个请求,流程如下:
请求 1 -> 响应 1 -> 请求 2 -> 响应 2 -> 请求 3 -> 响应 3
在 HTTP/1.1 中,客户端可以同时发出多个请求,而无需等待响应,流程如下:
请求 1 -> 请求 2 -> 请求 3
响应 1 -> 响应 2 -> 响应 3
根据上面的内容,我们可以知道管道化的优点主要有以下几个方面:
-
减少连接数与开销:在
HTTP/1.0
中,每个请求默认需要建立一个新的 TCP 连接,这会带来较大的延迟和资源消耗。虽然HTTP/1.0
通过引入 Keep-Alive 机制允许复用连接,但仍需等待每个请求的响应后才能发起下一个请求。而在HTTP/1.1
管道化 中,客户端可以通过一个持久连接同时发送多个请求,无需等待每个请求的响应完成,从而进一步减少了频繁建立和断开连接的开销,提升了资源利用率。 -
减少请求延迟:HTTP/1.0 即使启用了 Keep-Alive,仍需逐一发送请求并等待响应完成,延迟较高。而 HTTP/1.1 的管道化允许在同一个连接上连续发送多个请求,客户端不需要等待响应返回即可发起后续请求。这在加载多个资源时,显著减少了请求之间的等待时间。
-
提高吞吐量:HTTP/1.1 的管道化机制允许多个请求在同一连接上并行发送,从而充分利用网络带宽和服务器处理能力。相比 HTTP/1.0 的 Keep-Alive,管道化减少了连接空闲时间,尤其在高延迟网络中表现更为明显。
尽管管道化在理论上提高了性能,但也存在一些问题,尤其是 队头阻塞(Head-of-Line Blocking) 问题。
队头阻塞 是管道化的一个严重问题。即使后续请求的响应已经准备好,如果第一个请求的响应没有返回,后续的响应也无法立即返回。所有响应必须按请求的顺序返回,因此一个慢响应会导致后续所有响应的延迟。
比如,假设有 3 个请求,分别请求 index.html,style.css 和 script.js。如果 index.html 的响应非常慢,那么 style.css 和 script.js 的响应会被阻塞,即使它们的处理完成得很快。
客户端发送请求 1 -> 请求 2 -> 请求 3
响应 1(慢)-> 响应 2 -> 响应 3
这使得 HTTP/1.1 的管道化在高延迟环境下的性能提升有限,因为请求的顺序决定了响应的顺序,后续的响应会被前面的慢响应阻塞。
传输编码
在 HTTP/1.1 中,传输编码(Transfer-Encoding) 是一种机制,用于对 HTTP 响应的消息体进行分块或其他形式的编码,以便支持动态生成内容、节省带宽以及满足其他特定需求。
HTTP/1.1 在响应消息体的传输中,需要处理动态内容生成、带宽优化等问题。而 Content-Length 头部虽然能描述整个消息体的长度,但它有以下局限:
-
如果响应内容是动态生成的,服务器可能无法在开始传输前知道总长度。
-
如果传输的资源很大且无法一次性发送,提前计算内容长度会影响性能。
为了解决这些问题,HTTP/1.1
引入了 Transfer-Encoding
头部,允许服务器在不确定消息体总长度的情况下动态分块传输内容。
传输编码的核心思想是对消息体进行分块或压缩。使用传输编码时,服务器在传输响应数据前声明编码方式,客户端根据声明的编码方式解析和处理数据。
常见的传输编码方式包括:
-
分块传输编码(Chunked Transfer-Encoding)
-
压缩传输编码(Compression Transfer-Encoding)
-
身份传输编码(Identity Transfer-Encoding)
其中,分块传输编码是最常用的一种方式。
分块传输编码(Chunked Transfer-Encoding)
分块传输编码允许服务器将响应消息体分成一个个独立的块,每个块可以在生成后立即发送,而不需要提前计算整个消息体的总长度。这在动态生成内容时非常实用。
分块传输编码的特点:
-
每个块由两部分组成:块的大小(以 16 进制表示)和块的数据。
-
块之间以 CRLF(回车换行)分隔。
-
最后一个块的大小为 0,表示消息体结束。
分块传输编码的格式:
Transfer-Encoding: chunked
Transfer-Encoding: compress
Transfer-Encoding: deflate
Transfer-Encoding: gzip
// 可以列出多个值,用逗号分割
Transfer-Encoding: gzip, chunked
分块编码主要应用于如下场景,即要传输大量的数据,但是在请求在没有被处理完之前响应的长度是无法获得的。例如,当需要用从数据库中查询获得的数据生成一个大的 HTML 表格的时候,或者需要传输大量的图片的时候。一个分块响应形式如下:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/html
5\r\n
Hello\r\n
7\r\n
World!\r\n
0\r\n
\r\n
-
第一块的大小是 5,表示 5 个字节的数据 Hello。
-
第二块的大小是 7,表示 7 个字节的数据 World!。
-
最后一块大小为 0,表示响应结束。
客户端解析时会根据块的大小动态读取数据,直到遇到大小为 0 的块。
压缩传输编码(Compression Transfer-Encoding)
在传输数据时,为了节省带宽,可以对消息体进行压缩。HTTP/1.1 支持的压缩编码方式包括:
-
gzip:使用 GNU zip 压缩算法。
-
deflate:使用 zlib 压缩格式。
-
compress:使用 Unix compress 压缩算法(较少使用)。
服务器可以通过 Transfer-Encoding 指定压缩方式。例如:
Transfer-Encoding: gzip
服务器在发送数据前会对消息体进行 gzip 压缩,客户端收到数据后根据 Transfer-Encoding 中声明的方式进行解压。
压缩传输编码的优势在于大幅降低了传输数据的体积,但需要客户端和服务器支持相应的压缩算法。
当使用 Transfer-Encoding 时,服务器不需要再发送 Content-Length 头部,因为分块传输编码本身会标明每个块的大小。
如果同时包含了 Transfer-Encoding: chunked 和 Content-Length,会优先使用 Transfer-Encoding,因为分块传输编码不需要总长度。
缓存控制
缓存机制是 HTTP 协议中重要的一环,其主要目的是通过减少重复请求来提升性能、节省带宽和加速页面加载。在 HTTP/1.0 和 HTTP/1.1 中,缓存机制有很大的不同,HTTP/1.1 针对 HTTP/1.0 的不足进行了大幅优化。
HTTP/1.0 的缓存机制相对简单,主要依赖以下几个头部字段进行缓存管理:
- Expires:指定资源的过期时间。
Expires 是 HTTP/1.0 中最重要的缓存控制头部,服务器通过它指定资源的过期时间。客户端会在该时间之前直接使用缓存,而不会向服务器发起请求。
例如,以下响应头表示资源将在 2025 年 1 月 10 日 08:00 前有效:
Expires: Wed, 10 Jan 2025 08:00:00 GMT
Expires 使用绝对时间,依赖客户端和服务器的系统时间同步。如果客户端时间不准确,可能会导致缓存误用或缓存提前失效。
- If-Modified-Since 和 Last-Modified:用于判断资源是否需要更新。
HTTP/1.0 使用 If-Modified-Since 头部来实现缓存验证。当客户端缓存了某个资源后,可以通过发送请求头 If-Modified-Since,附带上次缓存资源的时间,询问服务器资源是否发生了更新。 服务器会根据资源的 Last-Modified 时间判断,如果资源未修改,返回状态码 304 Not Modified,客户端继续使用缓存;如果资源已更新,返回新的资源内容和状态码 200 OK。
Last-Modified 的精度仅到秒,无法检测 1 秒内的多次更新。并且如果资源未更改内容,但时间戳更新(例如重新部署资源),可能会导致客户端误认为资源已更新,强制重新下载。
- Pragma: no-cache
Pragma 是 HTTP/1.0 中的另一个缓存相关头部,通常用于请求中,指示客户端或中间代理不使用缓存,而是直接向服务器请求最新资源。
例如:
Pragma: no-cache
Pragma 的功能有限,且主要用于请求,不能在响应中灵活控制缓存行为。
总结来看,HTTP/1.0 的缓存机制存在诸多局限性,包括依赖绝对时间、验证机制粗糙以及对动态内容支持不足等问题。
为了弥补 HTTP/1.0 的不足,HTTP/1.1 对缓存机制进行了全面改进,引入了更多头部字段和灵活的缓存控制策略,使其更加高效、精准和可靠。
- Cache-Control
Cache-Control 是 HTTP/1.1 中最核心的缓存控制头部,它取代了 HTTP/1.0 中的 Expires 和 Pragma,提供了丰富的缓存控制指令。它既可以出现在响应头中,也可以出现在请求头中,用于定义缓存的行为。
常见的指令包括:
max-age=<秒>
:指定资源在缓存中的最大有效时间,以秒为单位。例如:
Cache-Control: max-age=3600
表示该资源在缓存中有效期为 3600 秒(1 小时)。max-age 使用相对时间,避免了 Expires 依赖系统时间的问题。
-
no-cache:指示客户端在使用缓存前必须向服务器验证缓存的有效性。
-
no-store:禁止缓存,无论是客户端还是代理服务器,都不允许存储资源。
-
public 和 private:public 允许资源被代理缓存,private 表示资源只能被客户端缓存。
-
must-revalidate:指示缓存过期后必须向服务器重新验证。
相比 Expires,Cache-Control 提供了更灵活、更精确的缓存控制能力。
- ETag 和 If-None-Match
HTTP/1.1 引入了 ETag(实体标签)作为资源版本的标识符。每个资源都有一个唯一的 ETag 值,通常是基于资源内容生成的哈希值或指纹。
服务器返回资源时附带 ETag 头:
ETag: "1234567890"
客户端在缓存资源后,可以通过 If-None-Match 头发送 ETag 值,询问服务器资源是否发生了更新。
例如:
If-None-Match: "1234567890"
如果服务器检测到 ETag 未变化,则返回 304 Not Modified,客户端继续使用缓存;如果 ETag 改变,则返回新的资源内容。
ETag 不依赖时间戳,能够精确判断资源内容是否变化,解决了 Last-Modified 的精度问题。即使资源的最后修改时间未变,但内容发生了变化,ETag 依然可以检测到。
- Vary
HTTP/1.1 引入了 Vary 头部,用于动态内容的缓存控制。它指示缓存代理根据请求头的某些字段来区分缓存内容。例如,如果资源内容会因 Accept-Encoding 变化(如 gzip 压缩或未压缩),服务器可以返回以下头部:
Vary: Accept-Encoding
这意味着代理服务器需要为不同的 Accept-Encoding 值缓存不同版本的资源。
值得注意的是 HTTP/1.1 仍然支持 Expires,但如果同时存在 Cache-Control: max-age,后者会优先使用。Cache-Control 的相对时间机制解决了 Expires 的时间同步问题,使其更可靠。
HTTP/1.1 还实现了虚拟主机的支持,通过 Host 头部来区分不同的虚拟主机。它可以实现一个服务器托管多个域名:
Host: www.example.com
通过 Host 头部,服务器可以区分不同的虚拟主机,并根据不同的域名返回不同的资源。
HTTP/1.1 是 HTTP 协议的重要版本,解决了 HTTP/1.0 中的许多性能问题,尤其是通过持久连接和缓存机制的改进显著提升了效率。然而,HTTP/1.1 仍然存在一些局限性,例如队头阻塞和并发能力不足等问题。
HTTP/1.1 的每个请求都会附带完整的请求头和上下文信息(如 User-Agent、Cookies、Authorization 等),在复杂应用场景中,这些头部可能非常冗长,尤其是携带大量 Cookies 的场景。对于每个请求都需要重新传输这些数据,这无疑会增加带宽使用,浪费传输资源。
这些问题在后续的 HTTP/2 和 HTTP/3 中得到了进一步优化和解决。
SPDY
SPDY(发音为“Speedy”)是由 Google 于 2009 年提出的一种实验性网络协议,它的主要目的是加速 Web 页面加载速度。SPDY 是基于 HTTP/1.1 的优化协议,它解决了 HTTP/1.1 在性能上的诸多瓶颈,并为后来 HTTP/2 的设计提供了重要的参考和基础。
SPDY 的主要改进包括:
-
多路复用:SPDY 允许在同一个连接上同时发送多个请求,而无需等待前一个请求的响应返回。这显著提高了连接的利用率和请求的并发性。
-
请求优先级:SPDY 允许为每个请求设置优先级,服务器可以根据优先级调整资源分配,从而优化加载顺序。
-
服务器推送:SPDY 允许服务器在客户端请求之前主动推送资源,减少延迟。
-
压缩头部:SPDY 使用了一种称为 HPACK 的压缩算法,对 HTTP 头部进行压缩,减少了传输的数据量。
-
安全加密:SPDY 支持 TLS 加密,确保数据传输的安全性。
SPDY 的这些改进显著提升了 Web 页面的加载速度,尤其是对于复杂的页面和多媒体内容。然而,SPDY 并没有成为主流协议,而是被 HTTP/2 所取代。
多路复用(Multiplexing)
SPDY 允许在单一的 TCP 连接上同时处理多个请求和响应,所有请求和响应都可以并发进行,而不会相互阻塞。每个请求和响应被分配一个唯一的流 ID,从而实现流之间的独立传输。
它的工作原理如下步骤所示:
-
每个请求和响应被分成多个帧(Frame)。
-
帧可以交错传输,并在接收端通过流 ID 重新组装。
-
避免了 HTTP/1.1 中的队头阻塞问题。
这样就减少了 TCP 连接的数量(通常只有一个连接)。提高了带宽利用率和请求响应的并发性。
请求优先级(Request Prioritization)
SPDY 允许为每个请求设置优先级,服务器可以根据优先级调整资源分配,从而优化加载顺序。
每个请求被分配一个优先级值,从 0 到 255,0 表示最低优先级,255 表示最高优先级。服务器可以根据这些优先级值调整资源的加载顺序,优先加载高优先级的资源。
优先级高的请求会被服务器优先处理,确保关键资源(如 HTML、CSS 文件)能尽快加载,而非关键资源(如图片)可以延后。
服务器推送(Server Push)
SPDY 允许服务器在客户端请求之前主动推送资源,减少延迟。
服务器在发送响应时,可以附带一个或多个资源,客户端在收到响应后,可以立即使用这些资源,而无需再次请求。
服务器推送可以显著减少页面加载时间,尤其是在资源依赖关系复杂的情况下。
压缩头部(Header Compression)
SPDY 使用 HPACK 算法对 HTTP 请求和响应头部进行压缩,从而减少了传输中头部数据的大小。HTTP/1.1 中,头部字段冗长且重复,尤其在携带大量 Cookie 的场景下会显著增加传输量。
HPACK 通过使用 Huffman 编码和静态表、动态表等技术,有效地减少了头部数据的大小,提高了传输效率。
它减少了头部的传输开销,提高了传输速度,特别是在高延迟网络中表现明显。
加密优先
SPDY 强制使用 TLS/SSL 加密传输。这意味着 SPDY 的所有通信都必须是安全的,避免了明文传输的安全风险。
工作原理
SPDY 的设计基于 HTTP/1.1,但优化了其传输层。以下是 SPDY 的基本工作原理:
-
建立连接:SPDY 使用 TCP 作为传输层,并强制使用 TLS/SSL 加密。客户端和服务器协商使用 SPDY 协议(通过 ALPN 或 NPN 协议进行协商)。
-
请求和响应的帧传输:HTTP 请求和响应被拆分成多个帧(Frame)。每个帧通过流 ID 进行标识,支持多路复用和独立传输。
-
头部压缩:请求和响应的头部被压缩后传输,减少传输体积。
-
优先级调度:服务器根据请求的优先级,优先处理和传输关键资源。
-
服务端推送:服务器在客户端请求之前主动推送资源。
SPDY 的这些改进显著提升了 Web 页面的加载速度,尤其是对于复杂的页面和多媒体内容。然而,SPDY 并没有成为主流协议,而是被 HTTP/2 所取代。
虽然 SPDY 解决了 HTTP 层的队头阻塞问题,但仍然存在 TCP 层的队头阻塞。如果一个 TCP 包丢失,会影响同一连接上的所有流。
总的来说,SPDY 是一个革命性的网络协议,通过多路复用、头部压缩、服务端推送等技术,解决了 HTTP/1.1 的性能瓶颈问题,大幅提升了 Web 应用的加载速度。虽然 SPDY 已被 HTTP/2 取代,但它的设计理念为 HTTP/2 提供了重要的参考。
HTTP/2.0
HTTP/2 是超文本传输协议(HTTP)的第二个主要版本,由 IETF 于 2015 年发布。它在 SPDY 协议(由 Google 提出的实验性协议)的基础上进行了改进,并成为正式的 HTTP 协议标准。HTTP/2 的主要目标是解决 HTTP/1.1 的性能瓶颈,提高网络传输效率,减少延迟,为现代 Web 提供更高效的通信方式。
由于前面的 HTTP/1.1 在 HTTPS 的加持下已经在安全方面做得非常好了,所以 HTTP/2 的唯一目标就是改进性能。 但它不仅背负着众多的期待,同时还背负着 HTTP/1.1 庞大的历史包袱,所以协议的修改必须小心谨慎,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产。
因为必须要保持功能上的兼容,所以 HTTP/2 把 HTTP 分解成了 语义 和 语法 两个部分,语义层不做改动,与 HTTP/1 完全一致。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用 http 表示明文协议,用 https 表示加密协议。
接下来我们就来用一个动图的方式看看 HTTP/2 有多牛逼吧:
头部压缩
HTTP
协议的报文是由 Header + Body
构成的,对于 Body
部分,HTTP/1.1
协议可以使用头字段 Content-Encoding
指定 Body
的压缩方式,比如用 gzip
压缩,这样可以节约带宽,但报文中的另外一部分 Header
,是没有针对它的优化手段。
HTTP/1.1
中的 Header
存在一些问题,主要包括以下几点:
-
文本格式:
HTTP/1.1
中的Header
采用纯文本格式传输,以键值对的形式表示。这种文本格式有一定的冗余,占用了大量的传输空间。而且由于文本格式的不规范,解析起来相对较慢。并且含有很多固定的字段,例如Cookie
、User Agent
、Accept
等,这就成了不择不扣的大头儿子,而小头爸爸就是body
。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费; -
重复传输: 在每个请求和响应中,都需要发送完整的
Header
信息,即使前后两个请求或响应中有部分Header
是相同的。这样就造成了重复传输,浪费了带宽和网络资源;
HTTP/2
没使用常见的 gzip
压缩方式来压缩头部,而是开发了 HPACK
算法,该算法主要包含三个组成部分:
-
静态字典;
-
动态字典;
-
Huffman
编码(压缩算法);
HTTP/2
头部的编码通过静态表、动态表、Huffman
编码共同完成的,如下图所示:
为什么是 HPACK
我们知道压缩算法并不是一个新鲜玩意,http 对 header 的压缩为什么不使用已有的类似 deflate 这种通用压缩算法呢。事实上在 http 在历史上确实这么干过,比如在很早之前,https 中的 tls 协议承载的应用层数据就是 deflate 压缩后的 header 和 body 数据,http2 的前身 SPDY 协议也采用一个带 preset dictionary 的类似 deflate 对 header 进行压缩,但是这种做法存在严重的安全漏洞,也就是臭名昭著的 CRIME 攻击。
CRIME 攻击利用了压缩和加密相结合时的一个漏洞,攻击者通过操控请求的部分数据来推测加密密文的内容。这种攻击依赖于压缩算法对重复数据的处理方式,并通过压缩比的变化来泄漏加密数据的信息。通过多次试探,攻击者可以逐步破解加密数据中的某些部分。
另外一点,HTTP header 这个场景足够特殊,因为通常来说一个连接上的 header 都比较固定,数据存在很大的冗余,随着连接上数据交换的进行,header 压缩率理论上可以越来越高,因为大部分 header 都可能在之前出现过。deflate 这种通用的压缩算法不能有效利用这个场景知识。
基于这两个原因,http2 提出适合 http2 协议的 header 压缩算法 HPACK。
HPACK 的压缩机制主要基于两个核心概念:静态表(Static Table)和动态表(Dynamic Table)。这两者用于存储和引用头部字段,以便通过索引值进行压缩。
静态表
静态表是一个预定义的、固定的头部字段集合,包含了一些常见的头部字段。HTTP/2 规定了静态表的内容,客户端和服务器都知道这个静态表的内容。
如下所示:
Index | Header Name | Header Value |
---|---|---|
1 | :authority | |
2 | :method | GET |
3 | :method | POST |
4 | :path | / |
5 | :path | /index.html |
6 | :scheme | http |
7 | :scheme | https |
8 | :status | 200 |
9 | :status | 204 |
这些条目在所有的 HTTP/2 连接中都是相同的。使用这些静态表,客户端和服务器可以通过索引直接引用这些头部字段,而不必每次都发送完整的字段和值。
动态表
动态表是一个可变的、由客户端和服务器动态构建的表格,用于存储双方在当前连接中曾经出现的头部字段。动态表允许客户端和服务器交换它们之前使用过的头部,以减少重复的传输。
每个连接会有一个自己的动态表,并且这个表随着请求和响应的进展不断更新。动态表中的每个条目都有一个索引,这个索引可以被用来引用已经传输过的头部字段。
在 HTTP/2 中,动态表的作用是存储当前连接中已发送过的头部字段及其值。由于每个客户端和服务器之间的交互是独立的,且每个连接有自己的状态,每个客户端的动态表都是独立管理的,并且它不需要与其他客户端的动态表进行交互。
例如,某个客户端发送了以下头部字段:
User-Agent: Mozilla/5.0
Accept-Language: en-US
这些字段将被加入到客户端的动态表中,以后客户端可以通过索引引用这些字段。
HPACK 执行流程
接下来我们将使用一张图来理解一下 HPACK 的工作细节,这里将详细地讲解了它的整个压缩和解压的大致过程,如下图所示:
它的大致工作流程如下:
编码(发送方)
-
发送方从原始头部列表中提取每个字段。
-
先查静态表,能匹配的直接用索引。
-
静态表无匹配时,再查动态表,找到就用动态表的索引。
-
如果两个表都没有匹配:以字面量形式传输 Name 和 Value 并决定是否将此字段加入动态表。
-
将所有字段压缩后,生成一个头部块发送给接收方。
解码(发送方)
-
接收方收到头部块后,逐项解析字段。
-
如果是索引字段,直接从静态表或动态表查找内容。
-
如果是字面量字段,直接提取 Name 和 Value,并根据指令决定是否加入动态表。
-
动态表根据需要更新,删除超出容量的字段。
-
解码完成后,重建完整的头部字段列表。
这种机制通过增量更新动态表的内容并灵活选择字段是否加入动态表,极大减少了头部的传输开销。
二进制分帧
HTTP/2
厉害的地方在于将 HTTP/1.1
的文本格式改成二进制格式传输数据,极大提高了 HTTP
传输效率,而且二进制数据使用位运算能高效解析。
它把 TCP
协议的部分特性挪到了应用层,把原来的 Header+Body
的消息打散为数个小片的二进制帧(Frame)
,用 HEADERS
帧存放头数据、DATA
帧存放实体数据,如下图所示:
又或者可以像下图那样:
流
HTTP/2
中的流是帧的逻辑容器。每个流都有一个唯一的流标识符,用于区分不同的流。流可以用于承载请求和响应,并支持双向通信。通过流的并发和优先级管理,可以实现更高效的数据传输。
因为 流
是虚拟的,实际上并不存在,所以 HTTP/2
就可以在一个 TCP
连接上用 流
同时发送多个碎片化的消息,这就是常说的多路复用,多个往返通信都复用一个连接来处理。
多路复用
在 HTTP/2
中,有了二进制分帧之后,HTTP/2
不再依赖 TCP
连接去实现多流并行了。
HTTP/2
通过多路复用机制实现在单个 TCP
连接上并发处理多个请求和响应。这种能力大大提高了性能和效率,以下是关于 HTTP/2
多路复用的详细解释以及实现原理:
-
多路复用的概念: 在传统的
HTTP/1.1
协议中,每个请求都需要建立一个独立的TCP
连接,导致了连接的创建、维护和关闭等开销增加,虽然它们在同一个TCP
连接上,但它们仍然是按照请求的顺序进行处理。也就是说,如果前面的请求还没有响应返回,后续的请求需要等待。而HTTP/2
通过在一个TCP
连接上同时处理多个流来实现多路复用,使得多个请求和响应可以同时在同一个连接上进行传输; -
帧和流的关系: 在
HTTP/2
中,数据被分割为一个个小的帧,然后由服务器和客户端之间交换。每个帧都包含一个特定的类型和标识符,以及帧的有效载荷。多个帧组成了一个流Stream
,每个流都有唯一的流标识符用于标识; -
帧的优先级: 在发起请求时,客户端可以为每个请求设置优先级。服务器可以根据这些优先级来决定处理顺序,以确保重要或紧急的请求可以尽早得到处理。这种方式可以提供更好的资源管理和性能控制;
-
帧的交错发送: 在
TCP
连接上传输帧时,帧可以以任意的顺序进行交错发送。这意味着不同流的帧可以混合在一起发送,而无需等待之前的流完成。这样就能够充分利用带宽,并显著减少延迟; -
流级别的流量控制: 为了防止某个流过多占用连接资源,
HTTP/2
引入了流级别的流量控制机制。每个流都有自己的发送窗口和接收窗口,控制着数据的传输速率。发送方发送的帧大小受到接收方窗口大小的限制,从而实现了流量的平衡;
通过多路复用,HTTP/2
克服了传统 HTTP/1.1
中串行传输的限制,允许多个请求和响应同时在单个 TCP
连接上进行传输。这样可以降低建立和维护连接的开销,减少了网络延迟和资源占用,提高了性能和效率。通过与流量控制和优先级机制的结合,确保了对带宽和资源的有效管理,提供了更好的用户体验。
在 HTTP/2
连接中,流标识符是全局唯一的,每个帧都会包含一个头部,其中包括了流标识符字段,表示该帧属于哪个流。通过流标识符,接收方能够将属于同一流的帧组装在一起,实现对请求和响应的解复用。
服务器推送
HTTP/2
引入了服务器推送机制,它允许服务器在客户端请求之前主动将额外的资源推送给客户端,提前缓存可能需要的资源,从而减少延迟和提高性能。
服务器接收到客户端的请求后,会解析请求并识别出请求所需的资源。服务器根据请求所需的资源,主动将相关的资源推送给客户端。服务器会生成一个新的帧,其中包含被推送资源的信息,并通过同一连接发送给客户端。推送的资源可以是 HTML
、CSS
、JavaScript
、图片等。
安全
出于兼容的考虑,HTTP/2
延续了 HTTP/1.1
的明文特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。
但由于 HTTPS
已经是大势所趋,而且主流的浏览器 Chrome
、Firefox
等都公开宣布只支持加密的 HTTP/2
,所以事实上的 HTTP/2
是加密的。也就是说,互联网上通常所能见到的 HTTP/2
都是使用 https
协议名,跑在 TLS
上面。
虽然 HTTP/2.0
在设计上极大地减轻了队头阻塞问题,但并不意味着完全消除了这个问题。在一些特定情况下,仍然可能存在队头阻塞,例如当某些请求占用了大量带宽或传输时间,导致后续请求等待较长时间。不过相对于 HTTP/1.x
,HTTP/2.0
显著改善了性能和效率,提供更快的页面加载速度和更高的并发性。
HTTP/2 的缺点
虽然 HTTP/2
解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP
协议造成的。HTTP/2
的缺点主要有以下几点。
队头阻塞
虽然 HTTP/2.0
在设计上极大地减轻了队头阻塞问题,但并不意味着完全消除了这个问题。在一些特定情况下,仍然可能存在队头阻塞,例如当某些请求占用了大量带宽或传输时间,导致后续请求等待较长时间。不过相对于 HTTP/1.x
,HTTP/2.0
显著改善了性能和效率,提供更快的页面加载速度和更高的并发性。
只能说 HTTP/2
解决了 HTTP
的队头阻塞问题,但是并没有解决 TCP
队头阻塞问题!
HTTP/2
废弃了管道化的方式,而是创新性的引入了帧、消息和数据流等概念。客户端和服务器可以把 HTTP
消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。因为没有顺序了,所以就不需要阻塞了,就有效的解决了 HTTP
对头阻塞的问题。
TCP 传输过程中会把数据拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。
但是如果其中的某一个数据包没有按照顺序到达,接收端会一直保持连接等待数据包返回,这时候就会阻塞后续请求。这就发生了 TCP
队头阻塞。
HTTP/1.1
的管道化持久连接也是使得同一个 TCP
链接可以被多个 HTTP
使用,,但是 HTTP/1.1
中规定一个域名可以有 6
个 TCP 连接。而 HTTP/2
中,同一个域名只是用一个 TCP
连接。
所以,在 HTTP/2
中,TCP
队头阻塞造成的影响会更大,因为 HTTP/2
的多路复用技术使得多个请求其实是基于同一个 TCP
连接的,那如果某一个请求造成了 TCP
队头阻塞,那么多个请求都会受到影响。
总结
最后我们用一张图来看看 HTTP/1
、HTTPS
和 HTTP/2
的协议栈,你应该就可以对这个知识点有更好的理解了。