HTTP 灵魂之问,巩固你的 HTTP 知识体系
Category(分类): NET Protocol Status: 未知
本文尽量保留原文从 HTTP 报文、请求方法、URI、状态码、缓存、跨域、TLS 和 HTTP/2 逐层展开的结构,同时依据 RFC 9110、RFC 9112、RFC 8446、RFC 9113、Fetch Standard 和 Cookie 规范修正过时或错误内容。
作为一个 Web 开发者,HTTP 几乎是天天要打交道的东西,但很多人对 HTTP 只是浅尝辄止,对更多细节和原理了解不深,面试时就容易感觉吃力。这篇文章用于帮助建立较完整的 HTTP 知识体系,也帮助理解浏览器、代理、TLS 和 HTTP/2 的关系。

001. HTTP 报文结构是怎样的?
对于 TCP 来说,传输的数据可以抽象为 TCP 首部和数据部分。对于 HTTP/1.1,也可以把一条消息理解为:
起始行 + 首部字段 + 空行 + 内容(可选)
这里的“内容”在新规范中通常称为 content,旧资料也常称为实体(entity)或消息体(body)。并不是每种请求或响应都允许携带内容,例如 HEAD 响应不能有响应体,204、304 等响应通常也不能有响应体。
HTTP/2 和 HTTP/3 在网络上传输的是二进制帧,不再使用 HTTP/1.1 的文本起始行和空行分隔,但它们仍然表达相同的 HTTP 方法、目标、状态码、首部和内容语义。
起始行
对于 HTTP/1.1 请求报文,请求行类似:
GET /home HTTP/1.1
它由以下部分组成:
方法 SP 请求目标 SP HTTP 版本 CRLF
响应报文的起始行叫状态行:
HTTP/1.1 200 OK
状态行通常包含 HTTP 版本、三位状态码和原因短语。原因短语主要用于人类阅读,客户端不应依赖具体文字;状态码才是机器判断结果的主要依据。
HTTP/1.1 的行结束符是 CRLF,即 \r\n,不能笼统写成任意平台的换行符。HTTP/2、HTTP/3 没有这样的文本起始行,而是使用伪首部字段,例如 :method、:path、:status。
首部字段


请求和响应都可以携带大量首部字段。HTTP 首部字段的基本语法和注意事项包括:
- 字段名大小写不敏感;
- 字段名必须符合
token语法; - 字段名后面不能有空格再写冒号,冒号应紧跟字段名;
- 字段值冒号后通常允许可选空白;
- 下划线并不是 HTTP
field-name语法禁止的字符,但部分服务器、代理或网关默认拒绝带下划线的首部,因此应用中应优先使用标准字段名和连字符形式; - 同一个字段是否可以出现多次、多个值如何合并,要根据具体字段规范判断,不能一概拼接。
空行
在 HTTP/1.1 文本报文中,首部结束后会出现一个空行,也就是连续的两个 CRLF:
首部字段\r\n
\r\n
内容
它用于区分首部和内容。如果在首部中间插入一个空行,后续内容可能会被解析为消息内容,或者让不同的代理和服务器产生不同解析结果。利用不一致解析可能导致请求走私,因此服务端必须严格按照 HTTP framing 规则处理。
内容和消息体
内容就是请求或响应携带的实际数据,例如 JSON、HTML、图片或文件。请求内容通常称为请求体,响应内容通常称为响应体。
内容是否存在、长度是多少,不由 GET 或 POST 这几个字母单独决定,而由请求方法语义、状态码和消息 framing 共同决定。HTTP/1.1 通常通过 Content-Length 或 Transfer-Encoding 确定内容边界;HTTP/2/3 使用帧和 Stream 表达边界。
002. 如何理解 HTTP 的请求方法?
有哪些请求方法?
HTTP 方法是可扩展的,不只限于 HTTP/1.1 最常见的几种。常见方法包括:
- GET:获取目标资源的当前表示;
- HEAD:获取与 GET 类似的响应首部,但不返回响应内容;
- POST:让目标资源根据请求内容进行资源特定的处理;
- PUT:用请求内容创建或替换目标资源的表示;
- DELETE:请求删除目标资源;并不是“几乎用不到”,在 REST API 中很常见;
- CONNECT:建立到目标服务器的隧道,常用于代理;
- OPTIONS:查询目标资源或服务器支持的通信选项,也常用于 CORS 预检;
- TRACE:诊断请求经过的路径,生产环境通常会出于安全原因禁用;
- PATCH:对资源进行部分修改,由 RFC 5789 定义。
方法名通常使用大写,但 HTTP 方法在语法上是区分大小写的 token,具体方法规范会定义它的大小写形式。
GET 和 POST 有什么区别?
GET 和 POST 的核心差别是语义,而不是“一个把参数放 URL、另一个把参数放 body”这一条语法限制。
- 安全语义:GET 被定义为 safe 方法,预期不会修改资源状态;POST 默认不是 safe;
- 幂等性:GET 是幂等的,PUT 和 DELETE 也被定义为幂等;POST 默认不是幂等的,但接口可以通过业务设计和幂等键实现重复请求保护;
- 缓存:GET 响应通常可缓存;POST 响应也可能在满足显式条件时缓存,只是实际较少;
- 参数位置:GET 通常把查询参数放在 URI 中,POST 通常把提交内容放在请求体中,但 POST 也可以带 query,GET body 没有 RFC 9110 定义的通用语义,不应依赖;
- 安全性:POST 不会因为 body 不显示在地址栏就自动安全。GET、POST 都需要使用 HTTPS 保护传输,敏感数据也不应放入 URL,因为 URL 可能进入历史记录、访问日志、代理日志和 Referer。
“幂等”表示重复相同请求的预期服务器效果与执行一次相同,不代表每次响应状态码或日志记录都相同。
POST 会分成两个 TCP 数据包吗?
不会。POST 不会因为方法本身就必然产生两个 TCP 数据包。
HTTP 客户端可以选择在发送较大请求内容前使用:
Expect: 100-continue
此时服务端可以先返回:
HTTP/1.1 100 Continue
客户端再发送内容。但这个机制不是 POST 专属,是否使用由客户端、服务器和代理实现决定。TCP 也没有 HTTP Header 和 Body 的边界概念,一次 send() 不对应一个 TCP 包,一次 recv() 也不对应一个 HTTP 请求。
003. 如何理解 URI?
URI(Uniform Resource Identifier,统一资源标识符)用于标识资源。URL(Uniform Resource Locator)和 URN(Uniform Resource Name)可以看作 URI 体系中的不同形式,日常开发中常把 HTTP URL 直接称为网址。
URI 的结构
通用 URI 语法可以抽象为:
scheme://[userinfo@]host[:port]/path?query#fragment

各部分含义如下:
- scheme:方案名,例如
http、https、file、mailto、urn。并不是所有 URI 方案都使用://,这是 HTTP URL 中常见的形式; - userinfo:用户信息,历史上可写成
user:password@,但把密码放入 URI 很不安全,现代应用应避免; - host:主机名或 IP 地址;
- port:端口,HTTP 默认 80,HTTPS 默认 443;
- path:资源路径;
- query:查询参数;
- fragment:客户端片段标识符,通常由浏览器本地处理,不会作为 HTTP 请求目标发送给服务器。
例如:
https://www.baidu.com/s?wd=HTTP&rsv_spt=1#result
其中 https 是 scheme,www.baidu.com 是 host,/s 是 path,wd=HTTP&rsv_spt=1 是 query,#result 是 fragment。
URI 编码
URI 的语法使用 ASCII 范围内的字符表示。非 ASCII 字符以及在当前 URI 组件中具有分隔意义的字符,通常需要先按 UTF-8 编码,再使用百分号编码:
空格 → %20
三元 → %E4%B8%89%E5%85%83
但“所有特殊字符都必须编码”并不准确:哪些字符需要编码取决于它所在的 URI 组件。HTML 表单的 application/x-www-form-urlencoded 还可能把空格编码成 +,这与通用 URI 百分号编码不是完全相同的规则。
004. 如何理解 HTTP 状态码?
HTTP 状态码是三位数字,第一位表示响应类别:
- 1xx:临时信息响应,还需要后续处理;
- 2xx:请求成功;
- 3xx:重定向或缓存相关结果;
- 4xx:当前服务器无法满足请求,通常与请求格式、认证、权限或资源状态有关;
- 5xx:服务器、代理或网关无法完成看起来有效的请求。
不能简单地把 4xx 理解成“客户端代码错误”,也不能把 5xx 理解成“一定是源服务器错误”,因为 CDN、代理和网关也可能生成这些状态码。
1xx
101 Switching Protocols:HTTP/1.1 中服务器同意通过 Upgrade 切换协议时使用,例如经典 WebSocket 握手。HTTP/2 和 HTTP/3 不使用传统的 Connection: Upgrade 流程。
2xx
- 200 OK:请求成功,响应内容取决于方法;HEAD 成功时不能包含响应体;
- 201 Created:请求成功并创建资源,通常可以配合
Location; - 202 Accepted:已接受处理但尚未完成,不代表最终结果一定成功;
- 204 No Content:请求成功但没有响应内容,不能包含响应体;
- 206 Partial Content:满足 Range 请求,通常带有
Content-Range。
3xx
- 301 Moved Permanently:永久重定向。历史客户端可能把 POST 改成 GET,必须保留方法时应考虑 308;
- 302 Found:临时重定向。历史客户端也可能把 POST 改成 GET,必须保留方法时应考虑 307;
- 303 See Other:要求客户端使用 GET 访问另一个 URI;
- 304 Not Modified:条件 GET/HEAD 验证缓存后,表示客户端可以继续使用缓存。304 不包含响应体。
301 是否被浏览器缓存、缓存多久,取决于缓存规则和响应首部,不应简单说“浏览器一定永久缓存”。
4xx
- 400 Bad Request:请求语法、消息 framing 或请求内容无效;
- 401 Unauthorized:缺少有效认证凭据,响应应包含
WWW-Authenticate; - 403 Forbidden:服务器理解请求但拒绝提供服务;
- 404 Not Found:没有找到目标资源,也可能是服务器故意隐藏资源存在性;
- 405 Method Not Allowed:目标资源不支持当前方法,响应应包含
Allow; - 406 Not Acceptable:内容协商无法提供客户端可接受的表示;
- 408 Request Timeout:服务器在等待完整请求时超时;
- 409 Conflict:请求与资源当前状态冲突;
- 413 Content Too Large:请求内容超过服务器处理上限,旧资料称 Payload Too Large;
- 414 URI Too Long:请求目标 URI 过长;
- 415 Unsupported Media Type:请求内容类型不被支持;
- 416 Range Not Satisfiable:Range 范围无法满足;
- 422 Unprocessable Content:语法正确但语义无法处理,旧资料称 Unprocessable Entity;
- 425 Too Early:服务端拒绝处理可能来自 TLS 0-RTT 且可能被重放的请求;
- 429 Too Many Requests:触发限流;
- 431 Request Header Fields Too Large:请求首部过大;
- 451 Unavailable For Legal Reasons:因法律原因无法提供资源。
5xx
- 500 Internal Server Error:服务器遇到未预期的内部错误;
- 501 Not Implemented:服务器不支持完成请求所需的功能;
- 502 Bad Gateway:网关或代理从上游收到无效响应;
- 503 Service Unavailable:服务器暂时无法处理请求,常见原因是过载或维护;
- 504 Gateway Timeout:网关或代理等待上游响应超时。
005. 简要概括 HTTP 的特点和缺点
HTTP 的特点
- 灵活可扩展:方法、状态码、首部字段和媒体类型都可以扩展,但基础语法仍然受到规范约束,并不是“其他部分都没有严格语法限制”;
- 请求—响应模型:传统交互是客户端发请求、服务端发响应。响应可以是流式的,HTTP 还可以配合 SSE、WebSocket 等实现不同的实时能力;
- 语义上的无状态:HTTP 不要求服务器自动保存跨请求的会话上下文。Cookie、Session、Token 等机制可以在应用层建立状态;
- 支持不同传输映射:HTTP/1.1 和 HTTP/2 常见于 TCP,HTTP/3 映射到 QUIC/UDP。HTTP 语义本身不等同于某一个传输协议;
- 支持多种表示:可以传输 HTML、JSON、图片、音视频和任意二进制内容。
HTTP/1.1 在明文 TCP 上可以观察到文本报文,但 HTTP/2 使用二进制帧,HTTP/3 使用 QUIC。HTTP 是否加密由 TLS 等机制决定,不是由“文本还是二进制”决定。
HTTP 的缺点
无状态
在需要保存大量会话上下文的场景中,每次请求都需要携带认证和状态信息,可能增加开销;但无状态也便于缓存、扩展和故障转移,因此不能简单地说无状态只有缺点。
明文传输
普通 http:// 通信没有 TLS 保护,网络旁观者可能读取或修改报文。Wi-Fi 中间人、恶意热点、代理和被攻陷的网络设备都可能造成风险。HTTPS 使用 TLS 提供机密性、完整性和服务器身份认证,但不能解决服务端漏洞、客户端恶意软件和钓鱼域名等问题。
队头阻塞
- HTTP/1.1 管线化和同一连接的请求—响应顺序会造成应用层队头阻塞;
- HTTP/2 解决了部分 HTTP 层队头阻塞,但多个 Stream 共享 TCP,仍存在 TCP 层 HOL;
- HTTP/3 使用 QUIC 独立 Stream,减少跨 Stream 的传输层 HOL,但单个 Stream 内仍保持有序。
006. 对 Accept 系列字段了解多少?
这里需要区分“请求方告诉服务端自己愿意接受什么”和“服务端描述实际返回了什么”。HTTP 内容协商常见字段包括:
- Accept:客户端可接受的响应媒体类型;
- Content-Type:当前请求或响应内容的媒体类型;
- Accept-Encoding:客户端支持的内容编码;
- Content-Encoding:当前内容已经使用的编码;
- Accept-Language:客户端偏好的自然语言;
- Content-Language:当前内容使用或面向的自然语言;
- Accept-Charset:历史上的字符集协商字段,现代浏览器很少发送,不应作为主要字符集协商机制;
- charset 参数:通常放在
Content-Type的媒体类型参数中,例如text/html; charset=utf-8。
数据格式
MIME(Multipurpose Internet Mail Extensions)最初用于电子邮件,HTTP 借用了媒体类型体系来描述 body 的格式。常见媒体类型包括:
text/html、text/plain、text/css;image/gif、image/jpeg、image/png;audio/mpeg、video/mp4;application/json、application/pdf、application/octet-stream。
例如:
Accept: application/json, text/plain;q=0.8
Content-Type: application/json; charset=utf-8
Accept 是偏好,不代表服务端一定能提供;服务端可以选择合适表示,也可能返回 406 或忽略部分偏好。
压缩方式
发送方在响应中使用 Content-Encoding 描述已经应用的内容编码,客户端通过 Accept-Encoding 表示支持的编码:
Accept-Encoding: gzip, br
Content-Encoding: br
常见值包括:
gzip;deflate;br(Brotli);identity(不编码)。
Content-Encoding 不是描述原始媒体类型的字段,媒体类型仍由 Content-Type 表示。
语言
Accept-Language: zh-CN, zh;q=0.9, en;q=0.8
Content-Language: zh-CN
Accept-Language 是客户端偏好,Content-Language 描述实际表示的语言。它们不是“接收方和发送方必须完全相同的语言列表”。
字符集
现代 Web 通常直接使用 UTF-8,并在媒体类型参数中声明:
Content-Type: text/html; charset=utf-8
Accept-Charset 已经很少在浏览器中使用,服务端不应把它当作唯一字符集协商依据。

007. 对于定长和不定长的数据,HTTP 是怎么传输的?
定长内容
对于 HTTP/1.1 定长内容,服务端通常通过 Content-Length 指明内容的字节数:
Content-Length: 10
一个简化的 Node.js 示例:
const http = require('http')
const server = http.createServer((req, res) => {
if (req.url === '/') {
const body = 'helloworld'
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
'Connection': 'close'
})
res.end(body)
return
}
res.writeHead(404, { 'Content-Length': 0 })
res.end()
})
server.listen(8081, () => {
console.log('成功启动')
})
Content-Length 写小了,客户端可能只读取声明的字节,剩余内容可能被丢弃或导致协议错误;写大了,客户端会等待不存在的字节,通常导致超时、连接异常或内容长度不匹配。具体表现取决于 Node.js、浏览器、代理和连接状态,不能把某一次浏览器表现当作通用规则。

不定长内容与 chunked
在 HTTP/1.1 中,如果服务端生成响应内容时无法提前知道完整长度,可以使用:
Transfer-Encoding: chunked
chunked 编码让响应按多个 chunk 发送,每个 chunk 的结构大致是:
chunk-size(十六进制)\r\n
chunk-data\r\n
...
0\r\n
可选 trailer\r\n
\r\n
Transfer-Encoding: chunked 与 Content-Length 不能同时作为同一消息的有效 framing。不能像原文示例那样同时手动设置两者。chunked 可以用于动态、流式响应,但它本身不等于 HTTP 长连接,也不保证客户端一定持续等待很久。
HTTP/2 和 HTTP/3 不使用 HTTP/1.1 的 chunked transfer coding,它们使用帧和 Stream 表达数据边界。
Node.js 动态响应示例:
const http = require('http')
const server = http.createServer((req, res) => {
if (req.url !== '/') {
res.writeHead(404, { 'Content-Length': 0 })
res.end()
return
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Connection': 'close'
})
res.write('<p>来啦</p>\n')
setTimeout(() => {
res.write('第一次传输<br/>\n')
}, 1000)
setTimeout(() => {
res.end('第二次传输')
}, 2000)
})
server.listen(8009, () => {
console.log('成功启动')
})
在 HTTP/1.1 中,Node.js 可能自动选择 chunked framing;生产代码应让框架正确管理 Content-Length、Transfer-Encoding 和连接复用,不要同时手动设置互相冲突的首部。


008. HTTP 如何处理大文件的传输?
对于几百 MB 甚至几 GB 的大文件,一次性传输会导致较长等待和失败重试成本。HTTP 可以使用 Range 请求,让客户端只获取资源的一部分。
如何表示支持范围请求
服务端可以通过响应首部表示支持字节范围:
Accept-Ranges: bytes
Accept-Ranges: none 表示不支持范围请求。没有 Accept-Ranges 并不一定代表服务器绝对不支持,客户端仍需根据实际响应判断。
Range 字段拆解
请求首部使用如下格式:
Range: bytes=start-end
常见写法:
bytes=0-499:第 0 到第 499 个字节,共 500 字节;bytes=500-:从第 500 个字节到文件结尾;bytes=-100:文件最后 100 个字节。
原文中把 500 和 100 单独写成范围是不完整的,必须带上 -。
如果 Range 合法且服务器决定满足请求,返回 206 Partial Content,并通过 Content-Range 描述范围:
Content-Range: bytes 0-9/100
如果所有请求范围都无法满足,返回 416 Range Not Satisfiable,通常可以返回:
Content-Range: bytes */100
服务器也可以忽略 Range,直接返回完整资源和 200 OK。
单段数据
HTTP/1.1 206 Partial Content
Content-Type: text/plain
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
0123456789
多段数据
多段范围使用 multipart/byteranges:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
0123456789
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
abcdefghij
--00000010101--
这里的 boundary 用于分隔各段数据,结束边界以 -- 结尾。实际响应还应正确处理 CRLF、Content-Length、缓存验证和条件范围请求。
009. HTTP 中如何处理表单数据的提交?
HTML 表单常见的编码方式包括:
application/x-www-form-urlencoded;multipart/form-data。
表单也可以使用 GET,此时数据通常进入 URL query;POST、PUT、PATCH 等方法则常把数据放在请求体中。
application/x-www-form-urlencoded
数据通常编码成 & 分隔的键值对:
a=1&b=2
键和值中的特殊字符按表单规则编码,空格常被编码为 +,其他字符使用百分号编码。例如:
{a: 1, b: 2} → a=1&b=2
原文把 a=1&b=2 整体编码成 a%3D1%26b%3D2,这不是通常的表单键值对编码方式;= 和 & 作为结构分隔符通常保留,具体键和值再分别编码。
multipart/form-data
multipart/form-data 适合文件上传和同时提交文本、二进制字段。请求头中的 Content-Type 会带有 boundary 参数:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRRJKeWfHPGrS4LKe
请求体大致如下:
------WebKitFormBoundaryRRJKeWfHPGrS4LKe\r\n
Content-Disposition: form-data; name="data1"\r\n
\r\n
data1\r\n
------WebKitFormBoundaryRRJKeWfHPGrS4LKe\r\n
Content-Disposition: form-data; name="data2"\r\n
\r\n
data2\r\n
------WebKitFormBoundaryRRJKeWfHPGrS4LKe--\r\n
每个 part 有自己的首部和内容,boundary 只用于分隔 part。boundary 一般由用户代理或 multipart 库生成,服务端必须按实际首部解析,不能写死某个浏览器的 boundary。
对文件上传来说,multipart/form-data 可以避免把二进制文件整体转换为 URL 编码文本。它并不代表没有开销,仍然会增加每个 part 的首部和分隔符。
010. HTTP/1.1 如何缓解 HTTP 的队头阻塞问题?
什么是 HTTP 队头阻塞?
HTTP/1.1 的持久连接允许复用连接,但同一连接上的请求和响应受到顺序约束。管线化场景中,如果队首请求响应很慢,后面的响应可能等待,这就是 HTTP 层队头阻塞。
HTTP/1.1 非管线化请求也会受到连接复用和请求顺序的限制,但浏览器通常通过多个连接并发请求来缓解。
并发连接
早期 HTTP/1.1 资料常说同一域名最多 2 个连接,但这是早期浏览器实现策略,不是 RFC 规定的固定上限。现代浏览器对 HTTP/1.1 通常允许更多连接,但连接数仍受浏览器、代理、服务器和操作系统限制。
增加连接数可以把请求分摊到多个 TCP 队列,但会带来:
- 更多 TCP/TLS 握手;
- 更高的连接和文件描述符开销;
- 多条 TCP 连接竞争带宽;
- 更多慢启动和拥塞控制状态。
域名分片
早期网页常使用 content1.example.com、content2.example.com 等多个域名,让浏览器创建更多 HTTP/1.1 连接。这叫域名分片(domain sharding)。
在 HTTP/2/HTTP/3 中,多个域名可能破坏连接复用、增加 DNS/TLS 成本,通常不再推荐。现代优化应优先使用 HTTP/2/3、合理资源优先级、缓存和 CDN,而不是盲目增加域名。
011. 对 Cookie 了解多少?
Cookie 简介
HTTP 的语义默认不保存跨请求的会话状态,但 Web 应用经常需要登录态、购物车或偏好设置。Cookie 是浏览器管理的一组小型键值数据,浏览器会根据域名、路径、安全属性和 SameSite 规则决定是否发送。
服务端通过 Set-Cookie 响应首部设置 Cookie,客户端请求时通过 Cookie 首部发送符合条件的 Cookie:
Cookie: a=xxx; b=xxx
Set-Cookie: a=xxx; Path=/; Secure; HttpOnly
Set-Cookie: b=xxx; Max-Age=3600
Cookie 的属性不是普通 Cookie 请求首部的一部分,而是在 Set-Cookie 中由服务端设置。
生存周期
Expires:绝对过期时间;Max-Age:从接收开始计算的相对秒数;- 没有这两个属性的 Cookie 通常是会话 Cookie,但浏览器可能因会话恢复而保留更久。
过期或被删除的 Cookie 不会再按正常规则发送。
作用域
Domain:决定可发送到哪些域名。省略时通常是 host-only Cookie;设置 Domain 时不能随意跨越公共后缀;Path:决定请求路径是否匹配。Path=/通常覆盖该 Cookie 域名下的路径,但不等于跨域名共享。
安全属性
Secure:只通过安全连接发送,通常即 HTTPS。它不能阻止 JavaScript 访问;HttpOnly:阻止 JavaScript 通过document.cookie读取 Cookie,但 Cookie 仍会随符合条件的 HTTP/HTTPS 请求发送,不能理解为“只能通过 HTTP 协议传输”;SameSite:控制跨站请求中的 Cookie 发送,常用于降低 CSRF 风险;__Host-、__Secure-等前缀还可以施加额外约束。
SameSite
SameSite 的判断基于 site,而不是简单的 origin,因此同站但跨源的情况需要区别对待。
Strict:更严格地限制跨站场景携带 Cookie;Lax:现代浏览器常见默认值,通常允许同站请求以及部分顶级跨站安全导航,例如 GET 导航;None:允许跨站发送,但现代浏览器通常要求同时设置Secure。
原文说 None 是默认模式已经过时。现代浏览器通常把未设置 SameSite 的 Cookie 按 Lax 或 Lax-allow-unsafe 等兼容策略处理,具体还与浏览器版本有关。
SameSite 不是完整的 CSRF 防护。修改状态的请求仍应结合 CSRF Token、Origin/Referer 校验和合理的权限控制。
Cookie 的缺点
- 容量和数量有限:常见实现约为每个 Cookie 4KB,但这不是所有浏览器和服务器都严格相同的统一上限;
- 请求开销:符合域名和路径条件的 Cookie 会随请求发送,可能浪费带宽。应合理设置 Domain、Path,静态资源还可以使用无 Cookie 域名;
- 安全风险:没有 HTTPS 时 Cookie 可能被窃听;没有 HttpOnly 时可能被脚本读取;即使设置了 HttpOnly,XSS 仍可能利用当前登录态发起请求;
- 隐私和合规风险:第三方 Cookie、追踪和跨站使用还受到浏览器策略和隐私法规影响。
012. 如何理解 HTTP 代理?
HTTP 通常采用请求—响应模型。引入代理后,代理对客户端表现为服务器,对源服务器表现为客户端,因此具有中间人的双重角色。
代理类型
- 正向代理:帮助客户端访问客户端直接访问不到或不希望直接访问的服务器;
- 反向代理:代表一组源服务器接收客户端请求,再转发给合适的后端。

反向代理的常见功能
- 负载均衡:按照轮询、随机、一致性哈希等策略分配请求;
- 健康检查:通过探活或主动检查发现故障实例并暂时摘除;
- 缓存代理:缓存可共享的响应,减少源站压力;
- TLS 终止:在代理处完成 TLS,再以 HTTP 或 HTTPS 连接后端;
- 访问控制和限流:过滤来源、限制请求速率、隐藏源站拓扑。
Via
Via 是标准 HTTP 首部,用于记录请求或响应经过的代理。例如:
Via: 1.1 proxy1, 1.1 proxy2
代理通常在 Via 链中追加自己的信息。具体顺序取决于消息经过的方向和各代理的处理方式,不能只根据一张示意图推断所有响应顺序。
X-Forwarded-For
X-Forwarded-For 是事实上的常用字段,不是最初 HTTP 核心规范中的统一认证机制。它通常包含逗号分隔的客户端地址和各级代理地址,例如:
X-Forwarded-For: client-ip, proxy1-ip, proxy2-ip
应用只能信任由自己控制的可信代理写入或追加的部分,不能直接相信来自公网客户端的 X-Forwarded-For,否则攻击者可以伪造来源 IP。
X-Real-IP、X-Forwarded-Host 和 X-Forwarded-Proto
这些通常是代理约定的非标准字段:
X-Real-IP:常被代理用于传递一个客户端地址,但不保证一定是真实地址;X-Forwarded-Host:原始 Host;X-Forwarded-Proto:客户端到代理使用的协议,例如http或https。
现代部署也可以使用标准化的 Forwarded 首部。无论使用哪种字段,都必须建立可信代理边界,避免开放代理直接覆盖安全相关信息。
PROXY Protocol
当代理在 TCP 层转发连接而又需要保留原始客户端地址时,可以使用 PROXY Protocol。它不是 HTTP 首部,也不是“写在 HTTP 请求行上面”的 HTTP 扩展,而是连接建立后、HTTP 数据之前发送的一段代理元信息。
PROXY Protocol v1 示例:
PROXY TCP4 192.0.2.10 198.51.100.20 12345 443\r\n
GET / HTTP/1.1\r\n
Host: example.com\r\n
PROXY Protocol v2 使用二进制格式。发送方和接收方必须同时配置支持,直接把 PROXY 头暴露给普通 HTTP 服务会造成解析错误或安全问题。
HTTPS 场景下,如果代理只做 TLS 透传,就不能修改加密后的 HTTP 首部;如果代理终止 TLS,它可以解析和重新生成后端 HTTP,但这意味着客户端到代理与代理到源站是两个安全区间,需要分别保护。
013. 如何理解 HTTP 缓存及缓存代理?
浏览器缓存的基本流程
浏览器通常先根据 Cache-Control、Expires 等信息判断缓存是否仍然新鲜:
- 如果缓存新鲜,可以直接使用,不发起验证请求;
- 如果缓存过期,可以使用
If-None-Match或If-Modified-Since发起条件请求; - 资源没有变化时,服务端返回
304 Not Modified,浏览器继续使用已有内容; - 资源发生变化时,服务端返回
200等最终响应和新内容。
ETag 通常比 Last-Modified 更精确,但缓存行为还受到 Vary、Authorization、Set-Cookie、Cache-Control 和代理策略影响。
代理缓存
客户端缓存失效时,如果每次都直接请求源服务器,源站压力会很大。共享缓存、反向代理或 CDN 可以把可共享响应缓存到离客户端更近的位置。
缓存代理的行为由源服务器首部、客户端请求首部和代理自身策略共同决定,并不是简单的“客户端缓存过期就一定从代理拿”。
源服务器的缓存控制
private 和 public
Cache-Control: private
表示响应主要只允许单用户缓存,不应由共享缓存存储。常用于个性化或包含用户隐私的响应。
Cache-Control: public
表示在满足其他缓存条件时允许共享缓存存储。public 并不意味着内容一定会被缓存,也不代表可以忽略敏感信息保护。
must-revalidate 和 proxy-revalidate
must-revalidate:缓存过期后不能随意使用陈旧响应,必须按规则重新验证;proxy-revalidate:对共享缓存施加类似要求,主要影响共享缓存。
它们都不是简单的“客户端缓存过期”或“代理缓存过期后直接到源服务器”的流程开关。
s-maxage
s-maxage 用于共享缓存,规定共享缓存中的新鲜时间,通常会优先于 max-age;max-age 更普遍地描述缓存的新鲜时间。
例如:
Cache-Control: public, max-age=1000, s-maxage=2000
可以理解为客户端和共享缓存采用不同的新鲜时间,但最终行为还要结合其他缓存首部。
客户端的缓存控制
max-stale 和 min-fresh
它们是请求中的 Cache-Control 指令,不是独立的冒号首部:
Cache-Control: max-stale=5
表示客户端允许使用最多陈旧 5 秒的响应;不带数值的 max-stale 表示允许任意陈旧程度,但代理可以有自己的限制。
Cache-Control: min-fresh=5
表示客户端希望响应至少还保持 5 秒新鲜,而不是“必须在到期前 5 秒请求”。
only-if-cached
Cache-Control: only-if-cached
表示客户端只接受缓存结果,不希望缓存代理联系源服务器。如果缓存无法满足,缓存代理通常返回 504 Gateway Timeout,但具体行为取决于缓存实现。
其他注意事项
Vary决定缓存是否需要按请求首部区分不同表示;- 带用户身份的响应通常需要谨慎处理共享缓存;
Set-Cookie、Authorization 和私有数据可能阻止共享缓存或改变缓存策略;- 缓存命中、回源、重验证和 CDN 命中需要结合响应首部与代理日志判断。
014. 什么是跨域?浏览器如何拦截响应?如何解决?
什么是同源和跨源
同源通常要求以下三项都相同:
scheme(协议) + host(主机) + port(端口)

不同源的页面受到浏览器同源策略限制,例如通常不能直接读取对方 DOM、Cookie、IndexedDB 和 LocalStorage,也不能任意读取跨源 Fetch/XHR 的响应内容。
但“跨源请求被浏览器完全阻止”并不准确:
- 图片、脚本、表单等跨源能力有各自规则;
- CORS simple request 可能会真实发送并在服务端产生副作用,只是浏览器不把响应内容交给脚本;
- CORS 主要控制脚本是否能读取响应,不是服务端 CSRF 防护机制。
浏览器内部过程


现代 Chromium 通常由渲染进程、浏览器进程和 Network Service/网络进程等组件共同完成请求。不同版本的进程模型、IPC 框架和站点隔离策略会变化,不能把某一版本的 Chromium 源码结构当作所有浏览器的通用实现。

一般过程可以理解为:页面脚本发起 Fetch/XHR,浏览器根据安全策略、CORS、凭据和网络权限发出请求;网络响应返回后,浏览器判断是否满足 CORS 规则,满足才把响应暴露给脚本。
CORS
CORS(Cross-Origin Resource Sharing,跨源资源共享)现在主要由 Fetch 标准定义。浏览器和服务端共同参与:浏览器发送 Origin,服务端通过响应首部声明允许哪些来源、方法、首部和凭据。
Simple request
满足特定条件的请求可以直接发送,不需要先发预检。常见条件包括:
- 方法为 GET、HEAD 或 POST;
- 请求首部使用 CORS-safelisted 的首部字段;
Content-Type为application/x-www-form-urlencoded、multipart/form-data或text/plain;- 没有触发其他需要预检的条件。
这类请求也会带上 Origin。如果响应没有匹配的 Access-Control-Allow-Origin,浏览器可能仍然让请求到达服务端,但不会把响应内容交给脚本。
服务端常见响应:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Origin 不能填写一个逗号分隔的任意来源列表;需要开放凭据时通常必须返回具体 Origin,不能与 * 同时使用。
凭据
前端可以通过 withCredentials 或 Fetch 的 credentials: 'include' 请求发送 Cookie、HTTP 认证等凭据:
const response = await fetch('https://api.example.com/user', {
credentials: 'include'
})
服务端需要返回:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Credentials 的有效值是字符串 true。同时还要受到 Cookie 的 SameSite、Secure、Domain 和 Path 规则影响。
Access-Control-Expose-Headers
浏览器默认只向脚本暴露 CORS-safelisted response headers,例如 Cache-Control、Content-Language、Content-Length、Content-Type、Expires、Last-Modified 和 Pragma。如果脚本要读取其他响应首部,服务端可以声明:
Access-Control-Expose-Headers: X-Request-Id, ETag
然后脚本才能通过 response.headers.get() 或 XHR API 读取符合规则的字段。
Preflight 预检
如果请求方法、首部或内容类型不满足 simple request 条件,浏览器会先发送 OPTIONS 预检:
OPTIONS /user HTTP/1.1
Origin: https://app.example.com
Host: api.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
预检响应示例:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Access-Control-Max-Age 的单位是秒,但浏览器可能设置上限,不能保证严格缓存任意长时间。预检失败时,浏览器通常不会发送真正的请求;即使预检通过,真正请求的响应仍需要满足 CORS 暴露规则。
原文示例中的 Access-Control-Allow-Credentials: trueAccess-Control-Max-Age 缺少换行,并且 Access-Control-Allow-Origin: * 与凭据不能组合,已经修正。
JSONP
由于浏览器允许 <script src="跨源地址"> 加载脚本,早期应用通过服务端返回函数调用来实现 JSONP:
function jsonp({ url, params = {}, callbackName = 'callback' }) {
return new Promise((resolve, reject) => {
const name = `${callbackName}_${Date.now()}`
const query = new URLSearchParams({ ...params, callback: name })
const script = document.createElement('script')
const cleanup = () => {
delete window[name]
script.remove()
}
window[name] = data => {
cleanup()
resolve(data)
}
script.onerror = () => {
cleanup()
reject(new Error('JSONP request failed'))
}
script.src = `${url}?${query}`
document.head.appendChild(script)
})
}
服务端返回类似:
res.type('application/javascript')
res.send(`${callback}(${JSON.stringify(data)})`)
JSONP 只支持 GET,而且返回内容会作为 JavaScript 执行,存在代码注入、回调名污染和敏感数据泄露风险。现代项目应优先使用 CORS,不能把 JSONP 作为通用安全跨域方案。
Nginx 反向代理
Nginx 可以把前端同源路径反向代理到后端,从浏览器角度避免跨源:
server {
listen 80;
server_name client.example.com;
location /api/ {
proxy_pass http://server.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
正向代理帮助客户端访问目标服务器,反向代理代表后端服务器接收客户端请求。反向代理方案不等于后端没有安全校验,后端仍应进行认证、授权、CSRF 防护和输入校验。
postMessage 可以在不同窗口之间安全传递经过验证的消息;WebSocket 也可以跨源建立,但服务端应校验 Origin 和权限。它们不属于普通 HTTP CORS 响应读取机制。
015. TLS 1.2 握手过程是怎样的?
HTTP 本身可以是明文传输。HTTPS 不是一个全新的应用层方法,而是 HTTP over TLS:
HTTPS = HTTP over TLS
SSL 2.0 和 SSL 3.0 已经废弃。TLS 1.0、TLS 1.1 也已被弃用,现代环境主要使用 TLS 1.2 和 TLS 1.3。
TLS 不应简单对应到 OSI 会话层。它是独立的安全协议层,为上层应用提供加密、完整性保护和身份认证。
传统 RSA 密钥传输
旧式 TLS 1.2 RSA 密钥传输套件中,客户端生成 premaster secret,并使用服务端证书中的 RSA 公钥加密后发送,服务端用私钥解密。这种方式不具备前向保密,现代 TLS 不再使用 RSA 作为密钥传输机制。
TLS 1.2 仍可以使用 RSA 作为身份认证/签名算法,例如 ECDHE_RSA;这和 RSA 密钥传输不是一回事。
TLS 1.2 ECDHE 握手
常见的 TLS 1.2 套件示例:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
可以拆解为:
ECDHE:使用临时椭圆曲线 Diffie-Hellman 进行密钥协商;RSA:服务端使用 RSA 证书私钥进行签名认证;AES_128_GCM:使用 AES-128-GCM 保护记录数据;SHA256:TLS 1.2 套件相关的 PRF/握手摘要算法。
GCM 是 AEAD 模式,不应只解释为普通的“分组模式”。

典型流程如下:
- ClientHello:客户端发送支持的 TLS 版本、随机数、密码套件、扩展和 ECDHE
key_share/参数; - ServerHello:服务端选择版本、密码套件,并发送自己的随机数和 ECDHE 参数;
- Certificate:服务端发送证书链,客户端验证证书链、域名、有效期和用途;
- ServerKeyExchange:服务端发送 ECDHE 参数,并使用证书对应私钥签署参数和握手上下文;客户端用证书公钥验证签名;
- ServerHelloDone:服务端表示本轮握手消息发送完成;
- ClientKeyExchange:客户端发送自己的临时 ECDHE 公钥;
- 双方分别使用自己的临时私钥和对方临时公钥计算相同的 ECDHE 共享秘密,再通过 TLS 的密钥派生过程生成会话密钥;
- 客户端发送
ChangeCipherSpec和Finished;服务端验证后发送自己的ChangeCipherSpec和Finished; - 双方验证 Finished 后开始传输加密的应用数据。
ECDHE 不是把 pre_random 加密后传输,而是让双方通过各自临时密钥计算共享秘密。证书数字签名也不是“私钥加密摘要、公钥解密摘要”这么简单的普通加密过程:服务端使用私钥对规定的握手内容签名,客户端使用公钥验证签名。
客户端认证
默认 TLS 握手通常只认证服务端。若需要服务端认证客户端,应由服务端发送 CertificateRequest,客户端再发送客户端证书和 CertificateVerify。原文把普通 ECDHE 的 client_params 说成客户端身份认证是不准确的。
Finished 和 Change Cipher Spec
Finished 用于验证双方是否基于相同的握手 transcript 和密钥派生结果完成握手。TLS 1.2 的 ChangeCipherSpec 表示切换到协商好的保护状态。
TLS 1.3 中 ChangeCipherSpec 主要是为了兼容旧中间设备的兼容性消息,不能把它当作 TLS 1.3 中真正的密钥切换机制。
TLS False Start
TLS False Start 是部分客户端在满足特定密码套件、协议和安全条件时,提前发送应用数据的优化,不是所有 TLS 1.2 连接都会使用,也不是 TLS 握手的必经步骤。
016. TLS 1.3 做了哪些改进?
TLS 1.3 于 2018 年标准化,相比 TLS 1.2 主要改进了密码套件、握手往返次数、前向保密和早期数据机制。
强化安全
TLS 1.3 初始规范定义的主要密码套件包括:
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256
实践中最常见的是前 3 个。TLS 1.3 密码套件只描述对称 AEAD 算法和哈希算法,密钥交换与身份认证算法通过其他扩展和握手消息协商。
TLS 1.3 删除了 RSA 静态密钥传输、静态 DH、旧式弱密码和不安全散列组合,但 RSA 并没有从 TLS 1.3 中完全消失:RSA 证书仍可以用于服务端签名认证,只是不再用于直接加密传输 premaster secret。
原文以 FREAK 作为 RSA 被移除的主要原因并不准确。TLS 1.3 的设计目标是删除历史遗留的弱算法和不具备前向保密的密钥传输,并简化密码套件协商。
ECDHE 使用每次连接生成的临时密钥对。如果服务端长期证书私钥日后泄露,攻击者通常不能仅凭过去捕获的握手数据恢复历史会话密钥,这种性质称为前向保密。若临时密钥、终端或运行时在会话期间已经被攻破,前向保密不能保护该会话。
握手改进

TLS 1.3 的典型完整握手通常是 1-RTT:
- 客户端发送 ClientHello,包含支持的版本、密码套件、扩展和 ECDHE
key_share; - 服务端发送 ServerHello,之后发送加密的 EncryptedExtensions、Certificate、CertificateVerify 和 Finished;
- 客户端验证服务端证书和签名后发送 Finished;
- 双方进入应用数据阶段。
TLS 1.3 在 ServerHello 后就可以派生握手阶段密钥,所以服务端可以更早发送加密握手消息。握手具体消息可能因证书、客户端认证、密钥更新和扩展而变化。
会话恢复和 PSK
TLS 1.2 的历史会话恢复包括 Session ID 和 Session Ticket。Session ID 通常需要服务端维护会话状态;Session Ticket 则把经过服务端保护的恢复信息交给客户端保存。具体实现可能是有状态或无状态的,不能简单假设所有 Session Ticket 都由客户端独立解密。
TLS 1.3 主要使用 PSK(Pre-Shared Key)进行会话恢复。服务端在握手后通过 NewSessionTicket 提供恢复票据,客户端下次连接时使用 PSK binder 证明其拥有相关密钥材料。票据保护密钥需要轮换和安全管理,但票据密钥泄露的影响不应简单描述成“直接解密所有历史 TLS 流量”。
会话恢复通常可以减少完整握手的计算和延迟,但是否能减少到特定 RTT 取决于连接建立、网络和客户端行为。
0-RTT Early Data
TLS 1.3 允许客户端在使用恢复 PSK 时发送 0-RTT early data,从而在收到服务端完整握手确认前发送一部分应用数据。
0-RTT 的核心风险是重放:攻击者可能复制并再次发送早期数据。它不是因为攻击者能够直接解密 PSK,而是因为同一早期数据可能在协议允许的窗口内被重复处理。因此 0-RTT 不应承载支付、下单、转账、修改密码等不可重放的操作,服务端应采用幂等设计和重放防护。
0-RTT 也不等于整个 TLS 和 HTTP 请求绝对零延迟,它只是允许应用数据提前发送,服务端仍需完成后续握手确认。
017. HTTP/2 有哪些改进?
HTTP/2 在保留 HTTP 方法、URI、状态码和首部语义的前提下,主要引入:
- 二进制分帧;
- 多路复用;
- HPACK 首部压缩;
- Stream 和 Connection 流量控制;
- Stream 重置;
- 可选服务器推送;
- 历史优先级机制,以及当前推荐的可扩展 HTTP Priority。
头部压缩
HTTP/1.1 也可以通过其他方式压缩内容,但没有 HPACK 这样的标准连接级首部压缩。HTTP/2 使用 HPACK 静态表、动态表、整数编码和可选 Huffman 编码减少重复首部传输。

HTTP/2 没有完全“废除起始行”,而是把 HTTP/1.1 起始行中的方法、目标和状态转换为伪首部字段:
:method
:path
:scheme
:authority
:status
它们与普通首部字段有不同的语法和出现顺序要求。
多路复用与 HTTP 队头阻塞
HTTP/1.1 的并发连接和域名分片只是分摊队列,没有从协议层改变请求—响应顺序。HTTP/2 将请求和响应拆成不同 Stream 的帧,允许不同 Stream 的帧交错传输。
但必须区分两种 HOL:
- HTTP 层 HOL:HTTP/2 通过 Stream 多路复用减少了不同请求之间的互相等待;
- TCP 层 HOL:所有 HTTP/2 Stream 仍共享一条 TCP 字节流,TCP 丢包可能阻塞整条连接。
HTTP/2 的帧并不是完全乱序到达:不同 Stream 可以交错,同一 Stream 内的帧必须有序。HEADERS、DATA、CONTINUATION 等帧还必须遵守各自的协议状态。
服务器推送
HTTP/2 Server Push 允许服务器通过 PUSH_PROMISE 提前声明并发送客户端可能需要的资源。客户端可以通过 SETTINGS 禁用,也可以通过 RST_STREAM 取消。
这项功能仍存在于协议规范中,但主流浏览器已经禁用或移除实际支持,且错误预测会浪费带宽。现代网页通常优先使用 preload、103 Early Hints、缓存和 CDN。
安全和传输
HTTP/2 规范不强制 TLS,但浏览器通常通过 TLS ALPN 使用 HTTP/2;明文 h2c 在浏览器环境中并不常见。HTTP/2 使用 TCP,HTTP/3 则基于 QUIC/UDP,不能把两者的队头阻塞特性混为一谈。

018. HTTP/2 中的二进制帧是如何设计的?
帧结构

每个 HTTP/2 帧由 9 字节帧头和帧载荷组成:
- 前 3 字节是帧长度,表示帧载荷长度;
- 第 4 字节是帧类型;
- 第 5 字节是帧标志,具体含义由帧类型定义;
- 后 4 字节包含保留位和 31 位 Stream ID。
帧不是简单分为“数据帧和控制帧”两大类的正式统一枚举,但可以做这种入门层面的归类:DATA 用于内容,HEADERS 用于首部,SETTINGS、PING、GOAWAY、WINDOW_UPDATE、RST_STREAM 等用于连接或 Stream 管理。
常见标志包括:
END_STREAM:表示该端在这个帧之后不再发送该 Stream 的内容;它是帧标志,不是单独的 END_STREAM 帧;END_HEADERS:表示首部块结束,适用于 HEADERS 或 CONTINUATION;PADDED、PRIORITY等:是否存在和含义由具体帧类型决定。
Stream 的状态变化

HTTP/2 Stream 状态机包括 idle、open、half-closed (local)、half-closed (remote) 和 closed 等状态。
以普通请求—响应为例:
- 客户端发送 HEADERS,创建一个新的客户端发起 Stream;
- 如果 HEADERS 没有
END_STREAM,客户端还可以发送 DATA; - 客户端发送带
END_STREAM的 HEADERS 或 DATA 后,表示客户端方向结束,客户端进入half-closed (local),但仍可接收服务端响应; - 服务端收到客户端方向结束后,从服务端视角进入
half-closed (remote),仍可以发送响应; - 服务端发送带
END_STREAM的 HEADERS 或 DATA 后,服务端方向也结束,Stream 最终进入closed; - 任一端发送
RST_STREAM,也可能让 Stream 直接终止。
一条 Stream 的关闭是两个方向分别结束的过程,不能简单说“客户端发一个 END_STREAM 帧、服务器再发一个 END_STREAM 帧”。END_STREAM 是附着在某个 HEADERS 或 DATA 等帧上的标志。
Stream ID 是 31 位数值,最高位保留,因此可表示的数值范围是 0 到 2^31-1。Stream ID 0 保留给连接级帧,客户端发起的 Stream 通常使用奇数,服务器发起的 Stream 通常使用偶数。已使用的 Stream ID 不会重新使用,并不意味着普通应用会真的用到 21 亿个 Stream;达到实现或协议限制时,连接会通过 GOAWAY 等机制逐步关闭,客户端再建立新连接。
Stream 的特性
- 并发性:一个 HTTP/2 连接可以同时存在多个 Stream;
- 有序性:同一 Stream 内的帧遵循协议顺序;
- 不可复用 ID:Stream ID 不会重新分配给另一个 Stream;
- 双向或受限方向性:普通请求 Stream 可以双向传输,服务端推送等场景具有不同的建立和使用规则;
- 流量控制:同时受到 Stream 级和 Connection 级窗口限制;
- 优先级:旧的依赖树/权重机制已被 RFC 9113 弃用,新系统应参考 RFC 9218,但服务端是否遵循优先级仍由实现决定。
总结
HTTP 是一个可扩展的请求—响应协议族,现代 Web 开发不能只记住“文本、TCP、GET/POST”这些早期印象,还需要理解:
- HTTP/1.1 报文 framing 和持久连接;
- HTTP 方法的 safe、幂等和缓存语义;
- URI 编码、状态码和内容协商;
- Range、chunked 和表单编码;
- Cookie、代理、缓存和 CORS 的安全边界;
- TLS 1.2 ECDHE、TLS 1.3、会话恢复和 0-RTT;
- HTTP/2 二进制帧、HPACK、多路复用和 TCP HOL;
- HTTP/3、QUIC、QPACK 以及跨 Stream 队头阻塞的改进。