简单认识 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
,注意所有的请求都将被定向为 GET302 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 协议
参考资料:
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP
- https://zh.wikipedia.org/wiki/HTTP/3
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: <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 响应,通常还同时会返回一个
-
如何判断响应后端是真实的服务端还是 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
- CDN 厂商通常都会添加一些自定义的头:
参考资料: 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 的高级协议,如 Websocket
和 WebDAV
等等。升级方式为添加以下两个头:
- 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
头白名单的方式一定程度上实现防盗链。