HTTP协议
HTTP是工作在应用层的协议,用于在客户端比如浏览器和服务器之间传输数据,是web的基石,采用的是请求-响应模型,无状态、可扩展。
状态码来说:
1xx比如101 Switching Protocols。2xx开头是成功的,比如200 OK 201 Created、204 No Content。3xx开头为重定向,301是永久重定向,302是临时的。4xx开头是客户端错误,5开头的是服务端错误。
常见头部
常见请求头/响应头
| 头部 | 作用 |
|---|---|
Host | 目标主机(HTTP/1.1 必需) |
Content-Type | 报文体的 MIME 类型 |
Content-Length | 报文体长度 |
Accept | 客户端可接受的类型 |
Authorization | 认证凭证(如 Bearer Token) |
Cookie / Set-Cookie | 会话状态管理 |
Cache-Control | 缓存策略 |
ETag / If-None-Match | 协商缓存 |
Last-Modified / If-Modified-Since | 协商缓存 |
Access-Control-Allow-Origin | CORS 跨域许可 |
| 浏览器同源策略限制跨域请求。CORS 通过响应头授权: |
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
预检请求:非简单请求(如 PUT、自定义头)会先发 OPTIONS 请求确认。OPTIONS请求,cors预检请求的响应状态码通常是204 No Content。当使用了非简单方法比如PUT、DELETE、PATCH等的时候就会自动发送这个,或是使用了自定义请求头Authorization、X-Custom-Header也会。这个在另一个笔记里 [[RestfulAPI和GraphQL]] 这里有讲过,可以直接看。
长连接
http长连接核心是复用一个tcp连接发送多个请求,避免每次都重新握手等。短连接是http/1.0默认,每次都要经过tcp握手。长连接是http/1.1默认(通过keep-alive复用实现),多个请求会复用一个tcp。但是http/1.1会有队头阻塞问题,同一个连接上,前一个请求还没有响应完,后面的请求要等待。h2里面连接本身就是一个多路复用器了。
只需要一个tcp连接是设计目标但是不是绝对的,准确说,每个origin通常有一条,不是全站一条
http/2的多路复用解决了这个问题:
一个 TCP 连接
├── Stream 1: 请求 A
├── Stream 2: 请求 B
├── Stream 3: 请求 C
└── 并发传输,互不阻塞
左边是为什么当年要做 h2:浏览器被逼着对一个域名开 6 条 TCP,每条上面请求还得排成一队,一个慢接口就把后面全卡住。h2 把这些塌缩成右边这一条连接,里面用 **stream(流) 区分不同请求,可以交错传输。
- 刚才我们说过tcp按照origin算,不是按照页面算,如果站点使用了CDN、图床、第三方统计,那就是好几个不同的origin,各开各的。h2的连接合并不能跨origin。
- 浏览器没有义务只保持一条。连接建立慢时会开"竞速连接",连接空闲久了会回收,有网络切换或异常时也可能重建。
HTTP版本协商路径与占比:
h2 只跑在 TLS 上。浏览器的明文 h2(h2c)支持早就移除了,所以 http:// 站点实际只能是 h1.1。这就是为什么"h2 默认"约等于"HTTPS 默认"。通过一个连接并发多个请求,将请求/响应拆成帧,交错传输和头部压缩的方式解决了,http/2的长连接更加高效,一个连接搞定所有请求。
所以,阅读完上面的文字,我们就能清楚。使用明文的http的时候基本就是h1.1协议,"h2 需要 TLS"、"尽量维持一条连接更好",一条连接同时省掉 TCP 和 TLS 两套握手成本,而且版本是绑死在连接上的(ALPN 在建立时定,中途不能换),所以换连接就等于重来一遍。
长连接通过Connection: keep-alive声明,依靠tcp keep-alive传输层:
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
可以检测连接是否还或者,对端挂了,网络断了。应用层自己会发送心跳包,比如说30s一个,来保持链接活跃。如果说应用崩溃,连接不会通过FIN关闭的话,内核会替他关闭,可能是RST,也可能是FIN,比如说进程如果被SIGKILL 杀死,还有未发送的消息,他就会发RST,处理完了就会发FIN。
头部压缩
头部压缩式http/2的核心优化,因为http/1.1头部每次都会携带大量重复头部:
GET /index.html
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
Accept: text/html,application/xhtml+xml,...
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br
Cookie: session=abc123; theme=dark; ...
Connection: keep-alive
下一个请求也是一样。每个头部都会重复传递这些,显然会浪费带宽,HTTP/1.1 用 gzip 压缩 body,但不压缩头部——头部一直是明文。HTTP/2 引入头部压缩——HPACK。61个常见头部,然后给编号压缩,这就是静态表,对于连接期间动态维护的头部,使用索引来压缩,这就是静态表。对字符串使用哈夫曼编码,这样就可以让头部压缩很大一部分。
https
我们都知道http是明文传输,数据裸奔,中间人可以直接看到密码、cookie等信息 还能篡改,https可以用tls解决这些问题,https分为四层:
HTTP ← 应用层
↓
TLS/SSL ← 安全层(加解密、认证、完整性)
↓
TCP ← 传输层
↓
IP ← 网络层
https = http + tls,http报文会被tls加密之后走tcp传输。tls使用非对称加密,在握手阶段解决。公钥是公开的,私钥是服务器自己保留,握手阶段会协商出会话秘钥,对称加密使用一个秘钥加解密,在非对称加密协商的时候,这个秘钥会被协商出来。
我们用tls1.2版本解释:
客户端 服务器
│ │
│ ① Client Hello │
│ - 支持的 TLS 版本 │
│ - 支持的加密套件 │
│ - 客户端随机数(Random1) │
│ ─────────────────────────────────> │
│ │
│ ② Server Hello │
│ - 选定的 TLS 版本 │
│ - 选定的加密套件 │
│ - 服务器随机数(Random2) │
│ <───────────────────────────────── │
│ │
│ ③ Certificate │
│ - 服务器证书(含公钥) │
│ <───────────────────────────────── │
│ │
│ ④ Server Key Exchange(可选) │
│ - 密钥交换参数 │
│ <───────────────────────────────── │
│ │
│ ⑤ Server Hello Done │
│ <───────────────────────────────── │
│ │
│ ⑥ 客户端验证证书 │
│ - 检查证书是否可信 │
│ - 检查域名是否匹配 │
│ - 检查是否过期 │
│ │
│ ⑦ Client Key Exchange │
│ - 用服务器公钥加密"预主密钥" │
│ ─────────────────────────────────> │
│ │
│ ⑧ Change Cipher Spec │
│ - 通知"后续用协商的密钥加密" │
│ ─────────────────────────────────> │
│ │
│ ⑨ Finished │
│ - 加密的握手摘要 │
│ ─────────────────────────────────> │
│ │
│ ⑩ Change Cipher Spec │
│ <───────────────────────────────── │
│ │
│ ⑪ Finished │
│ <───────────────────────────────── │
│ │
│ ⑫ 开始加密通信(对称加密) │
│ <════════════════════════════════> │
客户端会检查证书链,证书是否是可信CA签发,浏览器会内置CA列表。证书里面的域名是否匹配访问的域名,以及是否过期。客户端会生成预主秘钥,用于服务器公钥加密,发送给服务器,服务器用私钥解密,拿到预主密钥。
主密钥 = PRF(预主密钥, "master secret", Random1 + Random2)
主密钥用来生成会话秘钥,也就是对称加密的秘钥,这样握手就完成了。