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      ## 一些可选的 header
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
    
  • 2xx
    :成功响应

    200 OK
    201 Created
    204 No Content
    206 Partial Content
    
  • 3xx
    :重定向

    301 Moved Permanently
    302 Found
    304 Not Modified
    307 Temporary Redirect
    308 Permanent Redirect
    
  • 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
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/html content-type: application/json
accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7 Content-Language: zh-CN
accept-encoding: br, gzip, deflete content-encoding: gzip

|Vary: Accept-Encoding

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

跨源资源共享 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):

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

哪些响应可以被缓存:

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

常见缓存头:

Expires
(Since HTTP/1.0)

  • Expires:
    : 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

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

Pragma: no-cache
(Since HTTP/1.0)

cache-control: no-cache
cache-control

缓存 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 的缓存资源是否过期?

    Age: 
    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=
,如:

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 的高级协议,如 Websocket
WebDAV
等等。升级方式为添加以下两个头:

  • Connection: Upgrade
  • Upgrade: protocols

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