HTTP之二:HTTP Headers

August 10, 2021

数据类型与编码

MIME type

全称“多用途互联网邮件扩展”(Multipurpose Internet Mail Extensions), 用于标记 body 的数据类型, 形式是 type/subtype 的字符串, 常用的有:

  • text: text/html text/css text/plain
  • image: image/gif image/jpeg image/png
  • audio/video: audio/mpeg video/mp4
  • application: application/json application/javascript application/pdf application/octet-stream

数据编码/压缩

  • gzip: GNU zip, 对于文本数据通常超过 60%压缩率
  • deflate: zlib
  • br: 专为 HTTP 优化的新压缩算法 Brotli, 通常比 gzip 还好

数据类型相关 Headers

  • 请求头: 客户端声明希望/能够接收的数据类型

    • Accept: text/html, application/json, image/png, image/webp
    • Accept-Encoding: gzip, deflate, br
  • 响应头: 服务端告知实际发送的数据类型

    • Content-Type: text/html
    • Content-Encoding: gzip

国际化 i18n

主要分为语言和字符集:

  • 请求头

    • Accept-Language: zh-CN, zh, en
    • Accept-Charset: utf-8, gbk
  • 响应头

    • Content-Language: zh-CN
    • Content-Type: text/html; charset=utf-8
    • 字符集在响应头中没有单独的字段, 而是作为 Content-Type 的后半部分

Accept-CharsetContent-Language 通常不会发送.

优先级

可以在 Accept Accept-Encoding Accept-Language 等请求头字段中的内容中添加 q 参数来设定优先级, 如 Accept: text/html, application/xml;q=0.9 .

优先级默认以及最大为 1, 最小为 0.01.

注: 在此处 ; 的断句小于 , , 表示仅作用于分号之前的一个内容.

响应头中的 Vary

表示服务器在内容协商时参考的请求头字段, 如:

Vary: Accept-Encoding,User-Agent,Accept

请求头变化时, Vary 也可能变化, 主要用于在传输链路中间的代理服务器实现缓存服务, 后文 缓存代理 部分详述.

大文件传输

分块传输/Transfer-Encoding

  • 响应头: Transfer-Encoding: chunked

    • Content-Length 字段互斥, 要么长度已知, 要么是 chunked(长度未知).
    • Transfer-Encoding 字段值可以是 gzip deflate 等, 表示压缩编码; 与 Content-Encoding 相比, Transfer-Encoding 在传输后将被自动解码.
  • 每个分块内容:

    • length + CRLF
    • chunked data + CRLF
  • 分块末尾

    • 0 + CRLF (length 为 0)
    • CRLF (空行结尾)
    • 末尾允许有拖尾数据 Trailer

范围请求/Range

获取文件的一部分, 如视频进度拖拽、多线程并发下载、断点续传.

服务器在响应头中添加 Accept-Ranges: bytes 来告知客户端支持范围请求.

客户端在请求头中添加 Range: bytes=x-y 来进行范围请求.

  • x、y 表示从 0 开始的偏移量
  • x、y 可省略其中之一, 但 - 不能省略, 如: bytes=10- bytes=-20

服务器接收到 Range 字段后,

  1. 检查范围是否合法, 不合法返回 416;
  2. 若范围合法, 根据 Range 头偏移量读取文件片段, 确定状态码 206(Partial Content);
  3. 服务器添加响应头字段 Content-Range: bytes x-y/length , length 表示总长度;
  4. 通过 TCP 发送数据

Range 是针对原文件的范围, 而不是压缩后的文件范围(除非原文件就是 gzip 的).

条件范围请求 If-Range 在后文详述.

多段数据

Range 请求头中添加逗号分隔的多段范围, 如: Range: bytes=0-9, 20-29.

响应头中多段数据得到的数据 MIME 类型为 multipart/byteranges , 同时需要给出段之间的 boundary=xxx , 如: Content-Type: multipart/byterange; boundary=00000000001.

具体的数据如下

  • 以双“-”开头的分隔符: --{boundary} + CRLF
  • 数据类型: Content-Type: type/subtype + CRLF
  • Range: Content-Range: types x-y/length + CRLF
  • 空行 CRLF
  • 分段数据 multipart data + CRLF
  • (下一段数据)分隔符: --{boundary} + CRLF
  • ...其他数据
  • 分段结束: --{boundary}-- + CRLF

连接管理/Connection

短连接和长连接

HTTP 0.9/1.0 使用短连接, 收到响应后立刻关闭连接, 效率很低:

  • TCP 三次握手建立连接 → 请求 → 响应 → TCP 四次握手断开连接

HTTP/1.1 默认使用长连接, 在一次连接中收发多个请求-响应.

  • 请求头和响应头加 Connection: keep-alive 表示支持长连接
  • 请求头和响应头加 Connection: close 告知对方本次通信后关闭连接
  • 请求头和响应头加 Keep-Alive: timeout=value 限定超时时间(约束力不强,可能不遵守)
  • Nginx 可以设置 keepalive_timeoutkeepalive_request 规定一次连接的超时时间和最大请求数
  • Connection 的另一个值 Upgrade 配合状态码 101, 表示协议升级, 如 HTTP 升级为 WebSocket
  • HTTP 还有第三种连接方式 “pipeline”, 但主流浏览器都未实现, 事实上“废弃”了.

队首阻塞问题

HTTP 规定的“请求-响应”模型是先进先出的串行队列, 即 同域名 下收到前一次的响应后才能发起新请求, 一次收发过慢时会导致后续所有请求被阻塞.

  • 利用 HTTP 长连接特性对服务器发起大量请求, 导致服务器耗尽资源, 就是 DDoS .

由于 HTTP/1.1 的“请求-应答”模型不能变, 只能通过数量来暂时“解决”质量问题:

  • 并发连接数: 浏览器支持 6~8 个同域并发长连接
  • 域名分片: 使用多个域名指向同一台服务器

重定向和跳转/Location

当状态码 301/302 时为重定向, 响应头 Location 标记重定向 URL.

Location 支持绝对和相对 URI, 如 Location: /index.html .

循环跳转: 几个 URI 连续重定向导致无限循环, 浏览器可以检测循环跳转并提示失败.

Refresh 延时重定向, 如 Refresh: 5; url=xxx 表示 5 秒后跳转.

RefererReferrer-Policy 表示浏览器跳转来源, 用于统计分析或防盗链.

3xx 状态码重定向由浏览器执行, 对于服务器来说是“外部重定向”, 服务器的“内部重定向”是在服务器中直接跳转 URI.

重定向相关状态码

  • 301: 永久重定向, 会更新浏览器历史记录和书签;
  • 302: 临时重定向, 仅本次访问有效;
  • 其他状态码, 浏览器和服务器不一定支持:

    • 303 See Other: 类似 302, 但要改为 GET 方法避免重复操作;
    • 307 Temporary Redirect: 类似 302, 要求重定向后的方法和实体不变;
    • 308 Permanent Redirect: 301 + 307, 永久重定向且方法和实体不变.
    • 300 Multiple Choices: 返回一个有多个链接选项的页面, 用户自行选择跳转

服务器在响应头添加 Set-Cookie: key=value , 给予客户端身份标识, 响应头可具有多个 Set-Cookie .

客户端保存 Cookie 并在之后的请求头中添加 Cookie: key1=value1; key2=value2 , 请求头的 Cookie 将多个 Cookie 合并为 ; 分隔的形式.

Cookie 最大 4K.

Cookie 不属于 HTTP 标准, 所以语法上与别的属性不一致(分隔符是分号而非逗号).

一个真实 Cookie: Set-Cookie: favorite=humburger; Max-Age=10; Expires=Fri, 07-Jun-19 08:19:00 GMT; Domain=www.xxx.com; Path=/; HttpOnly; SameSite=Strict

有效期:

  • Expires 过期的时间点
  • Max-Age 有效时间(秒数)
  • 浏览器优先采用 Max-Age , 如果不指定有效期, 则为会话 Cookie 关闭失效.

作用域:

  • Domain 域名
  • Path 路径, 使用 / 表示该域名下任意路径
  • 浏览器会进行对比, 如果请求 URI 不相符则不会发送 Cookie.

安全性:

  • HttpOnly 禁止非 HTTP 协议方式访问,禁用 document.cookie 相关 API.
  • SameSite 防范“跨站请求伪造”(XSRF)攻击

    • SameSite=Strict 限定 Cookie 不能随着跳转链接跨站发送
    • SameSite=Lax 允许 GET/HEAD 等安全方法, 禁止 POST 跨站发送
  • Secure 该 Cookie 只能用 HTTPS 协议加密传输, 但 Cookie 本身在浏览器中还是明文存在

防范 XSS(跨站脚本攻击): HttpOnly 禁止 JS 脚本访问 Cookie; Secure 只在 HTTPS 时加密传输 Cookie.

  • 身份识别: 登录信息、会话事务等
  • 广告追踪: 用户被广告商加上 Cookie, 实现精准推送

Cache-Control

服务器端的缓存控制

服务器在响应头中添加 Cache-Control 控制浏览器缓存资源, 如 Cache-Control: max-age=30 .

  • max-age 有效期(秒), 从响应报文的创建时刻开始计算.
  • no_store 不允许缓存
  • no_cache (语义相反)可以缓存, 需在服务器验证缓存是否有新版本

    • 相当于 max-age=0, must_revalidate
  • must-revalidate 缓存未过期可使用, 过期需验证

浏览器端的缓存控制

不使用缓存数据:

  • 刷新按钮: 浏览器在请求头中添加 Cache-Control: max-age=0 , 表示越过缓存获取新数据;
  • 强制刷新(Ctrl+F5 或 Command+R): 请求头添加 Cache-Control: no-cache , 通常与刷新效果相同, 取决于服务器处理.

使用缓存数据的情况: 请求头中无 Cache-Control 属性

  • 前进/后退 按钮
  • 重定向跳转

条件请求

先验证缓存是否有效, 若无效再请求新数据 需要两次请求, 成本过高. 所以 HTTP 定义了一系列 If 开头的条件请求字段来合并数据验证和请求.

在首次请求中, 服务器在响应头中添加 Last-ModifiedETag 属性

通过缓存的更新时间:

  • 浏览器在请求头添加 If-Modified-Since 属性
  • 若缓存无更新, 服务器在响应头添加 Last-Modified 属性并返回"304 Not Modified", 浏览器更新缓存有效期

通过 ETag(资源标识):

  • 浏览器在请求头添加 If-None-Match: "xxxx"
  • 若本资源无更新则返回"304 Not Modified"并更新有效期
  • ETag 有"强" "弱"之分, 以 "W/"开头的为弱 ETag, 只要求资源在语义上无变化, 但内部可能发生变化; 强 ETag 要求资源在字节级别完全相符.

Proxy

Via 在请求和响应头可以出现, 没经过一个代理就追加一个代理主机名/域名, 如 Via: proxy1, proxy2 .

  • 有的响应报文会使用 X-Via , 与 Via 含义相同

两个常用的事实上的标准(非 HTTP 标准):

  • X-Forwarded-ForVia 类似, 但追加的是请求方的 IP 地址.

    • X-Forwarded-Host 记录主机名
    • X-Forwarded-Proto 记录协议名
  • X-Real-IP 记录客户端 IP 地址, 无中间代理信息.

抓包含代理的通信过程

  • 客户端与代理的 80 端口通过三次握手建立 TCP 连接;
  • 客户端向代理发送 HTTP 请求, 代理接收并返回 ACK 确认;
  • 代理与源服务器通过三次握手建立 TCP 连接;
  • 代理向源服务器发送 HTTP 请求, 源服务器接收并返回 ACK 确认;
  • 源服务器向代理发送 HTTP 响应, 代理接收并返回 ACK 确认;
  • 代理与源服务器通过四次挥手断开 TCP 连接;
  • 代理将响应数据发给客户端, 客户端接收并返回 ACK 确认.

代理协议

由于 X-Forwarded-For 要解析和修改 HTTP Headers, 会影响性能, 在加密通信中甚至无法实现, 所以出现了一个事实标准 代理协议/The PROXY protocol .

代理协议有 v1 和 v2 两个版本:

  • v1: 明文, 在 HTTP 报文前增加一行 ASCII 码文本:

    PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
    GET / HTTP/1.1\r\n
    ...
    
    • 必须 PROXY 开头, 放在 HTTP 报文首行之前
    • TCP4 表示客户端的 IP 地址类型
    • 之后是请求方地址, 应答方地址, 请求方端口, 应答方端口号
    • 最后与 HTTP 报文一样以回车换行结束
  • v2: 非明文二进制格式, 相对复杂可自行查阅.

Cache Proxy

处于客户端和源服务器之间, 作为数据的中转站.

面向源服务器时是客户端, 面相客户端时是服务器.

服务端的缓存控制

Cache-Control 响应头中可以对客户端和代理的缓存策略进行配置.

首先是四个基本属性 max-age no_store no_cache must-revalidate 可以通过添加 privatepublic 约束客户端和代理的缓存策略:

  • private 表示客户端私有缓存, 如用户信息 Cookie.
  • public 表示客户端与代理都可以进行缓存

其次, Cache-Control 还可以添加 must-revalidateproxy-revalidate 属性, 用于说明在缓存失效时必须回源服务器验证还是只验证代理即可.

s-maxage 用于单独标识代理端的缓存生效时间, 与客户端的 max-age 类似.

no-transform 代理专用属性, 表示禁止对数据进行优化处理(一些代理可能会对某些数据进行一些优化处理).

注意: 服务器设置完 Cache-Control 后要添加 Last-modifiedEtag 响应头才能使用条件请求.

客户端的缓存控制

Cache-Control 部分讲述的的浏览器缓存控制对代理同样有效, 即通过 max-age no_store no_cache 属性作用于源服务器和代理来进行缓存控制.

还有两个属性用于声明客户端对缓存有效期的偏移量限定:

  • max-stale 可接受缓存过期 x 秒
  • min-fresh 缓存必须在当前以及 x 秒后未过期

only-if-cached 属性表示客户端只接受代理缓存的数据, 如果代理没有缓存或缓存过期, 就返回 504.

缓存数据匹配

i18n 部分提到过 Vary 字段, 表示服务器在内容协商时参考的请求头字段, 可以作为报文的一个版本标记. 如: Vary: Accept-Encoding,User-Agent,Accept , 缓存代理会存储这些版本. 当再次收到相同请求时, 代理读取缓存的 Vary , 匹配时缓存数据.

缓存清理

常用的做法是使用自定义请求方发 PURGE 来告诉代理服务器删除本 URI 对应的缓存数据.

需要进行缓存清理的场景: 缓存数据过期; 源服务器存在新数据; 一些无用或有害数据.


Profile picture

佚树 的个人博客

关于前端、音乐与生活