老胡茶室
老胡茶室
Beta

HTTP Header 中的黑科技

冯宇

简单认识 HTTP 协议

HTTP 协议是一种基于 TCP 的纯文本协议,一个基本的 HTTP 协议交互过程如下:

使用 nc 命令演示以及 mock 最基本的 http server 交互响应过程:

终端 1:

nc -kl 8080

终端 2:

curl -v localhost:8080

终端 1 结果类似如下:

GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.75.0
Accept: */*

此时,我们在终端 1 手工输入一个模拟 http server 的响应:

HTTP/1.1 200 OK

Hello

此时,在终端 2 应该看到类似于下面的响应:

< HTTP/1.1 200 OK
<
Hello

HTTP 的协议的请求内容格式如下:

GET / HTTP/1.1          # <======== 第一行为 method location protocol
Host: localhost:8080    ######################
User-Agent: curl/7.75.0 ## 一些可选的 headers
Accept: */*             ######################
                        # 一个空行
{body}                  # 可选的 request body

HTTP 协议的响应内容格式如下:

HTTP/1.0 200 OK                          # 第一行固定 protocol code description
Server: SimpleHTTP/0.6 Python/3.9.1      ################
Date: Sun, 14 Feb 2021 09:24:59 GMT      ## 一些可选的 headers
Content-type: text/html; charset=utf-8   ##
Content-Length: 496                      ################
                                         # 一个空行
{body}                                   # 响应正文内容

通常描述 REST 接口的文档中,直接省略请求过程以及非关键的 Header 部分,贴出关键部分的请求命令和 header,以及 body 部分,以一个登录 API 为例,可能看到的接口文档描述如下:

客户端请求:

POST /login
Content-Type: application/json

{"usernamae":"u1","password":"p1"}

服务端响应:

200 OK
Content-Type: application/json

{"token":"a1b2c3..."}

常见 HTTP Code 总结

  • 1xx: 信息响应
    • 100 Continue,这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。
    • 101 Switching Protocol,该代码是响应客户端的 Upgrade 标头发送的,并且指示服务器也正在切换的协议。
  • 2xx:成功响应
    • 200 OK
    • 201 Created
    • 204 No Content
    • 206 Partial Content,部分成功,断点续传必备
  • 3xx:重定向
    • 301 Moved Permanently,注意所有的请求都将被定向为 GET
    • 302 Found,与 301 最直观的区别是 302 请求不会被搜索引擎收录
    • 304 Not Modified,见下文缓存部分
    • 307 Temporary Redirect,与 302 唯一的区别在于 method 保持原样
    • 308 Permanent Redirect,与 301 唯一的区别在于 method 保持原样
  • 4xx: 客户端错误
    • 400 Bad Request
    • 401 Unauthorized
    • 403 Forbidden
    • 404 Not Found
    • 405 Method Not Allowed
  • 5xx: 服务端失败
    • 500 Internal Server Error
    • 501 Not Implemented
    • 502 Bad Gateway,常见于反向代理
    • 503 Service Unavailable,常见于云加速服务
    • 504 Gateway Timeout,常见于反向代理

参考资料: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status

HTTP 协议的版本的主要演进

HTTP/1.0 增加以下内容:

  • 增加协议版本号,如 GET HTTP/1.0, POST HTTP/1.1
  • 引入 header 内容,无论请求成功与否都可以添加 header,增加灵活性
  • 增加 Content Type 支持,以便 http 协议传输更多更丰富的内容

HTTP/1.1 增加以下内容:

  • 连接可以复用,增加 keepalive 支持
  • 支持 pipeline,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。
  • 支持分块编码技术(chunked)
  • 引入额外缓存机制(基于相对时间缓存的 cache-control 头)
  • 引入内容协商头(Accept-*头)
  • 引入 Host 头,以便支持虚拟主机

HTTP/2 主要改进:

  • 不再基于纯文本,而是使用二进制(但第一次协商为了向下兼容 HTTP/1.1 依旧使用纯文本)
  • 是一个复用协议。并行请求和响应在同一个链接完成
  • 压缩 headers,节省传输成本

HTTP/3(QUIC)主要改进(目前仍是草案状态):

  • 基于 UDP 协议,废弃 TCP 协议

参考资料:

HTTP header 中的黑科技实例

基于域名的虚拟主机(Host 头)

通过 client 添加 Host 头(通常不需要用户干预,命令部分和 Host 头部分通常客户端会自动处理)以及服务端响应 Host 头,可以实现同一个服务器上提供多个网站的的场景。例:

GET / HTTP/1.1
Host: localhost:8080

内容协商

内容协商的方式为客户端请求带上 Accept 类的头(尽管 User-Agent 不是标准的内容协商内容,但是实际开发中很多人还是会使用 User-Agent 作为协商依据),服务端会根据这个头响应客户端期望的内容以及对应的 header(header 也可能没有,而是直接返回对应期望的内容或者重定向到目标页面)。

常见的 Accept 对与示例:

客户端 Request服务端 Response(可能没有)
accept: application/json, text/htmlcontent-type: application/json
accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7Content-Language: zh-CN
accept-encoding: br, gzip, defletecontent-encoding: gzip
Vary: Accept-Encoding

Vary 会影响下级服务端或用户浏览器的缓存策略。vary 头表示服务端基于哪个请求头做了内容协商(可能没有),再简单一点就是因为什么头的内容不同而响应内容不同。Vary 头可以防止缓存错乱,但是滥用会导致缓存命中率下降,因此通常不推荐 Vary: *。实际使用常见的 Vary: Accept-EncodingUser-AgentOrigin

跨源资源共享 CORS(仅浏览器)

原理和过程部分强烈建议阅读 MDN 文档: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

客户端请求头:

OPTIONS /resource/foo     # fetch 请求通常使用 OPTIONS 命令
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

服务端期望的响应头:

HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

下载文件(仅浏览器)

打开链接下载文件需要满足以下二选一:

  • content-type 非浏览器能直接支持的,或者为默认的 application/octet-stream
  • 响应头中包含 Content-Disposition: attachment,无论 content-type 是什么,都将变为文件下载,如果 Content-Disposition 的值包含 filename=for.bar,则默认下载文件名为指定 filename 的值。例:
Content-Disposition: attachment;filename=download.pdf

文件下载带进度提醒

响应头中包含 Content-Length,则客户端可以根据这个文件长度提醒下载进度

多线程下载/断点续传

服务端必须支持 Accept-Ranges 响应,大部分情况下这个值是 bytes

Accept-Ranges: bytes

客户端请求带上 Range:(unit=first byte pos)-[last byte pos],如:

Range: bytes=0-499

服务端应该返回:

206 Partial Content
Content-Range: bytes 0-1023/146515

大数据量传输/流式传输(chunked)

对于大容量数据、动态数据、流式数据这种不可提前预知容量(content-length)的内容,应当采用 chunked 方式。它可以方便处理动态内容,以及动态维持客户端链接。客户端通常会对 chunked 进行流式处理。一个典型的 chunked 响应如下:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

具体的 chunked 部分格式如下:

[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]

缓存(通过 header 控制)

HTTP 的缓存可以针对浏览器,也可以针对中间的代理层(如 CDN 等)。Vary 头会影响缓存效果(下不赘述)。

HTTP 协议的缓存结构可以参考下图(来自: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ ):

What a cache provide, advantages/disadvantages of shared/private caches.

更灵活的本地缓存策略可以考虑上 service worker: https://developers.google.cn/web/ilt/pwa/caching-files-with-service-worker

哪些响应可以被缓存:

  • 通常只有 GET 可以被浏览器缓存,而 OPTIONSHEAD 可以被 CDN 缓存(可能需要配置)
  • 响应码为 200, 206, 301, 404

常见缓存头:

Expires(Since HTTP/1.0)

  • Expires: <http-date>: Expires: Thu, 01 Dec 1994 16:00:00 GMT
  • 本地时间到达指定时间后缓存会过期,会重新发起 HTTP 请求
  • 本地时间如果不同步会影响缓存效果

Last-Modified / If-Modified-Since(Since HTTP/1.0)

  • Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
  • If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
  • 使用服务端时间,因此本地时间不同步时不影响缓存效果
  • 资源未过期服务端应该返回 304 Not Modified

Etag / If-Non-Match(Since HTTP/1.1)

  • ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  • If-Non-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"
  • 和本地时间无关,本地时间不影响缓存效果
  • 资源未过期服务端应该返回 304 Not Modified
  • 使用 CDN 及分布式存储的场景通常不建议使用 Etag 头

Cache-Control(Since HTTP/1.1)

  • cache-control: max-age=3600,public
  • max-age 字段表示自获取资源之后,在本地缓存多久(单位:秒)
  • public/private 代表这个是公共还是私有缓存,公共缓存是可以被所有中间层缓存的,私有缓存只能在本地浏览器中缓存。包含 max-age 参数或包含 Expires 头的时候,默认为 public,否则默认 private
  • cache-control 头可以同时包含在请求头与响应头中

如果以上头在响应中都包含,并且客户端均支持,那么优先级如下: Cache-Control > Expires > Etag > Last-Modified

缓存控制

Cache-Control(Since HTTP/1.1)

  • no-store:不使用任何缓存,每次重新发起请求,下载最新资源
  • no-cache:每次重新验证,如果服务端未更新返回 304 Not Modified

Pragma: no-cache(Since HTTP/1.0)

  • cache-control: no-cache 效果相同
  • 不能代替 cache-control,只是向后兼容 HTTP/1.0 的缓存层和客户端

缓存 FAQ

  • 浏览器直接地址栏输入和 F5 以及 CTRL+F5 有什么区别?

    • 地址栏直接输入相比普通请求无任何区别,如果缓存还未过期,不会发起请求
    • 使用 F5/CTRL+R 刷新页面会在请求头加上 cache-control: max-age=0,强行发起缓存验证,会保留 If-Modified-Since/If-Non-Match 请求头,如果服务端的资源未更新,返回 304 Not Modified。而页面其他相关联的请求(如 js, css, 图片资源等)不做任何特殊处理。
    • 使用 CTRL+F5/CTRL+SHIFT+R 刷新页面会在请求头带上 cache-control: no-cache 和 pragma: no-cache,同时会去掉 If-Modified-Since/If-Non-Match 请求头,完全不使用本地缓存,强行从服务端重新获取资源,成功应该返回 200 OK。页面其他相关请求做同样缓存过期处理。这个效果几乎就等同于开发者工具中 Network-Disable Cache 勾选上的效果
  • 如何判断浏览器/CDN 的缓存资源是否过期?

    • 如果资源由缓存服务器或 CDN 响应,通常还同时会返回一个 Age: <seconds>头表示资源在服务器上缓存了多久(单位:秒),可以根据这个头判断最近资源是否有更新
    • 通过 Last-Modified 头判断
  • 如何判断响应后端是真实的服务端还是 CDN?

    • 通过 Server 头判断。绝大多数自建服务器的 Server 可能为 Apache, Nginx, Tomcat 之类的,而 CDN 通常不直接使用这些 Server
    • 绝大多数 CDN 通常会带上一个 Via 头,可以通过这个头的内容判断是否来自于 CDN,以及使用哪个 CDN
  • 如何判断是否命中 CDN 的缓存?

    • CDN 厂商通常都会添加一些自定义的头: x-****,这些信息用来帮助用户调试 CDN。多数 CDN 厂商会把是否命中缓存放在 x-cache 这个头。如阿里云 CDN:x-cache: HIT TCP_MEM_HIT dirn:11:115947616,Cloudfront: x-cache: Hit from cloudfront

参考资料: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ

强制 HTTPS

强制 HTTPS 通常有两种方案:重定向和 HSTS

HSTS(HTTP Strict Transport Security)是一个特殊的 http header: Strict-Transport-Security: max-age=<expire-time>,如:

Strict-Transport-Security: max-age=31536000;includeSubDomains

用于告诉浏览器必须使用 HTTPS 访问指定资源。二者比对如下:

重定向(浏览器通常有上限次数限定)HSTS
适用性所有客户端(必须开启 follow redirect)仅现代化的浏览器
强制性强制。不跳转到 Location 无法获取资源非强制。旧的 URL 依旧可用
额外请求多一次服务端请求内部重定向,无额外请求
非标准端口支持不支持

参考资料: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/HTTP_Strict_Transport_Security

基于 HTTP/1.1 之上的协议

HTTP/1.1 协议可以通过升级方式使用基于 HTTP/1.1 的高级协议,如 WebsocketWebDAV 等等。升级方式为添加以下两个头:

  • Connection: Upgrade
  • Upgrade: protocols

升级成功服务端应该返回 101 Switching Protocols,并且之后的交互则使用高级协议规范进行交互。如果服务端不支持该升级协议,则应该返回 200 OK,之后由客户端继续按照 HTTP 协议降级处理。

简易图片防盗链(仅浏览器)

图片防盗链一种比较简单的防护策略是通过 Referer 头进行防护。假想你的网页代码中引入图片的部分如下:

<img src="pic.jpg" />

那么浏览器请求这个图片资源的时候,通常还会附带上当前的地址栏到 Referer 头,请求示例如下:

GET /pic.jpg HTTP/1.1
Host: localhost:8080
referer: http://localhost:8080/

服务端可以通过设置 Referer 头白名单的方式一定程度上实现防盗链。