mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
docs: SSL/TLS
This commit is contained in:
@@ -1,16 +1,203 @@
|
||||
# HTTP 请求头 Range 信息
|
||||
# HTTP/HTTPS 细节探索
|
||||
|
||||
## TCP/UDP
|
||||
|
||||
## HTTP 缓存控制
|
||||
|
||||
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
|
||||
|
||||
由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。所以,非常有必要把“来之不易”的数据缓存起来,下次再请求的时候尽可能地复用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。
|
||||
|
||||
试想一下,如果有几十 K 甚至几十 M 的数据,不是从网络而是从本地磁盘获取,那将是多么大的一笔节省,免去多少等待的时间。
|
||||
|
||||
实际上,HTTP 传输的每一个环节基本上都会有缓存,非常复杂。
|
||||
|
||||
基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存。
|
||||
|
||||
### 服务器的缓存控制
|
||||
|
||||
一个缓存控制的标准流程是:
|
||||
|
||||
- 浏览器第一次发送请求,向服务器获取资源;
|
||||
|
||||
- 服务器响应请求,返回资源,同时标记资源的有效期;服务器标记资源有效期使用的头字段是 `Cache-Control`,里面的值 `max-age=30` 就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用”
|
||||
|
||||
- 浏览器缓存资源,等待下次复用
|
||||
|
||||
- 后续浏览器的每次请求都会携带缓存相关的头信息(比如 if-not-match、if-last-modified等)
|
||||
|
||||
- 浏览器会根据服务端的资源情况,判断客户端的缓存有没有更新或者变化,来继续在响应头中将缓存信息返回,如果没有过期则返回类似 “304 Not Modified” 之类的信息。
|
||||
|
||||
QA: 浏览器直接缓存数据就好了,为什么需要加有效期?
|
||||
|
||||
服务端上的资源可能会变,比如网关层生成的商品 DB,客户端缓存后每次 App 启动都需要去向服务端问有没有新的数据。可能服务端晚上10点定时生成新的商品数据。所以需要加缓存有效期。
|
||||
|
||||
Cache-Control 字段里的 max-age 指的是**生存时间**。时间的起点是响应报文的创建时间(Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路上传输过程中的所有节点停留时间。比如 “max-age=5”,也就是缓存有效期为5s,恰好网络比较糟糕,等客户端收到报文已经过了4s,那么缓存在客户端1s有效,之后就失效了。
|
||||
|
||||
Cache-Control 中比较常用的就是 max-age,此外还有几个
|
||||
|
||||
- no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面
|
||||
|
||||
- no-cache:它的字面含义容易与 no-store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本
|
||||
|
||||
- must-revalidate:又是一个和 no-cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
|
||||
|
||||
- public:可以被所有用户缓存(多用户共享),包括终端和 CDN 等中间代理服务
|
||||
|
||||
- private:只能被终端浏览器缓存(而且是私有缓存),不允许中继缓存服务器进行缓存
|
||||
|
||||
### 客户端的缓存控制
|
||||
|
||||
有没有注意到这样一个现象:每次浏览器强制刷新,页面上的内容可能会变。之前返回的数据明明说缓存3600s。为什么?
|
||||
|
||||
其实不止服务器可以设置 `Cache-Control` ,浏览器也可以设置。也就是说:请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
|
||||
|
||||
点击刷新,浏览器会在请求头加“Cache-Control:max-age=0”。代表着浏览器告诉服务器,我需要一个最新的数据。
|
||||
|
||||
点击强制刷新,浏览器会在请求头将 `If-Modified-Since`、`If-None-Match` 清空。所以服务器会返回最新的数据(当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200)
|
||||
|
||||
将网页点击前进、后退会发现“ Status Code:200 from disk cache”
|
||||
|
||||

|
||||
|
||||
### 条件请求
|
||||
|
||||
早期浏览器可以用2个连续请求完整缓存验证:第一个 HEAD,获取资源的修改时间等信息,然后与本地缓存数据相比较,如果没有修改则使用缓存,节省网络流量,否则就发第二个 GET 请求,获取最新的资源数据。
|
||||
|
||||
但是2个请求网络成本太高,所以 HTTP 协议就定义了一系列 `If` 开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。
|
||||
|
||||
条件请求共5个头字段,常用的是 `If-Modified-Since` + `Last-modified` 和 `If-None-Match` + `ETag`这两个(需要搭配使用)。需要在第一次响应报文预先提供 `Last-modified` 和 `ETag`,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果服务端资源没有更新,则返回 “304 Not Modified”,表示缓存有效, 浏览器只需要更新缓存日期便可继续使用本地缓存
|
||||
|
||||

|
||||
|
||||
Last Modified 代表资源的最后修改时间
|
||||
|
||||
ETag 即 Entity Tag,代表资源的唯一标识。主要用来解决修改时间无法准确区分文件变化的问题。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
|
||||
|
||||
- 一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分
|
||||
|
||||
- 一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽
|
||||
|
||||
- 强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个 `W/` 标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)
|
||||
|
||||
## HTTP 代理服务
|
||||
|
||||
引入 HTTP 代理后,原来简单的双方通信就变复杂了一些,加入了一个或者多个中间人,但整体上来看,还是一个有顺序关系的链条,而且链条里相邻的两个角色仍然是简单的一对一通信,不会出现越级的情况。代理在 HTTP 协议里对它并没有什么特别的描述,它就是在客户端和服务器原本的通信链路中插入的一个中间环节,也是一台服务器,但提供的是“代理服务”。
|
||||
|
||||
所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
|
||||
|
||||
代理最基本的一个功能是负载均衡。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。
|
||||
|
||||
代理中著名的负载均衡算法,比如轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。
|
||||
|
||||
在负载均衡的同时,代理服务还可以执行更多的功能,比如:
|
||||
|
||||
- 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用
|
||||
|
||||
- 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载
|
||||
|
||||
- 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本
|
||||
|
||||
- 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应
|
||||
|
||||
- 内容缓存:暂存、复用服务器响应
|
||||
|
||||
### 代理头字段
|
||||
|
||||
代理体现在头信息上就是字段 `Via`,是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。
|
||||
|
||||

|
||||
|
||||
`X-Forwarded-For` 的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就是客户端的地址
|
||||
|
||||
`X-Real-IP` 是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。
|
||||
|
||||
### 代理协议
|
||||
|
||||
有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。
|
||||
|
||||
v1 规定,它在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头。这一行文本其实非常简单,开头必须是“PROXY”五个大写字母,然后是“TCP4”或者“TCP6”,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。
|
||||
|
||||
```shell
|
||||
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
|
||||
GET / HTTP/1.1\r\n
|
||||
Host: www.xxx.com\r\n
|
||||
\r\n
|
||||
```
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供“代理服务”
|
||||
|
||||
- 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能
|
||||
|
||||
- 代理服务器需要使用字段“Via”标记自己的身份,多个代理会形成一个列表
|
||||
|
||||
- 如果想要知道客户端的真实 IP 地址,可以使用字段“X-Forwarded-For”和“X-Real-IP”
|
||||
|
||||
- 专门的“代理协议”可以在不改动原始报文的情况下传递客户端的真实 IP
|
||||
|
||||
## 性能优化之 HTTP 缓存代理
|
||||
|
||||
客户端(浏览器)上的缓存控制,它能够减少响应时间、节约带宽,提升客户端的用户体验。
|
||||
|
||||
HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPS(request per second)降低好几个数量级,减轻应用服务器的并发压力,对性能的改善是非常显著的。
|
||||
|
||||

|
||||
|
||||
代理服务器没有缓存的时候:代理服务器每次直接转发来自客户端的报文给服务端,转发服务端的报文给客户端,中间不会存储任何数据,只有基础的中转功能。
|
||||
|
||||
有了缓存后:
|
||||
|
||||
- 把报文转发给客户端
|
||||
|
||||
- 把报文存储到缓存中
|
||||
|
||||
这样下次有相同的请求,代理服务器就可以直接返回 304 或者缓存数据,不必再从源服务器那里获取一次数据,降低了客户端等待时间、节约了源服务器的带宽
|
||||
|
||||
### 源服务器的缓存控制
|
||||
|
||||
客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件。
|
||||
|
||||
额外增加2个新属性:
|
||||
|
||||
- private:表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。比如你访问购物网站,返回的响应报文里用 "Set-Cookie" 添加了 token,这就属于私人数据,不能存在代理上
|
||||
|
||||
- public:表示缓存完全开放,谁都可以存,谁都可以用
|
||||
|
||||
关于缓存失效后的验证有一些新的属性:
|
||||
|
||||
- proxy-revalidate:只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理
|
||||
|
||||
- s-maxage:s即 share 的意思,表示限定在代理上能够存多久,而客户端仍然使用 `max-age`
|
||||
|
||||
- no-transform:代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,`no-transform` 则禁止这样做,不允许对资源做任何处理
|
||||
|
||||
下图是完整的服务器端缓存控制策略,可以同时控制客户端和代理
|
||||
|
||||

|
||||
|
||||
### 客户端的缓存控制
|
||||
|
||||

|
||||
|
||||
- max-stale:如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要
|
||||
|
||||
- min-fresh:缓存必须有效,而且必须在 x 秒后依然有效
|
||||
|
||||
## HTTP 请求头 Range 信息
|
||||
|
||||
请求资源的部分内容,单位是 byte(字节),从0开始。
|
||||
如果请求头携带了 Range 信息,也就是分批下载,这时候服务器会返回 206 Partial Content 的状态码及说明。
|
||||
|
||||
如果服务器不支持分批下载,那么会返回整个资源的大小以及状态码为200。
|
||||
|
||||
|
||||
## Range 请求头
|
||||
### Range 请求头
|
||||
|
||||
`Range: bytes=start-end`
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
Range: bytes=10- //:从第10个字节开始到最后一个字节的数据
|
||||
Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据
|
||||
@@ -18,31 +205,776 @@ Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据
|
||||
|
||||
注意:整个表示 [start, end] 是前闭后闭的,也就是包含请求头的 start 和 end。所以下次请求应该是 [end+1, nextEnd]。
|
||||
|
||||
|
||||
## Content-Range 响应头
|
||||
### Content-Range 响应头
|
||||
|
||||
`Content-Range:bytes 0-10/3000`
|
||||
表示服务器返回了前(0-10)个字节的数据,总共3000字节的数据。
|
||||
|
||||
### Content-Type 数据类型
|
||||
|
||||
## Content-Type 数据类型
|
||||
`Content-Type:image/png` 表示资源类型是 png 格式的图片
|
||||
|
||||
## Content-Length 资源的长度
|
||||
### Content-Length 资源的长度
|
||||
|
||||
`Content-Length:11` 表示服务器响应了11个字节的数据
|
||||
|
||||
## Last-Modified
|
||||
### Last-Modified
|
||||
|
||||
`Last-Modified:Tue, 30 Jun 2018 03:12:48 GMT` 表示资源最近被修改的时间,如果分批下载的时候发现 Last-Modified 被修改了,那么需要重新下载
|
||||
|
||||
## ETag
|
||||
### ETag
|
||||
|
||||
`ETag: W/"3103-1435633968000"` 表示资源版本的标示符。通常是消息摘要(类似MD5)。分段下载时需要注意,或者缓存控制也需要注意。如果是分布式缓存系统,需要确保每台计算机的 ETag 计算规则的一致性,缓存的过期需要结合 ETag 和 Last-Modified 共同决定。
|
||||
|
||||
## 分段下载
|
||||
### 分段下载
|
||||
|
||||
利用 HTTP 的头信息的上述几个特点,我们可以充分利用多线程的能力。
|
||||
|
||||
- 先发送一个 HEAD 方法的请求,知道总文件大小(Content-Length 就是总字节大小)
|
||||
- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...)
|
||||
- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...)
|
||||
|
||||
## HTTPS 安全
|
||||
|
||||
HTTP 的缺点之一就是:明文 + 不安全。为此诞生了 HTTPS 协议。
|
||||
|
||||
比如代理服务,它作为 HTTP 通信的中间人,在数据上下行的时候可以添加或删除部分头字段,也可以使用黑白名单过滤 body 里的关键字,甚至直接发送虚假的请求、响应,而浏览器和源服务器都没有办法判断报文的真伪。这是很糟糕的
|
||||
|
||||
如何定义安全:
|
||||
|
||||
- 机密性:指对数据保密,只能由可信的人访问,对其他人是不可见的“秘密”,简单来说就是不能让不相关的人看到不该看的东西
|
||||
|
||||
- 完整性:也叫一致性,数据在传输过程中没有被篡改,不多也不少,“完完整整”地保持着原状
|
||||
|
||||
- 身份认证:指确认对方的真实身份,也就是“证明你真的是你”,保证消息只能发送给可信的人
|
||||
|
||||
- 不可否认:不能否认已经发生过的行为
|
||||
|
||||
HTTPS 其实是一个“非常简单”的协议,RFC 只有短短的 7 页 描述,里面规定了新的协议名`https`,默认端口号 `443`,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。也就是说,除了协议名 `http` 和端口号 `80` 这两点不同,HTTPS 协议在语法、语义上和 HTTP 完全一样,有窃电也一样(但缺点不包含明文、不安全)
|
||||
|
||||
HTTPS 的核心在于 `s` ,也就是把 HTTP 下层传输协议由 TCP/IP 换为了 SSL/TLS,从 "HTTP Over TCP/IP" 变为 "HTTP Over SSL/TLS"。让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
|
||||
|
||||

|
||||
|
||||
SSL(Secure Sockets Layer),安全套接字层,在 OSI 模型中第五层会话层,由网景公司在1994年发明,有 V2、V3 版本,V1 因为有严重缺陷从未公开过。SSL 发展到 V3 被证明是一个非常好的安全通信协议,于是互联网工程组 IEFT 在1999年更名为 TLS(Transport Layer Security)传输层安全,正式标准化,版本号从1.0计数,所以 TLS1.0 也就是 SSL V3.1/
|
||||
|
||||
TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术
|
||||
|
||||
客户端和服务器在使用 TLS 建立链接时需要选择一组恰当的加密算法来实现安全通信,这组算法叫“密码套件”(Cipher Suite,加密套件)
|
||||
|
||||
TLS 的密码套件命名规范,格式固定:密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法。
|
||||
|
||||
`ECDHE-RSA-AES256-GCM-SHA384` 代表握手时使用 ECDHE 算法作为密钥交换算法,用 RSA 签名和身份认证,握手后通信使用 AES 对称加密方法,密钥长度256位,分组模式为 GCM,摘要算法 SHA38 用于消息认证和产生随机数。
|
||||
|
||||
TLS 中有很多对称加密算法可以选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20
|
||||
|
||||
- AES:Advanced Encryption Standard,高级加密标准”,密钥长度可以是 128、192 或 256。是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法
|
||||
|
||||
- ChaCha20:Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错的算法
|
||||
|
||||
### 加密分组模式
|
||||
|
||||
对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文。明文的长度不固定,而密钥一次只能处理特定长度的一块数据,这就需要进行迭代,以便将一段很长的明文全部加密,而迭代的方法就是分组模式。
|
||||
|
||||
比如 `AES128-GCM`,意思是密钥长度为 128 位的 AES 算法,使用的分组模式是 GCM;`ChaCha20-Poly1305` 的意思是 ChaCha20 算法,使用的分组模式是 Poly1305。
|
||||
|
||||
### 对称加密、非对称加密
|
||||
|
||||
对称加密实现了机密性,但有一个很大的问题:如何将密钥安全地传输给对方,也就是"密钥交换"。诞生了非对称加密。拥有2个密钥:公钥、私钥。
|
||||
|
||||
公钥可以公开给任何人使用,私钥必须秘密保存。
|
||||
|
||||
私钥、公钥都是“单向”的。也就是公钥加密后只能私钥解密,私钥加密后只能公钥解密。
|
||||
|
||||
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。非对称加密也叫“公钥加密算法”的原理。
|
||||
|
||||
RSA 是非对称加密中最著名的一个,可以说是非对称加密的代名词。“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。
|
||||
|
||||
RSA 公开密钥密码体制的原理是:根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。
|
||||
|
||||
10年前 RSA 密钥推荐长度为1024位,但随着计算机运算能力的提高,1024位已经不够安全了,目前需要2048位。
|
||||
|
||||
ECC(Elliptic Curve Cryptography) 是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。
|
||||
|
||||
### 混合加密
|
||||
|
||||
非对称的优点也是缺点:因为非对称加密密钥“密钥交换”的问题,但是是基于复杂的数学难题实现的,所以运算速度比较慢。下面是网上看到的测试速度
|
||||
|
||||
```shell
|
||||
aes_128_cbc enc/dec 1000 times : 0.97ms, 13.11MB/s
|
||||
rsa_1024 enc/dec 1000 times : 138.59ms, 93.80KB/s
|
||||
rsa_1024/aes ratio = 143.17
|
||||
rsa_2048 enc/dec 1000 times : 840.35ms, 15.47KB/s
|
||||
rsa_2048/aes ratio = 868.13
|
||||
```
|
||||
|
||||
TLS 将2者结合起来使用混合加密手段(取长补短)兼顾安全和性能。
|
||||
|
||||
- 通信刚开始使用非对称加密算法,比如 RSA、ECDHE,解决密钥交换的问题
|
||||
|
||||
- 然后用随机数产生对称算法使用的“会话密钥”,再用公钥加密。因为会话密钥很短,通常有16/32字节,所以慢一点无所谓。
|
||||
|
||||
- 对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。
|
||||
|
||||
总结:
|
||||
|
||||
- 加密算法的核心思想是“把一个小秘密(密钥)转化为一个大秘密(密文消息)”,守住了小秘密,也就守住了大秘密
|
||||
|
||||
- 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20
|
||||
|
||||
- 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC
|
||||
|
||||
- 把对称加密和非对称加密结合起来就得到了“又好又快”的混合加密,也就是 TLS 里使用的加密方式
|
||||
|
||||
### 数字签名与证书
|
||||
|
||||
虽然采用混合加密,黑客因为没有密钥,就无法破解得到秘文。但是中间人可以伪造身份发布公钥,客户端拿到假的公要钥,混合加密就失效了,以为是和某个网站进行通信,其实是黑客,这样手机号等敏感信息都泄漏了。
|
||||
|
||||
所以,在机密性的基础上还需要添加完整性、身份认证等特性,才可以实现真正的安全。
|
||||
|
||||
#### 摘要算法
|
||||
|
||||
Digest Algorithm 实现完整性校验的手段就是**摘要算法**,也就是常说的散列函数、哈希函数。
|
||||
|
||||
摘要算法近似地理解成一种特殊的压缩算法,可以任意长度的数据“压缩”成固定长度且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
|
||||
|
||||
换一个角度,也可以把摘要算法理解成特殊的“单向”加密算法,它只有算法,没有密钥,加密后的数据无法解密,不能从摘要逆推出原文。
|
||||
|
||||
摘要算法要对输入具有“单向性”和“雪崩效应”。对于输入数据的些许改变就会产生很大的变化,所以被用来 TLS 生成伪随机数(PRF,pseudo random function)
|
||||
|
||||
大家都听过 md5、SHA-1,他们是常见的摘要算法,但目前已经不够安全,在 TLS 中被禁止使用了。TLS 推荐 SHA-2。
|
||||
|
||||
SHA-2 其实是一系列摘要算法的统称,包含6中,常用的是 SHA224、SHA256、SHA384,分别生成28字节、32字节、48字节的摘要。
|
||||
|
||||
#### 完整性
|
||||
|
||||
摘要算法的作用就是保证“数字摘要”和原文是完全等价的。所以只需要在原文后附加上摘要,就能保证数据的完整性(还不是 TLS 的最终实现,继续向下分析)
|
||||
|
||||
比如原始信息是“我叫杭城小刘,喜欢乒乓球”,然后根据 SHA-2 计算出摘要 xxx,发送的时候就将“我叫杭城小刘,喜欢乒乓球”和摘要一起发送给服务器,服务器收到消息,然后重新计算一下摘要,将传输过来的摘要和自己计算的摘要进行比较,如果一致,则说明消息是可信的,没有被中间人修改。
|
||||
|
||||
够了吗?还不够,**摘要算法不具备“机密性”**,明文传输的时候黑客可以将消息修改后,重新生成一份摘要,一起发送给服务器,此时服务器就以为还是没有中间人攻击。
|
||||
|
||||
知道问题症结所在,也比较好解,真正的完整性必须建立在“机密性”基础上。使用混合加密系统中用会话密钥加密消息和摘要,这样中间人无法得知明文。这个过程叫做“哈希消息认证码”(HMAC)
|
||||
|
||||

|
||||
|
||||
#### 数字签名
|
||||
|
||||
加密算法 + 摘要算法,通信过程已经比较安全了,但是还是存在不足,比如通信的发送方和接收方。黑客可以伪装成客户端向服务器请求隐私数据,也可以伪装为服务器,获取你的隐私数据。
|
||||
|
||||
如何标记你就是你?可以利用非对称加密中的私钥。使用私钥,再加上摘要算法,就可以实现“数字签名”,同时保证“身份认证”和“不可否认”。
|
||||
|
||||
数字签名其实就是把公钥、私钥反过来使用。发送报文时,发送方用一个哈希函数从报文文本中生成报文摘要,然后用发送方的私钥对这个摘要进行加密,这个加密后的摘要将作为报文的数字签名和报文一起发送给接收方。接收方首先使用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要,接着再用公钥对报文附加的数字签名进行解密,如果2个摘要相同,则接收方认为该报文是发送方发送的。
|
||||
|
||||
非对称加密效率太低,所以私钥只加密原报文的摘要,这样运算量小,速度也快。得到的数字签名也很小,方便传输和保管。
|
||||
|
||||

|
||||
|
||||
这个过程被叫做“签名”、“验签”。
|
||||
|
||||
#### 数字证书和 CA
|
||||
|
||||
目前为止已经实现了安全的四大特性:机密性、完整性、身份认证、不可否认。够了吗?还不够,还存在“公钥信任”的问题。因为任何人都可以发布公钥,目前还缺少防止黑客伪造公钥的手段。
|
||||
|
||||
可能你会想到利用混合加密去实现?一环套一环,存在“鸡生蛋还是蛋生鸡“的问题。逻辑上死锁。
|
||||
|
||||
iOS 打破循环引用就可以使用 NSProxy 类的解决方案,这里也一样,引入一个具有权威性的第三方机构,也就是 CA(Certificate Authority)证书认证机构。来负责给各个公钥签名。
|
||||
|
||||
CA 对公钥的签名认证也是有格式的,不只是把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。知名的 CA 全世界就那么几家,比如 DigiCert、VeriSign、Entrust、Let’s Encrypt 等,它们签发的证书分 DV、OV、EV 三种,区别在于可信程度。DV 是最低的,只是域名级别的可信,背后是谁不知道。EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)
|
||||
|
||||
CA 如何自证?
|
||||
|
||||
信任链。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。
|
||||
|
||||

|
||||
|
||||
浏览器内置各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。
|
||||
|
||||
#### 证书体系的弱点
|
||||
|
||||
证书体系(PKI,Public Key Infrastructure)虽然是目前整个网络世界的安全基础设施,但绝对的安全是不存在的,它也有弱点,还是关键的“信任”二字。
|
||||
|
||||
- 如果 CA 失误或者被欺骗,签发了错误的证书,虽然证书是真的,可它代表的网站却是假的。解决方案:开发出了 CRL(证书吊销列表,Certificate revocation list)和 OCSP在线证书状态协议,Online Certificate Status Protocol),及时废止有问题的证书
|
||||
|
||||
- 更危险的是,CA 被黑客攻陷,或者 CA 有恶意,因为它(即根证书)是信任的源头,整个信任链里的所有证书也就都不可信了
|
||||
|
||||
### TLS 1.2 连接过程
|
||||
|
||||
#### HTTPS 建立连接
|
||||
|
||||
在地址栏输入了以 HTTPS 开头的 URI,会发生什么事情?
|
||||
|
||||
浏览器首先要从 URI 里提取出协议名和域名。因为协议名是“https”,所以浏览器就知道了端口号是默认的 443,它再用 DNS 解析域名,得到目标的 IP 地址,然后就可以使用三次握手与网站建立 TCP 连接了。在 HTTP 协议里,建立连接后,浏览器会立即发送请求报文。但现在是 HTTPS 协议,它需要再用另外一个“握手”过程,在 TCP 上建立安全连接,之后才是收发 HTTP 报文。
|
||||
|
||||
#### TLS 协议的组成
|
||||
|
||||
TLS 包含多个子协议,每个协议都有各自的职责,比较常用的协议有:
|
||||
|
||||
- 记录协议:Record Protocol,规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK
|
||||
|
||||
- 警报协议:Alert Protocol,的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接
|
||||
|
||||
- 握手协议:Handshake Protocol,是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统
|
||||
|
||||
- 变更密码规范协议:Change Cipher Spec Protocol,它非常简单,就是一个“通知”,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的
|
||||
|
||||
关于记录可以用下图解释
|
||||
|
||||

|
||||
|
||||
可以看出:
|
||||
|
||||
- 每一个框都可以看成一条记录(最顶上绿色框整体就是一个记录)
|
||||
|
||||
- 多个记录组合成一个 TCP 包发送(右侧最顶上3条记录,当作一个 TCP 包发送)
|
||||
|
||||
- TLS 最多经历2次消息往返(4个消息)就可以完成握手
|
||||
|
||||
#### ECDHE 握手过程(TLS核心)
|
||||
|
||||
下图是 TLS 完整的流程
|
||||
|
||||

|
||||
|
||||
上 Demo,以 Mac 上 wireshark 抓取 `https://github.com` 为例。
|
||||
|
||||
第一步:在 TCP 3次握手建立连接后,客户端发送一个 “Client Hello“ 的消息,表示开始和服务器沟通。包含客户端的 TLS 版本号、支持的密码套件、随机数,这些信息用于后续生成会话密钥。
|
||||
|
||||

|
||||
|
||||
其实,这些信息的作用都是**协商**,客户端告诉浏览器我这边的 TLS 协议是什么版本,我本地支持的加密套件都有哪些,你后续从我支持的列表中选一个。
|
||||
|
||||
第二步:服务端收到 “Client Hello” 的消息后,会返回一个 “Server Hello” 的消息。核对版本号,同时也会生成一个随机数,然后从客户端的加密套件中选择一个作为本次通信使用的密码套件。
|
||||
|
||||

|
||||
|
||||
可以看到此时,服务端选择了 `Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)`这个加密套件。服务端返回给客户端的这些信息也就代表“TLS 版本号对上了,你这边给的加密套件很多,我选了一个最合适的,椭圆曲线 + RSA + AES + SHA384。另外我给了你一个随机数,你需要保存后续使用”
|
||||
|
||||
QA:为什么看到 Client Hello、Server Hello 之间还有一个 TCP 包?
|
||||
|
||||
TLS 是建立在 TCP 的上层协议,因此要先按照 TCP 的规则来,也就是发出去的包,会收到相应的 ACK 包。如果对应的某个 TCP 数据包里面装的内容凑巧是 TLS 协议,那么 wireshark 就会把他展示成 TLS
|
||||
|
||||
注意这里不是 TCP Fast Open,TFO 是用来加速连续 TCP 连接的数据交互 TCP 拓展协议。原理如下:TCP 三次握手的过程中,当用户首次访问 Server 时,发送 SYN 包,Server 根据用户 IP 生成 Cookie(已加密),并与 SYN-ACK 一同发回 Client;当 Client 随后重连时,在SYN 包携带 TCP Cookie;如果 Server 校验合法,则在用户回复 ACK 前就可以直接发送数据;否则按照正常三次握手进行。
|
||||
|
||||
|
||||
|
||||
|
||||
第三步:服务器会把证书也发送给客户端(Server Certificate),见下图第一个大红框中的内容。
|
||||
|
||||
同时服务器选择了 ECDHE 算法,所以在发送了服务器证书后马上发送“Server Key Exchange”消息。里面是椭圆曲线的公钥(Server Params),用来实现密钥的交换算法,再加上自己的私钥签名认证(用私钥对椭圆曲线的 public key 做了签名认证生成了 Signature)
|
||||
|
||||

|
||||
|
||||
意味着,服务器告诉客户端,我这边选择的加密套件有点复杂,所以再给你一个算法的参数,和随机数一样,先保存后续使用。为了保证我就是我,我给参数 public key 做了签名。
|
||||
|
||||
第四步:服务端发送“Server Hello Done”消息。告诉客户端,我的基础信息就是这些,打招呼阶段结束
|
||||
|
||||

|
||||
|
||||
至此,第一个消息往返就结束了(2个TCP包),客户端和服务端通过**明文**共享了:Client Random、Server Random、Server Params。
|
||||
|
||||
同时,客户端拿着服务端的证书,他还不信任,他需要去找 CA,开始证书的逐级验证,确认证书的真实性,再用证书的公钥验证签名,就确认了服务器的身份。
|
||||
|
||||
第五步:客户端按照加密套件的要求,也生成了一个椭圆曲线的公钥(Client Params),用“Client Key Exchange”消息发送给服务器
|
||||
|
||||

|
||||
|
||||
至此,客户端和服务器都拿到了密钥交换算法的2个参数(Client Params、Server Params),然后用 ECDHE 算法计算出一个随机数,叫“Pre-Master”,也叫做“预主密钥”。
|
||||
|
||||
现在客户端和服务器都拥有3个随机数:Client Random、Server Random、Pre-Master,用这3个数,就可以生成用于加密会话的主密钥(Master Secret)。因为黑客拿不到 Pre-Master,所以也不会得到 Master Secret。
|
||||
|
||||
有了主密钥和会话密钥,握手就要结束了。此时客户端发送 “Change Cipher Spec”,再发送一个 “Encrypted Handshake Message” 的消息。把之前所有发送的数据做个摘要,再加密,让服务器做个验证。
|
||||
|
||||
也就是告诉服务器“后续都采用对称加密算法进行通信了,用的就是协商过的 AES,但是你还需要测试下能否正常解密”
|
||||
|
||||
QA:那里看出是 AES?
|
||||
|
||||
图上可以看到 “Change Cipher Spec”这里 `Content Type:Change Cipher Spec(20)`。
|
||||
|
||||
20对应16进制14。翻到最顶上看客户端的加密套件列表,`Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)`.
|
||||
|
||||
QA:为什么必须是3个随机数?
|
||||
|
||||
TLS 的设计者认为不能信任客户端和服务器伪随机数的可靠性,为了保证真正的“完全随机”、“不可预测”,将3个随机数混合起来,结果会更加随机,大大增加黑客破解成本,更加难以预测。
|
||||
|
||||
```shell
|
||||
master_secret = PRF(pre_master_secret, "master secret",
|
||||
ClientHello.random + ServerHello.random)
|
||||
```
|
||||
|
||||
这里的“PRF”就是伪随机数函数,它基于密码套件里的最后一个参数,比如这次的 SHA384,通过摘要算法来再一次强化 “Master Secret” 的随机性。主密钥有 48 字节,但它也不是最终用于通信的会话密钥,还会再用 PRF 扩展出更多的密钥,比如客户端发送用的会话密钥(client_write_key)、服务器发送用的会话密钥(server_write_key)等等,避免只用一个密钥带来的安全隐患。
|
||||
|
||||
第六步:服务器发送“Change Cipher Spec” 和 “Encrypted Handshake Message” 消息,双方都解密 OK,握手正式结束。后续请求就收发被加密的 HTTP 数据了。
|
||||
|
||||

|
||||
|
||||
#### RSA 握手
|
||||
|
||||
上述是主流的 TLS 握手过程,较传统握手过程稍微复杂些。
|
||||
|
||||
- 使用 ECDHE 实现密钥交换,而不是 RSA,所以会在服务端发送 “Server Key Exchange” 消息
|
||||
|
||||
- 使用 ECDHE,客户端可以不用等服务器发送 “Encrypted Handshake Message” 确认握手完成,立即发出 HTTP 报文,省去了一个消息往返的时间,叫做 “TLS False Start”,抢跑。不等建立连接完全建立就提前发送应用数据,提高传输效率
|
||||
|
||||
#### 双向认证
|
||||
|
||||
上述讲了“单向认证”握手过程,只认证了服务器的身份,而没有认证客户端的身份。因为单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。
|
||||
|
||||
但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现“双向认证”,这样会更加安全。
|
||||
|
||||
双向认证的流程也没有太多变化,只是在 “Server Hello Done” 之后,“Client Key Exchange” 之前,客户端要发送 “Client Certificate” 消息,服务器收到后也把证书链走一遍,验证客户端的身份。
|
||||
|
||||
总结:
|
||||
|
||||
- HTTPS 协议会先与服务器执行 TCP 握手,然后执行 TLS 握手,才能建立安全连接
|
||||
|
||||
- 握手的目标是安全地交换对称密钥,需要三个随机数,第三个随机数“Pre-Master”必须加密传输,绝对不能让黑客破解
|
||||
|
||||
- “Hello” 消息交换随机数,“Key Exchange” 消息交换 “Pre-Master”
|
||||
|
||||
- “Change Cipher Spec” 之前传输的都是明文,之后都是对称密钥加密的密文
|
||||
|
||||
### TLS 1.3 最新特性
|
||||
|
||||
上述研究的是10年前的 TLS1.2,性能存在问题,2018年 TLS1.3 亮相:兼容、安全、性能是最大亮点。
|
||||
|
||||
#### 最大化兼容性
|
||||
|
||||
由于 TLS1.1、1.2 等协议已经出现了多年,很多应用软件、中间代理只认老的记录协议格式,更新改造很困难。早期试验发现,变更了记录头字段里的版本号,也就是由 0x303(TLS1.2)改为 0x304(TLS1.3),大量的代理服务器、网关都无法正确处理,最终导致 TLS 握手失败。
|
||||
|
||||
为了保证这些被广泛部署的“老设备”能够继续使用,避免新协议带来的“冲击”,TLS1.3 不得不做出妥协,保持现有的记录格式不变,通过“伪装”来实现兼容,使得 TLS1.3 看上去“像是”TLS1.2。
|
||||
|
||||
如何实现?通过**拓展协议**(Extentsion Protocol),在记录末尾处增加一系列的拓展字段来增加新的功能。老版本 TLS 不认识则可以直接忽略,所以达到了“向后兼容”。只要是 TLS1.3协议,握手的 “Hello” 小西湖都必须携带 “supported_versions” 拓展。
|
||||
|
||||
```shell
|
||||
Handshake Protocol: Client Hello
|
||||
Version: TLS 1.2 (0x0303)
|
||||
Extension: supported_versions (len=11)
|
||||
Supported Version: TLS 1.3 (0x0304)
|
||||
Supported Version: TLS 1.2 (0x0303)
|
||||
```
|
||||
|
||||
Tips:TLS1.3 利用拓展协议增加了很多特性:`supported_groups`、`key_share`、`signature_algorithms`、`server_name`
|
||||
|
||||
#### 强化安全
|
||||
|
||||
TLS1.2 应用多年,期间发现了很多加密算法的不足和漏洞,所以 TLS1.3 就在协议里进行了修改。
|
||||
|
||||
- 伪随机数函数由 PRF 升级为 HKDF(HMAC-based Extract-and-Expand Key Derivation Function)
|
||||
|
||||
- 明确禁止在记录协议里使用压缩
|
||||
|
||||
- 废除了 RC4、DES 对称加密算法
|
||||
|
||||
- 废除了 ECB、CBC 等传统分组模式
|
||||
|
||||
- 废除了 MD5、SHA1、SHA-224 摘要算法
|
||||
|
||||
- 废除了 RSA、DH 密钥交换算法和许多命名曲线
|
||||
|
||||
TLS1.3 里只保留了 AES、ChaCha20 对称加密算法;分组模式只能用 AEAD 的 GCM、CCM 和 Poly1305;摘要算法只能用 SHA256、SHA384;密钥交换算法只有 ECDHE 和 DHE;椭圆曲线也被“砍”到只剩 P-256 和 x25519 等 5 种。
|
||||
|
||||
算法精简后带来了一个意料之中的好处:原来众多的算法、参数组合导致密码套件非常复杂,难以选择,而现在的 TLS1.3 里只有 5 个套件,无论是客户端还是服务器都不会再犯“选择困难症”了。
|
||||
|
||||
QA:为什么废除 RSA、DH?
|
||||
|
||||
假设黑客获取了大量的密文,如果加密系统使用服务器证书里的 RSA 做密钥交换,一旦私钥泄漏或者被超级计算机破解,那么黑客就可以使用私钥解密处之前所有的报文“Pre-Master”,再计算出会话密钥,破解出积累的一堆密文。
|
||||
|
||||
而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥,每次通信的密钥对都是不同的,也就是“一次一密”,即使黑客花大力气破解了这一次的会话密钥,也只是这次通信被攻击,之前的历史消息不会受到影响,仍然是安全的。所以现在主流的服务器和浏览器在握手阶段都已经不再使用 RSA,改用 ECDHE,而 TLS1.3 在协议里明确废除 RSA 和 DH 则在标准层面保证了“前向安全”。
|
||||
|
||||
#### 提升性能
|
||||
|
||||
HTTPS 建立连接时除了要做 TCP 握手,还要做 TLS 握手,在 1.2 中会多花两个消息往返(2-RTT),可能导致几十毫秒甚至上百毫秒的延迟,在移动网络中延迟还会更严重
|
||||
|
||||
现在因为密码套件大幅度简化,也就没有必要再像以前那样走复杂的协商流程了。TLS1.3 压缩了以前的“Hello”协商过程,删除了“Key Exchange”消息,把握手时间减少到了“1-RTT”,效率提高了一倍。
|
||||
|
||||
做法:利用了扩展。客户端在 “Client Hello” 消息里直接用 “supported_groups” 带上支持的曲线,比如 P-256、x25519,用“key_share”带上曲线对应的客户端公钥参数,用 “signature_algorithms” 带上签名算法。服务器收到后在这些扩展里选定一个曲线和参数,再用 “key_share” 扩展返回服务器这边的公钥参数,就实现了双方的密钥交换,后面的流程就和 1.2 基本一样了。
|
||||
|
||||

|
||||
|
||||
### HTTPS 加速
|
||||
|
||||
大家都知道 “HTTPS 连接很慢”,但 why?
|
||||
|
||||
HTTPS 连接大致上可以划分为两个部分:建立连接时的非对称加密握手 + 握手后的对称加密报文传输。由于目前流行的 AES、ChaCha20 性能都很好,还有硬件优化,报文传输的性能损耗可以说是非常地小,小到几乎可以忽略不计了。所以 “TTPS 连接慢”指的就是刚开始建立连接的那段时间。
|
||||
|
||||
在 TCP 建连之后,正式数据传输之前,HTTPS 比 HTTP 增加了一个 TLS 握手的步骤,这个步骤最长可以花费两个消息往返,也就是 2-RTT。而且在握手消息的网络耗时之外,还会有其他的一些“隐形”消耗,比如:
|
||||
|
||||
- 产生用于密钥交换的临时公私钥对(ECDHE)
|
||||
|
||||
- 验证证书时访问 CA 获取 CRL 或者 OCSP
|
||||
|
||||
- 非对称加密解密处理“Pre-Master”
|
||||
|
||||
在最差的情况下,也就是不做任何的优化措施,HTTPS 建立连接可能会比 HTTP 慢上几百毫秒甚至几秒,这其中既有网络耗时,也有计算耗时,就会让人觉得“打开一个 HTTPS 网站好慢啊”
|
||||
|
||||
下图是存在改进空间的地方。
|
||||
|
||||

|
||||
|
||||
#### 硬件优化
|
||||
|
||||
计算机中优化分为软件层面和硬件层面。HTTPS 也是一样,它的特点是计算密集型,所以可以
|
||||
|
||||
- 选择更快的 CPU,最好还内建 AES 优化,这样可以加速握手、加速传输
|
||||
|
||||
- 选择 ”SSL 加速卡“,加解密时调用它的 API,让专门硬件来做非对称加密,分担 CPU 的计算压力。但加速卡存在缺点,比如升级慢、支持算法优先,不能灵活定制解决方案
|
||||
|
||||
- SSL 加速服务器,用专门的服务器集群来彻底“卸载 TLS 握手时的加解密计算”,性能也比单纯的加速卡好得多。加速服务器通信必须异步,否则就会阻塞,没有加速效果了
|
||||
|
||||
#### 软件优化
|
||||
|
||||
分为软件升级和协议优化。其中软件升级比较好理解,就是把正在使用的软件升级为最新版本。比如把 Nginx、OpenSSL、Linux 都升级为最新版本,这个也很好理解,就是 iOS 开发一样,我们的 App 要是用来自 iOS 系统库、也要使用来自 github 的三方库、公司内部的二方库,升级一般都是解决问题和性能优化,所以享受系统升级带来的红利。可能需要运维配合做一些措施,但是优化效果来说 ROI 最佳。
|
||||
|
||||
#### 协议优化
|
||||
|
||||
TLS1.2 握手需要消费2-RTT,TLS1.3 只需要1-RTT,如果可能,升级 TLS 到1.3。如果不能升级到 TLS1.3,那么可以将握手使用的密钥交换协议改为椭圆曲线的 ECDHE 算法,不仅运算速度快,安全性高,还支持 “False Start”,能够把握手的消息往返由2-RTT减少为1-RTT,达到与 TLS1.3一样的效果。
|
||||
|
||||
椭圆曲线也要选高性能的曲线,最好是 `x25519`,其次选择 `P-256`。
|
||||
|
||||
对称加密选择 `AES_128_GCM`,其次是 `AES_256_GCM`
|
||||
|
||||
Nginx 可以用 `ssl_ciphers`、 `ssl_ecdh_curve` 等指令配置服务器使用的密码套件和椭圆曲线,把优先使用的放在前面。
|
||||
|
||||
#### 证书优化
|
||||
|
||||
除了密钥交换,握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。
|
||||
|
||||
存在两个优化点:证书传输、证书验证。
|
||||
|
||||
服务器的证书可以选择椭圆曲线(ECDSA)证书而不是 RSA 证书,因为 224 位的 ECC 相当于 2048 位的 RSA,所以椭圆曲线证书的“个头”要比 RSA 小很多,即能够节约带宽也能减少客户端的运算量,可谓“一举两得”。
|
||||
|
||||
客户端的证书验证其实是个很复杂的操作,除了要公钥解密验证多个证书签名外,因为证书还有可能会被撤销失效,客户端有时还会再去访问 CA,下载 CRL 或者 OCSP 数据,这又会产生 DNS 查询、建立连接、收发数据等一系列网络通信,增加好几个 RTT。
|
||||
|
||||
CRL(Certificate revocation list,证书吊销列表)由 CA 定期发布,里面是所有被撤销信任的证书序号,查询这个列表就可以知道证书是否有效。但 CRL 因为是“定期”发布,就有“时间窗口”的安全隐患,而且随着吊销证书的增多,列表会越来越大,一个 CRL 经常会上 MB。想象一下,每次需要预先下载几 M 的“无用数据”才能连接网站,实用性实在是太低了。
|
||||
|
||||
所以,现在 CRL 基本上不用了,取而代之的是 OCSP(在线证书状态协议,Online Certificate Status Protocol),向 CA 发送查询请求,让 CA 返回证书的有效状态。但 OCSP 也要多出一次网络请求的消耗,而且还依赖于 CA 服务器,如果 CA 服务器很忙,那响应延迟也是等不起的。
|
||||
|
||||
于是又出来了一个“补丁”,叫“OCSP Stapling”(OCSP 装订),它可以让服务器预先访问 CA 获取 OCSP 响应,然后在握手时随着证书一起发给客户端,免去了客户端连接 CA 服务器查询的时间。
|
||||
|
||||
#### 会话复用
|
||||
|
||||
HTTPS 建立握手过程中:TCP 3次握手、TLS 一次握手。TLS 握手的核心就是为了计算出 Master Secret,而每次建立连接都要重新计算一次,这个太奢侈了,如果可以将主密钥缓存重用,就省去了握手和计算成本。这叫做“会话复用” TLS Session Resumption, 和 HTTP Cache 一样,可以可以 HTTPS 性能而被浏览器和服务器广泛使用。
|
||||
|
||||
会话复用分为2种。
|
||||
|
||||
第一种是 Session ID,也就是客户端和服务端首次连接后各自保存一个会话 ID,内存里存储主密钥和其他信息。当客户端再次连接时,会发送一个 ID,服务器在内存中查找,找到就直接用主密钥恢复会话状态,跳过证书验证和密钥交换,只有一个消息往返就可以建立安全通信。
|
||||
|
||||
Session ID 存在缺点,服务器必须保存每个客户端会话数据,对于大型服务来说存储成本太大,加重服务器负担。
|
||||
|
||||
第二种是 Session Ticket 方案。类似 HTTP Cookie,存储责任从服务器放到了客户端,服务器加密会话信息用 “New Session Ticket” 消息发送给客户端,让客户端保存。重连的时候客户端使用拓展 “session_ticket” 发送 “Ticket”,服务器解密后验证有效期,就可以恢复会话,开始加密通信。
|
||||
|
||||
Session Ticket 方案需要使用一个固定密钥文件(ticket_key)来加密 Ticket,为了防止密钥破解,保证“前向安全”,密钥文件需要定时更换,比如一小时或者一天。
|
||||
|
||||
#### 预共享密钥
|
||||
|
||||
`False Start` 、`Session ID`、 `Session Ticket`等方式只能实现 1-RTT,而 TLS1.3 更进一步实现了 0-RTT,原理和 `Session Ticket` 差不多,但在发送 Ticket 的同时会带上应用数据(Early Data),免去了 1.2 里的服务器确认步骤,这种方式叫 `Pre-shared Key`,简称为“PSK”。
|
||||
|
||||

|
||||
|
||||
PSK 存在缺点,为了追求效率而降低一些安全性,容易收到“重放攻击”,黑客截获 PSK 后,原封不动的向服务器发出去。解决办法是只允许安全的 HTTP 请求方法,如 HEAD/GET,在消息中增加时间戳、nonce 验证。
|
||||
|
||||
总结:
|
||||
|
||||
- 可以有多种硬件和软件手段减少网络耗时和计算耗时,让 HTTPS 变得和 HTTP 一样快,最可行的是软件优化
|
||||
|
||||
- 应当尽量使用 ECDHE 椭圆曲线密码套件,节约带宽和计算量,还能实现“False Start”
|
||||
|
||||
- 服务器端应当开启“OCSP Stapling”功能,避免客户端访问 CA 去验证证书
|
||||
|
||||
- 会话复用的效果类似 Cache,前提是客户端必须之前成功建立连接,后面就可以用 `Session ID`、 `Session Ticket`等凭据跳过密钥交换、证书验证等步骤,直接开始加密通信
|
||||
|
||||
## HTTP/2 特性
|
||||
|
||||
HTTP/1 的缺点是性能问题和安全性问题。安全性问题通过 HTTP Over SSL/TLS 也就是 HTTPS 解决了。但是在数据传输方面还是很差的。HTTP/2 解决的就是解决性能问题。
|
||||
|
||||
### 兼容 HTTP/1.1
|
||||
|
||||
TLS从1.2 升级的时候踩坑了(使用 TLS1.2 无法握手了),所以 HTTP/2 把 HTTP 拆分为“语义”和“语法”。语义”层不做改动,与 HTTP/1 完全一致(即 RFC7231),比如请求方法、URI、状态码、头字段等概念都保留不变,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。
|
||||
|
||||
HTTP/2 真正改变的是语法层
|
||||
|
||||
### 连接前言
|
||||
|
||||
TLS 握手成功之后,客户端必须要发送一个“连接前言”(connection preface),用来确认建立 HTTP/2 连接。连接前言是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字 `PRI`,全文只有 24 个字节
|
||||
|
||||
```shell
|
||||
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
|
||||
```
|
||||
|
||||
但在 wireshark 抓包中,HTTP/2 连接前言被称为 “Magic“。
|
||||
|
||||

|
||||
|
||||
### 头部压缩
|
||||
|
||||
HTTP/1 使用头部字段“Content-Encoding” 指定 Body 的编码方式,比如使用 gzip 压缩来节约贷款,但报文的头部 Header 被忽视了。
|
||||
|
||||
报文 Header 会携带:User Agent、Cookie、Accept、Server 等许多固定格式的头字段,多达成百上千字节,但 Body 通常只有几十字节(如GET请求、204/301/304响应),成千上万的请求响应报文中很多字段都是重复的,严重浪费,长尾效应导致大量带宽消耗在这些冗余度很高的数据上。
|
||||
|
||||
所以 HTTP/2 把“头部压缩”当作性能改进的一个发力点,采用压缩策略。专门研发了 “HPACK” 算法,HPACK 算法是一个有状态的算法,在客户端和服务器建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可达到50%~90%的高压缩率。
|
||||
|
||||
为了方便管理和压缩,HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,并且给这些“不是头字段的头字段”起了个特别的名字“伪头字段”(pseudo-header fields)。而起始行里的版本号和错误原因短语因为没什么大用,顺便也给废除了。为了与“真头字段”区分开来,这些“伪头字段”会在名字前加一个“:”,比如“:authority” “:method” “:status”,分别表示的是域名、请求方法和状态码。
|
||||
|
||||
现在 HTTP 报文头就简单了,全都是 `Key-Value` 形式的字段,于是 HTTP/2 就为一些最常用的头字段定义了一个只读的“静态表”(Static Table)。
|
||||
|
||||

|
||||
|
||||
完整的静态表可以查看[RFC 7541 - HPACK: Header Compression for HTTP/2](https://httpwg.org/specs/rfc7541.html#static.table.definition)
|
||||
|
||||
假设使用了自定义字段怎么办?动态表(Dynamic Table) 添加在静态表后面,结构相同,在编码的时候随时更新。
|
||||
|
||||

|
||||
|
||||
在 HTTP/2 连接上发送的报文越来越多,客户端、服务器的“字典”也会越来越丰富,最终每次的头部字段都会变成一两个字节的代码,原来上千字节的头用几十个字节就可以表示了,压缩效果比 gzip 要好得多。
|
||||
|
||||
### 二进制格式
|
||||
|
||||
HTTP/2 以前采用纯文本格式的报文(ASCII 码),但 HTTP/2 向 TCP/IP 协议靠拢,采用二进制格式。
|
||||
|
||||
虽对人不友好,但却方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在处理时必须用复杂的状态机,效率低,还麻烦。而二进制只有0和1,可以严格规定字段大小、顺序、标志位等格式,“对就是对,错就是错”,解析起来没有歧义,实现简单,而且体积小、速度快,做到“内部提效”。
|
||||
|
||||
把 TCP 协议的部分特性挪到应用层,将原来的 “Header + Body” 消息打散到为多个小片的二进制“帧 Frame”。用 `HEADERS` 帧存放头数据,`DATA` 帧存放实体数据。
|
||||
|
||||
这种策略有点像 `Chunked` 分块编码的方式,化整为零,但 HTTP/2 数据分帧后 “Header + Body” 的报文结构就没了,协议看到的是一个个碎片。
|
||||
|
||||

|
||||
|
||||
HTTP/2 的帧结构有点类似 TCP 的段或者 TLS 里的记录,但报头很小,只有 9 字节,非常地节省(可以对比一下 TCP 头,它最少是 20 个字节)。
|
||||
|
||||

|
||||
|
||||
帧开头是 3 个字节的长度(但不包括头的 9 个字节),默认上限是 2^14,最大是 2^24,也就是说 HTTP/2 的帧通常不超过 16K,最大是 16M。
|
||||
|
||||
长度后面的一个字节是帧类型,大致可以分成数据帧和控制帧两类,HEADERS 帧和 DATA 帧属于数据帧,存放的是 HTTP 报文,而 SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。
|
||||
|
||||
HTTP/2 总共定义了 10 种类型的帧,但一个字节可以表示最多 256 种,所以也允许在标准之外定义其他类型实现功能扩展。这就有点像 TLS 里扩展协议的意思了,比如 Google 的 gRPC 就利用了这个特点,定义了几种自用的新帧类型。
|
||||
|
||||
第 5 个字节是非常重要的帧标志信息,可以保存 8 个标志位,携带简单的控制信息。常用的标志位有 `END_HEADERS` 表示头数据结束,相当于 HTTP/1 里头后的空行(“\r\n”),`END_STREAM` 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\r\n\r\n”)。
|
||||
|
||||
报文头里最后 4 个字节是流标识符,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。流标识符虽然有 4 个字节,但最高位被保留不用,所以只有 31 位可以使用,也就是说,流标识符的上限是 2^31,大约是 21 亿。
|
||||
|
||||
### 流
|
||||
|
||||
碎片化的消息到底目的地如何组装?HTTP/2 为此定义了一个“流”(Stream)的概念,它是二进制帧的双向传输序列,**同一个消息往返的帧会分配一个唯一的流 ID**。你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。
|
||||
|
||||
因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的 **`多路复用`(Multiplexing)多个往返通信都复用一个连接来处理**
|
||||
|
||||
从“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。
|
||||
|
||||

|
||||
|
||||
HTTP/1 中请求-响应报文来回一次是一次 HTTP 通信,HTTP/2中一个流也做了类似的事情。
|
||||
|
||||
HTTP/2流的特点:
|
||||
|
||||
- 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”
|
||||
|
||||
- 客户端和服务器都可以创建流,双方互不干扰
|
||||
|
||||
- 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回
|
||||
|
||||
- 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的
|
||||
|
||||
- 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验
|
||||
|
||||
- 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数
|
||||
|
||||
- 在流上发送 `RST_STREAM` 帧可以随时终止流,取消接收或发送
|
||||
|
||||
- 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制
|
||||
|
||||

|
||||
|
||||
可以看到:
|
||||
|
||||
- HTTP/2 在一个连接上使用多个流收发数据,那么它本身默认就会是长连接,所以不需要 `Connection` 头字段(keepalive 或 close)
|
||||
|
||||
- HTTP/1 里只能断开 TCP 连接重新“三次握手”,成本很高,而在 HTTP/2 里就可以简单地发送一个 `RST_STREAM` 中断流,而长连接会继续保持
|
||||
|
||||
- 客户端和服务器两端都可以创建流,而流 ID 有奇数偶数和上限的区分,所以大多数的流 ID 都会是奇数,而且客户端在一个连接里最多只能发出 2^30,也就是 10 亿个请求
|
||||
|
||||
### 流状态转换
|
||||
|
||||

|
||||
|
||||
上图对应到标准的 HTTP 请求应答。
|
||||
|
||||
- 开始的时候流都是“空闲”(idle)状态,也就是“不存在”,可以理解成是待分配的“号段资源”
|
||||
|
||||
- 当客户端发送 HEADERS 帧后,有了流 ID,流就进入了“打开”状态,两端都可以收发数据
|
||||
|
||||
- 客户端发送一个带 `END_STREAM` 标志位的帧,流就进入了“半关闭”状态。这个“半关闭”状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据
|
||||
|
||||
- 响应数据发完了之后,也要带上“END_STREAM”标志位,表示数据发送完毕,这样流两端就都进入了“关闭”状态,流就结束了
|
||||
|
||||
- 流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的“请求 - 应答”,流关闭就是一次通信结束
|
||||
|
||||
- 下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送 `GOAWAY` 帧开一个新的 TCP 连接,流 ID 就又可以重头计数
|
||||
|
||||
### Server Push
|
||||
|
||||
HTTP/2 还在一定程度上改变了传统的“请求 - 应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push)。
|
||||
|
||||
### 强化安全
|
||||
|
||||
出于兼容的考虑,HTTP/2 延续了 HTTP/1 的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。
|
||||
|
||||
但由于 HTTPS 已经是大势所趋,而且主流的浏览器 Chrome、Firefox 等都公开宣布只支持加密的 HTTP/2,所以“事实上”的 HTTP/2 是加密的。也就是说,互联网上通常所能见到的 HTTP/2 都是使用“https”协议名,跑在 TLS 上面。
|
||||
|
||||
为了区分“加密”和“明文”这两个不同的版本,HTTP/2 协议定义了两个字符串标识符:
|
||||
|
||||
- `h2`表示加密的 HTTP/2
|
||||
|
||||
- `h2c` 表示明文的 HTTP/2,多出的那个字母“c”的意思是“clear text”
|
||||
|
||||
在 HTTP/2 标准制定的时候(2015 年)已经发现了很多 SSL/TLS 的弱点,而新的 TLS1.3 还未发布,所以加密版本的 HTTP/2 在安全方面做了强化,要求下层的通信协议必须是 TLS1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了“黑名单”,比如 DES、RC4、CBC、SHA-1 都不能在 HTTP/2 里使用,相当于底层用的是“TLS1.25”。
|
||||
|
||||
### 协议栈
|
||||
|
||||
HTTP/2 是建立在 HPack + Stream + TLS1.2 之上的。
|
||||
|
||||

|
||||
|
||||
QA:明文形式的 HTTP/2(h2c)有什么好处,应该如何使用呢?
|
||||
|
||||
h2c使用明文传输,速度更快,不需要TLS握手,按需选择
|
||||
|
||||
QA:应该怎样理解 HTTP/2 里的“流”,为什么它是“虚拟”的?
|
||||
|
||||
HTTP/2 中数据不再像 “Header + Body” 的数据包格式,将多个请求分成不同的流,每个流中切成不同的帧(Frame),包括 `HEADER Frame`、`DATA Frame` 。发送的时候按帧发送,为了保证帧到达目的地址后可以组装还原为真实数据,给每个帧添加了一个 `流 ID`,标记该帧属于哪个流,服务端收到按照流 ID 进行组装拼接。从传输角度看流是不存在的,因为传输的时候就是一个个的帧,所以是虚拟的。
|
||||
|
||||
QA:对比一下 HTTP/2 与 HTTP/1、HTTPS 的相同点和不同点吗?
|
||||
|
||||
相同点:
|
||||
|
||||
- 都是基于TCP和TLS的,url格式都是相同的
|
||||
|
||||
- 都是基于header+body的形式。都是请求-应答模型
|
||||
|
||||
不同点:
|
||||
|
||||
- 使用了HPACK进行头部压缩
|
||||
|
||||
- HTTP/2 参考 TCP/IP 使用的是二进制的方式进行传输
|
||||
|
||||
- 将多个请求切分成帧发送,实现了多路复用
|
||||
|
||||
- 服务器可以主动向客户端推送消息。充分利用了 TCP 的全双工通道
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP 协议取消了小版本号,所以 HTTP/2 的正式名字不是 2.0
|
||||
|
||||
- HTTP/2 在“语义”上兼容 HTTP/1,保留了请求方法、URI 等传统概念
|
||||
|
||||
- HTTP/2 使用 “HPACK” 算法压缩头部信息,消除冗余数据节约带宽
|
||||
|
||||
- HTTP/2 的消息不再是“Header+Body”的形式,而是分散为多个二进制“帧”
|
||||
|
||||
- HTTP/2 使用虚拟的“流”传输消息,解决了困扰多年的“队头阻塞”问题
|
||||
|
||||
- 在一个 HTTP/2 连接上可以并发多个流,也就是多个“请求 - 响应”报文,这就是“多路复用”,提高连接的利用率
|
||||
|
||||
- HTTP/2 也增强了安全性,要求至少是 TLS1.2,而且禁用了很多不安全的密码套件
|
||||
|
||||
- HTTP/2 必须先发送一个“连接前言”字符串,然后才能建立正式连接
|
||||
|
||||
- HTTP/2 废除了起始行,统一使用头字段,在两端维护字段 `Key-Value`的索引表,使用“HPACK”算法压缩头部
|
||||
|
||||
- HTTP/2 把报文切分为多种类型的二进制帧,报头里最重要的字段是流标识符,标记帧属于哪个流
|
||||
|
||||
- 流是 HTTP/2 虚拟的概念,是帧的双向传输序列,相当于 HTTP/1 里的一次“请求 - 应答”
|
||||
|
||||
## HTTP/3
|
||||
|
||||
HTTP/2 通过头部压缩、二进制分帧、虚拟的“流”、多路复用,性能方面比 HTTP/1 有了很大的提升,基本上解决了“队头阻塞”。
|
||||
|
||||
为什么叫基本上?HTTP/2 虽然使用帧、流、多路复用,看上去解决了“队头阻塞”,但这些手段都是在应用层里,而在下层,传输还是需要依靠 TCP 协议,还是会发生“队头阻塞”。因为HTTP/2 把多个“请求 - 响应”分解成流,交给 TCP 后,TCP 会再拆成更小的包依次发送(其实在 TCP 里应该叫 segment,也就是“段”),在网络良好的情况下,包可以很快送达目的地。但如果网络质量比较差,就有可能丢包。而 TCP 为了保证可靠传输,有个特别的 "**丢包重传**”机制,丢失的包必须要等待重新传输确认,其他的包即使已经收到了,也只能放在缓冲区里,上层的应用拿不出来,还是存在队头阻塞。
|
||||
|
||||
比如客户端用 TCP 发送了三个包,但服务器所在的操作系统只收到了后两个包,第一个包丢了。那么内核里的 TCP 协议栈就只能把已经收到的包暂存起来,“停下”等着客户端重传那个丢失的包,这样就又出现了“队头阻塞”。
|
||||
|
||||
队头阻塞是 TCP 的基本特征,所以上层的 HTTP 再优化也解决不了。
|
||||
|
||||
HTTP Over QUIC 就是 HTTP3,完美解决队头阻塞问题。
|
||||
|
||||
### QUIC 协议
|
||||
|
||||

|
||||
|
||||
可以看到 HTTP3 对比 HTTP2 将 TCP 换为 UDP,因为 UDP 无序,包之间没有依赖关系,所以从根本上解决了“队头阻塞”问题
|
||||
|
||||
QUIC 选择 UDP,在之上将 TCP 那一套连接管理、拥塞窗口、流量控制搬来,集大成者。
|
||||
|
||||
### QUIC 特点
|
||||
|
||||
- 快:QUIC 基于 UDP,而 UDP 是“无连接”的,根本就不需要“握手”和“挥手”,所以天生就要比 TCP 快。
|
||||
|
||||
- 可靠传输:TCP 在 IP 的基础上实现了可靠传输一样,QUIC 也基于 UDP 实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的“流”和“多路复用”,单个“流”是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响
|
||||
|
||||
- 安全:为了防止网络上的中间设备(Middle Box)识别协议的细节,QUIC 全面采用加密通信,可以很好地抵御窜改和“协议僵化”(ossification)
|
||||
|
||||
- 0-RTT/1-RTT:TLS1.3 已经在去年(2018)正式发布,所以 QUIC 就直接应用了 TLS1.3,顺便也就获得了 0-RTT、1-RTT 连接的好处
|
||||
|
||||
QUIC 并不是建立在 TLS 之上,而是内部“包含”了 TLS。它使用自己的帧“接管”了 TLS 里的“记录”,握手消息、警报消息都不使用 TLS 记录,直接封装成 QUIC 的帧发送,省掉了一次开销。
|
||||
|
||||
### QUIC 结构
|
||||
|
||||
QUIC 的基本数据传输单位是包(packet)和帧(frame),一个包由多个帧组成,包面向的是“连接”,帧面向的是“流”。
|
||||
|
||||
QUIC 使用不透明的 “**连接 ID**” 来标记通信的两个端点,客户端和服务器可以自行选择一组 ID 来标记自己,这样就解除了 TCP 里连接对“IP 地址 + 端口”(即常说的四元组)的强绑定,支持“连接迁移”(Connection Migration)
|
||||
|
||||

|
||||
|
||||
比如从外面玩回到家,手机会自动由 4G 切换到 WiFi。这时 IP 地址会发生变化,TCP 就必须重新建立连接。而 QUIC 连接里的两端连接 ID 不会变,所以连接在“逻辑上”没有中断,它就可以在新的 IP 地址上继续使用之前的连接,消除重连的成本,实现连接的无缝迁移。
|
||||
|
||||
QUIC 的帧里有多种类型,PING、ACK 等帧用于管理连接,而 STREAM 帧专门用来实现流。
|
||||
|
||||
QUIC 里的流与 HTTP/2 的流非常相似,也是帧的序列。但 HTTP/2 里的流都是双向的,而 QUIC 则分为双向流和单向流。
|
||||
|
||||

|
||||
|
||||
QUIC 帧普遍采用变长编码,最少只要 1 个字节,最多有 8 个字节。流 ID 的最大可用位数是 62,数量上比 HTTP/2 的 2^31 大大增加。
|
||||
|
||||
流 ID 还保留了最低两位用作标志,第 1 位标记流的发起者,0 表示客户端,1 表示服务器;第 2 位标记流的方向,0 表示双向流,1 表示单向流。所以 QUIC 流 ID 的奇偶性质和 HTTP/2 刚好相反,客户端的 ID 是偶数,从 0 开始计数。
|
||||
|
||||
### HTTP3 协议
|
||||
|
||||
因为 QUIC 本身就已经支持了加密、流和多路复用,所以 HTTP/3 的工作减轻了很多,把流控制都交给 QUIC 去做。调用的不再是 TLS 的安全接口,也不是 Socket API,而是专门的 QUIC 函数。不过这个“QUIC 函数”还没有形成标准,必须要绑定到某一个具体的实现库。
|
||||
|
||||
HTTP/3 里仍然使用流来发送“请求 - 响应”,但它自身不需要像 HTTP/2 那样再去定义流,而是直接使用 QUIC 的流,相当于做了一个“概念映射”。
|
||||
|
||||
HTTP/3 里的“双向流”可以完全对应到 HTTP/2 的流,而“单向流”在 HTTP/3 里用来实现控制和推送,近似地对应 HTTP/2 的 0 号流。由于流管理被“下放”到了 QUIC,所以 HTTP/3 里帧的结构也变简单了。帧头只有两个字段:类型和长度,而且同样都采用变长编码,最小只需要两个字节。
|
||||
|
||||

|
||||
|
||||
HTTP/3 里的帧仍然分成数据帧和控制帧两类,HEADERS 帧和 DATA 帧传输数据,但其他一些帧因为在下层的 QUIC 里有了替代,所以在 HTTP/3 里就都消失了,比如 RST_STREAM、WINDOW_UPDATE、PING 等。
|
||||
|
||||
头部压缩算法在 HTTP/3 里升级成了“QPACK”,使用方式上也做了改变。虽然也分成静态表和动态表,但在流上发送 HEADERS 帧时不能更新字段,只能引用,索引表的更新需要在专门的单向流上发送指令来管理,解决了 HPACK 的“队头阻塞”问题。
|
||||
|
||||
另外,QPACK 的字典也做了优化,静态表由之前的 61 个增加到了 98 个,而且序号从 0 开始,也就是说“:authority”的编号是 0。
|
||||
|
||||
### 服务发现
|
||||
|
||||
HTTP/3 没有指定默认的端口号,也就是说不一定非要在 UDP 的 80 或者 443 上提供 HTTP/3 服务。
|
||||
|
||||
这就要用到 HTTP/2 里的“扩展帧”了。浏览器需要先用 HTTP/2 协议连接服务器,然后服务器可以在启动 HTTP/2 连接后发送一个 `Alt-Svc` 帧,包含一个 `h3=host:port` 的字符串,告诉浏览器在另一个端点上提供等价的 HTTP/3 服务。浏览器收到 `Alt-Svc` 帧,会使用 QUIC 异步连接指定的端口,如果连接成功,就会断开 HTTP/2 连接,改用新的 HTTP/3 收发数据。
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP/3 基于 QUIC 协议,完全解决了“队头阻塞”问题,弱网环境下的表现会优于 HTTP/2
|
||||
|
||||
- QUIC 是一个新的传输层协议,建立在 UDP 之上,实现了可靠传输
|
||||
|
||||
- QUIC 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连
|
||||
|
||||
- QUIC 的连接使用“不透明”的连接 ID,不绑定在“IP 地址 + 端口”上,支持“连接迁移”
|
||||
|
||||
- QUIC 的流与 HTTP/2 的流很相似,但分为双向流和单向流
|
||||
|
||||
- HTTP/3 没有指定默认端口号,需要用 HTTP/2 的扩展帧 `Alt-Svc` 来发现。
|
||||
|
||||
## Socket/WebSocket
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [HTTP/2 协议-HPACK(HTTP2 头部压缩算法)原理介绍_爱因诗贤的博客-CSDN博客_hpack算法](https://blog.csdn.net/qq_38937634/article/details/111410191)
|
||||
@@ -1,8 +1,7 @@
|
||||
# 第五部分
|
||||
|
||||
第五部分主要记录在计算机网络知识
|
||||
|
||||
* [1、HTTP请求头Range](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [1、HTTP/HTTPS 细节探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [2、认识HTTP、TCP、UDP](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.2.md)
|
||||
* [3、你知道字节序吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
|
||||
* [4、自定义报头协议](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.4.md)
|
||||
|
||||
Reference in New Issue
Block a user