Files
knowledge-kit/Chapter5 - Network/5.1.md
2024-06-29 16:00:34 +08:00

86 KiB
Raw Blame History

HTTP/HTTPS 细节探索

TCP/UDP

TCP 传输的核心公式:速度 = 窗口大小/往返时间,这个公式对于理解传输本质和排查传输问题具有很强的知道意义。

TCP 里面有三种窗口发送窗口、接收窗口、拥塞窗口。如果没有特别说明TCP Window 指的是接收窗口。

TCP Window Full指的是在途数据的大小等于接收窗口大小时窗口会“满”

Wireshark 分析得到的信息都会用方括号包起来TCP 报文本身的信息,没有方括号。

TCP 如何探测到拥塞

TCP 传输的起始阶段,速度都是从低到高升上来的,很少一上来就以最终速度运行的情况。

这个机制其实就是 TCP 的拥塞控制。

TCP 拥塞控制

TCP 使用拥塞机制来确保传输速度和稳定性。拥塞机制是通信双方自己要实现的功能,在途中的网络设备(交换机、路由器)转发,不关心拥塞机制。

拥塞机制包括:慢启动、拥塞避免、快速重传、快速回复。

慢启动

Slow start即 TCP 传输的开始阶段是从一个相对低的速度开始的。之后拥塞窗口会翻倍方式增长。每次 TCP 收到一个确认了数据的 ACK拥塞窗口就增加1个 MSS

当收到重复的 ACK 报文即确认号一样比如收到2个 ACK但他们的确认号一样那么第二个 ACK 就不算是“确认数据的 ACK”拥塞窗口就不会增加2个 MSS只会增加1个 MSS。

会持续增长吗?当然不是,有终止条件:

  • 遇到了拥塞

  • 拥塞窗口增长到慢启动阈值

注意慢启动阶段并不是“每过1RTT 就翻倍”也可能会比翻倍少一些。什么意思呢慢启动阶段TCP 每收到一个 ACK拥塞窗口就增加1MSS。假设初始拥塞窗口大小为2MSS发送2个数据报之后

  • 收到1个ACK间隔确认那么在1RTT内拥塞窗口从2变为3没有翻倍

  • 收到2个ACK那么在1RTT内拥塞窗口从2变为4翻倍了

慢启动阈值

慢启动阈值 ssthresh过了这个阈值拥塞窗口的增长速度就立刻变慢了变为每过一个 RTT拥塞窗口就增加一个 MSS之前是没收到一个确认数据的 ACK 就增加1个 MSS

上图所示,假设 ICW 是4个 MSSssthresh 是32个 MSS慢启动阶段经过1个 RTT 后CW 扩大为8MSS、16MSS、32MSS。等到了阈值之后TCP就进入拥塞避免阶段了。每过一个 RTT拥塞窗口只增加1MSS曲线就变为较为斜率较低的直线了。

QA如果拥塞窗口大小正好等于慢启动阈值那么发送方这时候是需要采用拥塞避免过程线性增长还是继续选择慢启动过程指数增长RFC5681 规定是说两者都可以。

间隔确认

很多 TCP 的实现中,如果收到连续多个报文,确认报文是间隔一个进行回复的。

比如发送方发送1、2接收方收到1、2此时针对2进行确认。发送方发送3、4接收方针对4进行确认。这样的机制下会使得拥塞窗口的增长速度比每次 ACK 更低一些(更稳)

拥塞窗口

Congestion Window拥塞窗口简写 CW。拥塞窗口是针对每个连接进行维护的比如某个主机有3个 TCP 连接在传输数据那么这3个连接就各自维护自己的拥塞窗口。

初始化拥塞窗口Initial Congestion Window简写为 ICWIW

在 Linux 内核3.0以前ICW 的比较小在2到4个MSS2010年谷歌提出为了充分利用现代互联网的传输能力Linux 应该把 ICW 从24个MSS提升到10MSS。这也被应用到 Linux 内核3.0及其以后的版本中规定了 TCP_INIT_CWND 为10.

#define TCP_INIT_CWND 10

这个值在慢启动阶段影响了传输速度慢启动阶段每经过1个RTT拥塞窗口就翻倍所以不同的 ICW 就会造成不同的传输速度。

ICW 1RTT后 2RTT后 3RTT后
2 4 8 16
10 20 40 80

拥塞避免

TCP 野蛮生长后当达到慢启动阈值之后会进入拥塞避免阶段。这个阶段的特点是“和性增长乘性降低”Addictive increase/multiplicative decreaseAIMD解释下就是拥塞避免阶段每个RTT时间拥塞窗口只增长1MSS这个阶段的拥塞窗口增长是线性的斜率比较低的直线当探测到拥塞时拥塞窗口就要往下降下降是直接减半的叫做乘性降低。

窗口和 MSS 的关系

窗口一般比MSS大MSS 是有确定上限的一般为1460当然实际情况下值可能更低。

窗口一般是n个MSS。

快速重传

TCP 每发送一个报文,就会启动一个超时计时器,若在限定时间内没有收到这个报文的确认,则发送方认为这个报文在网络上丢了,需要发送方重传这个报文,这个机制叫做“超时重传”。

TCP 最小超时重传时间为200ms在这个机制下虽然解决了丢包问题但带来一个新的问题某个包都已经丢失了还需要等待200ms或者更长时间那体验不就更糟糕了吗

TCP 会利用另一种机制来解决超时重传带来的时间等待问题就是快速重传机制一旦发送方收到3次重复确认确认 ACK 报文中有确认号加上第一次确认就一共4次就不用等待超时计时器了直接重传这个报文。

注意:快速重传看的是数据,超时重传看的是时间。

快速恢复

快速恢复是 TCP Reno 算法引入的一个阶段,是和“快速重传”搭配工作的。跟之前的“慢启动-拥塞避免-慢启动-拥塞避免”不同的是,当遇到拥塞点之后,通过快速重传,就不再进入慢启动了,而是从这个减半的拥塞窗口开始,保持跟拥塞避免一样的线性增长,直到遇到下一个拥塞点。

注意:“快速恢复”有几个小细节

  • 快速恢复一定是和快速重传搭配使用的。那有何表现或者为什么这么设计如果在快速重传的场景下也就是收到连续3次重复确认 ACK 报文那么这个特征可以理解为“网络虽然有点问题但是能收到3次重复 ACK 报文网络也没那么糟糕”所以如果走传统的拥塞避免直接从0开始就很慢也很浪费效率但如果按照拥塞避免直接减半之后再走慢启动也很浪费网络所以当遇到一个拥塞点之后减半就按照拥塞避免线性增长

总结:慢启动不只是在 TCP 连接启动的阶段才发生传输的过程中遇到网络较差也会发生多次慢启动。一旦拥塞避免阶段探测到了拥塞TCP 还是会回到慢启动过程,只不过这个慢启动阈值跟之前的不同,如果有多次拥塞,会重复这个过程直到传输结束。

总结

  • 慢启动:每收到一个 ACK拥塞窗口CW增加一个 MSS

  • 拥塞避免:策略是“和性增长乘性降低”,每一个 RTTCW 增加一个 MSS

  • 快速重传:接收到 3 次或者以上的重复确认后,直接重传这个丢失的报文

  • 快速恢复:结合快速重传,在遇到拥塞点后,跳过慢启动阶段,进入线性增长

拥塞窗口CW和接收窗口RW是如何决定了传输速度上限的简单来说

  • 当 RW > CW 时,速度由 CW 决定

  • 当 RW < CW 时,速度由 RW 决定

HTTP 特点及解决方案

无连接

因为 HTTP 无连接,客户端和服务端交互的时候,打开一个 TCP 连接,然后交互,然后关闭 TCP 连接。下次需要交互的时候,继续打开一个 TCP 连接,继续交互,最后又关闭 TCP 连接。

为了解决该问题HTTP 推出持久连接方案。

涉及哪些头部字段?

  • Connection: keep-alive:客户端期许使用持久连接
  • time: 2020s 内不会进行四次挥手关闭。20s 内发送请求会复用之前的 TCP 连接
  • max 10:最多可以发送多少个请求和响应对。

怎么样判断一个请求是否结束了2个方案

  • 服务端响应的时候会在 header 中携带 Content-length: 1024 如果接受到的数据字节数大小,等于 Content-length 则说明已经全部接受完毕。
  • chunked :使用 post 请求的时候,服务端返回给客户端是通过多个块返回的。每个报文都带有 chunked 这个字段,最后一个报文会带一个空的 chunked。

无状态 - Cookie/Session

Session、Cookie 都是针对 HTTP 协议无状态特点的补偿。

Cookie 主要用来记录用户状态,区分用户;状态保存在客户端。

客户端请求服务端的时候,服务端生成 Cookie通过 HTTP 响应报文Header 部分中 Set-Cookie 首部字段设置 Cookie。

  • 客户端发送的 Cookie 在 HTTP 请求报文的 Cookie 首部字段中
  • 服务端设置 HTTP 的响应报文 Set-Cookie 首部字段

怎么修改 Cookie

新 Cookie 覆盖旧 Cookie。

覆盖规则name、path、domain 等需要与原 Cookie 一致。

怎么删除 Cookie

  • 新 Cookie 覆盖旧 Cookie。覆盖规则name、path、domain 等需要与原 Cookie 一致。
  • 设置 Cookie 的过期时间在过去。比如 expires = 过去的一个时间点,或者 maxAge = 0

怎么保证 Cookie 的安全?

  • 对 Cookie 进行加密处理
  • 只在 HTTPS 上携带 Cookie
  • 设置 Cookie 为 httponly防止跨站脚本攻击

Session

Session 也用来记录用户状态,区分用户。只不过状态保存在服务端。

Session 需要依赖于 Cookie 机制。

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-Controlmax-age=0”。代表着浏览器告诉服务器我需要一个最新的数据。

点击强制刷新,浏览器会在请求头将 If-Modified-SinceIf-None-Match 清空。所以服务器会返回最新的数据(当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200)

将网页点击前进、后退会发现“ Status Code200 from disk cache”

条件请求

早期浏览器可以用2个连续请求完整缓存验证第一个 HEAD获取资源的修改时间等信息然后与本地缓存数据相比较如果没有修改则使用缓存节省网络流量否则就发第二个 GET 请求,获取最新的资源数据。

但是2个请求网络成本太高所以 HTTP 协议就定义了一系列 If 开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。

条件请求共5个头字段常用的是 If-Modified-Since + Last-modifiedIf-None-Match + ETag这两个(需要搭配使用)。需要在第一次响应报文预先提供 Last-modifiedETag,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果服务端资源没有更新,则返回 “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结束。

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 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPSrequest per second降低好几个数量级减轻应用服务器的并发压力对性能的改善是非常显著的。

代理服务器没有缓存的时候:代理服务器每次直接转发来自客户端的报文给服务端,转发服务端的报文给客户端,中间不会存储任何数据,只有基础的中转功能。

有了缓存后:

  • 把报文转发给客户端

  • 把报文存储到缓存中

这样下次有相同的请求,代理服务器就可以直接返回 304 或者缓存数据,不必再从源服务器那里获取一次数据,降低了客户端等待时间、节约了源服务器的带宽

源服务器的缓存控制

客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件。

额外增加2个新属性

  • private表示缓存只能在客户端保存是用户“私有”的不能放在代理上与别人共享。比如你访问购物网站返回的响应报文里用 "Set-Cookie" 添加了 token这就属于私人数据不能存在代理上

  • public表示缓存完全开放谁都可以存谁都可以用

关于缓存失效后的验证有一些新的属性:

  • proxy-revalidate只要求代理的缓存过期后必须验证客户端不必回源只验证到代理

  • s-maxages即 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: bytes=start-end

例如:

Range: bytes=10- //从第10个字节开始到最后一个字节的数据
Rangebytes=20-39 //从第20个字节到第39个字节之间的数据

注意:整个表示 [start, end] 是前闭后闭的,也就是包含请求头的 start 和 end。所以下次请求应该是 [end+1, nextEnd]。

Content-Range 响应头

Content-Range:bytes 0-10/3000 表示服务器返回了前0-10个字节的数据总共3000字节的数据。

Content-Type 数据类型

Content-Type:image/png 表示资源类型是 png 格式的图片

Content-Length 资源的长度

Content-Length:11 表示服务器响应了11个字节的数据

Last-Modified

Last-Modified:Tue, 30 Jun 2018 03:12:48 GMT 表示资源最近被修改的时间,如果分批下载的时候发现 Last-Modified 被修改了,那么需要重新下载

ETag

ETag: W/&quot;3103-1435633968000&quot; 表示资源版本的标示符。通常是消息摘要类似MD5。分段下载时需要注意或者缓存控制也需要注意。如果是分布式缓存系统需要确保每台计算机的 ETag 计算规则的一致性,缓存的过期需要结合 ETag 和 Last-Modified 共同决定。

分段下载

利用 HTTP 的头信息的上述几个特点,我们可以充分利用多线程的能力。

  • 先发送一个 HEAD 方法的请求知道总文件大小Content-Length 就是总字节大小)
  • 多线程下载线程1:Range:bytes=0-100线程2:Range:bytes=100-200,...

Charles 抓包原理

抓包原理其实就是HTTP 中间人攻击。

DNS 解析

域名到 IP 地址的映射DNS 解析请求采用 UDP 数据报,且明文传输。

DNS 解析方式

DNS 解析查询方式:

  • 递归查询,核心就是“我去给你问一下”

    客户端根据网址去请求服务器之前,会先获取 IP 地址信息。

    • 先去本地 DNS 服务器,本地 DNS 服务器可以处理结果(根据域名对应到 IP 数据)则直接返回给客户端
    • 如果不能解析,则会去请求根域 DNS 服务器,根域 DNS 告诉本地 DNS“你先等一下我去问问顶级 DNS”
  • 迭代查询,核心就是“我告诉你谁可能知道”

    • 客户端发送请求的时候问一下本地 DNS 服务器,该域名对应的 IP 地址是什么,本地 DNS 服务器说“我不知道,你去问问根域名服务器,它可能知道”

    • 然后客户端去问根域 DNS 服务器,根域 DNS 服务器说“我也不知道,你去问问顶级 DNS 服务器,它可能知道”

    • 然后客户端去问顶级 DNS 服务器,顶级 DNS 服务器说“我也不知道,你去问问权限 DNS 服务器,它可能知道”

    • 然后权限 DNS 服务器把域名对应的 IP 告诉客户端,客户端拿到 IP 后去请求

DNS 解析存在哪些常见问题

最容易遇到DNS 劫持问题、DNS 解析转发问题

DNS 劫持问题

什么是 DNS 劫持

由于 DNS 解析是采用 UDP 数据包、明文传输,所以很容易遇到中间人攻击,也就是 DNS 劫持。

QADNS 劫持和 HTTP 的关系是什么?

DNS 劫持和 HTTP 是没有关系的

  • DNS 解析是发生在 HTTP 连接建立前
  • DNS 解析请求采用 UDP 数据报端口为53
如何解决 DNS 劫持问题
  • httpDNS

    从“使用 DNS 协议向 DNS 服务器的53端口请求” 变成“使用 HTTP 协议向 HTTP 服务器的80端口请求”

    客户端通过 IP 直连的方式,向 DNS 服务器,通过 HTTP Get 请求的方式,携带域名参数,然后响应一个具体的 IP 地址值给客户端。剩余流程就是拿着请求后的 IP 地址去完成其他逻辑。

  • 长连接

    在客户端和业务服务器之间,建立一个长连接 Server可以理解成代理服务器。

    客户端和长连接 Server 建立一个长连接通道。客户端可以发送一个 HTTP 请求,通过长连通道将 HTTP 请求发送给长连 Server。

    长连 Server 可以通过内网专线的方式,进行 HTTP 的请求和响应。也就是说 DNS 解析这步还存在只不过是发生在内网专线阶段避免了在外部公网阶段DNS 劫持的问题。

DNS 解析转发

比如某移动 App 发起网络请求,移动 DNS 服务器为了节省资源,将请求转发到某电信 DNS 服务器,用于帮助移动 DNS 服务器,解析域名,获取对应的 IP 地址。

这个电信 DNS 服务器,会向权威 DNS 服务器去请求解析域名对应的 IP 地址。

权限 DNS 会根据不同运营商请求情况(网络请求)的流量调度分发:

移动返回是2.2.2.2电信返回是3333这种情况下客户端在通过电信网络请求移动 DNS 服务器,进而转发到电信 DNS 服务器之后,权威 DNS 会返回3.3.3.3,也就是客户端在移动环境,由于 DNS 解析转发,而返回的 DNS 是3.3.3.3的电信环境,造成了跨网访问的效率问题

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

  • AESAdvanced Encryption Standard高级加密标准”密钥长度可以是 128、192 或 256。是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法

  • ChaCha20Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES曾经在移动客户端上比较流行但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错的算法

加密分组模式

对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文。明文的长度不固定,而密钥一次只能处理特定长度的一块数据,这就需要进行迭代,以便将一段很长的明文全部加密,而迭代的方法就是分组模式。

比如 AES128-GCM,意思是密钥长度为 128 位的 AES 算法,使用的分组模式是 GCMChaCha20-Poly1305 的意思是 ChaCha20 算法,使用的分组模式是 Poly1305。

对称加密、非对称加密

对称加密实现了机密性,但有一个很大的问题:如何将密钥安全地传输给对方,也就是"密钥交换"。诞生了非对称加密。拥有2个密钥公钥、私钥。

公钥可以公开给任何人使用,私钥必须秘密保存。

私钥、公钥都是“单向”的。也就是公钥加密后只能私钥解密,私钥加密后只能公钥解密。

非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。非对称加密也叫“公钥加密算法”的原理。

RSA 是非对称加密中最著名的一个,可以说是非对称加密的代名词。“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。

RSA 公开密钥密码体制的原理是:根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

10年前 RSA 密钥推荐长度为1024位但随着计算机运算能力的提高1024位已经不够安全了目前需要2048位。

ECC(Elliptic Curve Cryptography) 是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换ECDSA 用于数字签名。

混合加密

非对称的优点也是缺点:因为非对称加密密钥“密钥交换”的问题,但是是基于复杂的数学难题实现的,所以运算速度比较慢。下面是网上看到的测试速度

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 生成伪随机数(PRFpseudo 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、Lets Encrypt 等,它们签发的证书分 DV、OV、EV 三种区别在于可信程度。DV 是最低的只是域名级别的可信背后是谁不知道。EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)

CA 如何自证?

信任链。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA就只能自己证明自己了这个就叫“自签名证书”Self-Signed Certificate或者“根证书”Root Certificate。你必须相信否则整个证书信任链就走不下去了。

浏览器内置各大 CA 的根证书上网的时候只要服务器发过来它的证书就可以验证证书里的签名顺着证书链Certificate Chain一层层地验证直到找到根证书就能够确定证书是可信的从而里面的公钥也是可信的。

证书体系的弱点

证书体系(PKIPublic 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 OpenTFO 是用来加速连续 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 TypeChange Cipher Spec(20)

20对应16进制14。翻到最顶上看客户端的加密套件列表Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014).

QA为什么必须是3个随机数

TLS 的设计者认为不能信任客户端和服务器伪随机数的可靠性为了保证真正的“完全随机”、“不可预测”将3个随机数混合起来结果会更加随机大大增加黑客破解成本更加难以预测。

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” 拓展。

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)

TipsTLS1.3 利用拓展协议增加了很多特性:supported_groupskey_sharesignature_algorithmsserver_name

强化安全

TLS1.2 应用多年,期间发现了很多加密算法的不足和漏洞,所以 TLS1.3 就在协议里进行了修改。

  • 伪随机数函数由 PRF 升级为 HKDFHMAC-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-RTTTLS1.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_ciphersssl_ecdh_curve 等指令配置服务器使用的密码套件和椭圆曲线,把优先使用的放在前面。

证书优化

除了密钥交换,握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。

存在两个优化点:证书传输、证书验证。

服务器的证书可以选择椭圆曲线ECDSA证书而不是 RSA 证书,因为 224 位的 ECC 相当于 2048 位的 RSA所以椭圆曲线证书的“个头”要比 RSA 小很多,即能够节约带宽也能减少客户端的运算量,可谓“一举两得”。

客户端的证书验证其实是个很复杂的操作,除了要公钥解密验证多个证书签名外,因为证书还有可能会被撤销失效,客户端有时还会再去访问 CA下载 CRL 或者 OCSP 数据,这又会产生 DNS 查询、建立连接、收发数据等一系列网络通信,增加好几个 RTT。

CRLCertificate 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 StartSession IDSession 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 IDSession 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 个字节

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

假设使用了自定义字段怎么办?动态表(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 表示单方向数据发送结束(即 EOSEnd 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/2h2c有什么好处应该如何使用呢

h2c使用明文传输速度更快不需要TLS握手按需选择

QA应该怎样理解 HTTP/2 里的“流”,为什么它是“虚拟”的?

HTTP/2 中数据不再像 “Header + Body” 的数据包格式,将多个请求分成不同的流,每个流中切成不同的帧(Frame),包括 HEADER FrameDATA 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-RTTTLS1.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

参考资料