该文章假设读者拥有基础的计算机网络基础
今年六月份,IETF(互联网工程任务组)宣布了 HTTP/3 标准,编号为 RFC 9114。
上图是在同一个浏览器中,使用不同版本 HTTP 协议,访问同一个网站 20 次的数据;
相信不少人会和我一样,心想“我连 HTTP/2 都还没搞明白,怎么就开始谈论甚至使用 HTTP/3 了,真让人头大”。
与其放任头大,不如让我们借着 HTTP/3 的东风,捋一下 HTTP 发展历程。
你也许纳闷,为什么三次握手是 1 RTT?
这是因为客户端发送的第三个握手包不需要确认,不会造成时延,也就是发送第三个包的下一瞬间就可以发送后续数据,所以说是 1 RTT
热芝士:上文之所以说 TLS “一般”是 2RTT,是因为在 TLS1.3 中,可以做到1-RTT甚至0-RTT
时间拨回1991年,此时的 TCP 已经是一个可靠、成熟的协议。
而 Tim Berners-Lee 基于 TCP 设计了一个简单的超文本传输协议来管理 Web;
而该协议后来被称为 HTTP/0.9,它可以让你从 Web 服务器中获取资源。
这一阶段的 HTTP 非常简单,没有 header,没有状态码,只有一个简单的响应
当然,它已经彻底成为历史了,Bye~
作为第一个正式的 HTTP 版本,HTTP/1.0 由 1996 年发布的 RFC1945 草案公布。
让我们来考古一下 HTTP/1.0 有哪些特性:
在 HTTP/1.0 版本中,每次进行 HTTP 请求都要重复进行三次握手和四次挥手,不能提供持久化连接,十分影响效率,于是,它来了—— HTTP 长连接(也许叫 TCP 长连接更加合适),由此迎来了 HTTP/1.1 时代;
其实 HTTP/1.0 时就有了长连接,只是在 HTTP1.1 版本才默认启用。
起初,采用长连接仅仅是为了避免重复的 TCP 三次握手,但是后面发现一个问题—— HTTP 队头阻塞;
虽然这种模式的长连接减少了 TCP 重复的三次握手,但是存在一个比较严重的问题——HTTP 队头阻塞;
为了解决队头阻塞问题,HTTP/1.1 新增了 pipelining(管道化)特性,管道化允许客户端在已发送的请求收到服务端的响应之前发送下一个请求,即实现并发发送;
完美解决了吗?NO!
虽然在TCP通道中实现了多个 HTTP 请求并发,但是返回却仍然保持顺序,谁先达到谁先返回,并未根治队头阻塞的问题。此外还有一个限制,那就是只能是幂等请求才能应用 pipeline,基于这点考虑,大部分浏览器默认是关闭 pipeline 的,所以管道化的推行并没有取得太大的成功。
什么是幂等请求?
指不会对服务器资源产生影响的请求,比如 GET,HEAD 等。
具体解释:当连接意外中断时,客户端需要重新发送未收到响应的请求。如果这个请求只是从服务器获取数据,那并不会对资源造成任何影响,但是如果是一个提交信息的请求,如 POST 请求,那么可能会造成资源多次提交从而改变资源,这是不允许的。
管道化行不通,那么该如何优化长连接串行带来的效率低下问题呢?HTTP/1.1给出了另一个优化方案:允许建立多个 TCP 连接,比如谷歌浏览器最多允许 6 个 TCP 连接存在;
有没有办法绕开浏览器 6 个 TCP 的限制,建立更多的TCP连接?
因为TCP通道的数量是由域名决定的,所以可以尝试多给网站几个域名,这种方法叫域名切片,通过域名切片,请求时间可以进一步缩短。
但是这样做也会导致服务器压力增大,且 TCP 本身的一些不好的特性,如慢启动、TCP 链接之间竞争网速等。
除了上面提到的“长连接”与“管道机制”外,HTTP/1.1 还具有以下特性:
尽管在 HTTP/1.1 中浏览器可以同时发出六个不同的请求,但是 HTTP 仍然很慢,且并没有完全解决 HTTP 队头阻塞问题。
鉴于此,2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题,但是面临繁多的竞争者,即便强如谷歌也不能稳操胜券,但是此时谷歌祭出它的了大杀器进行“偷袭”——他们直接将 SPDY 用在 Chrome 浏览器上,自从协议在 Chrome 浏览器上证明可行之后,就被当做了 HTTP/2 标准的基础。
2014 年 12 月,互联网工程任务组(IETF)的 Hypertext Transfer Protocol Bis(httpbis)工作小组将 HTTP/2 标准提议递交讨论,并于 2015 年 2 月 17 日被批准。HTTP/2 标准于 2015 年 5 月在 RFC 7540 正式发表,替换 HTTP 1.1 成为 HTTP 的最新实现标准。
让我们先来了解一下这三剑客:
HTTP/2 之前默认使用 ASCII 编码传输,而 HTTP/2 改为二进制格式编码,因为二进制格式编码解析速度更快,可以用来提升传输效率。
那么在不改变 HTTP1.x 的语义、方法、状态码、URL 以及首部字段的情况下,HTTP/2 是怎样进行二进制分帧的呢?答案就是在应用层(HTTP)和传输层(TCP)之间增加一个二进制分帧层。
二进制分帧层会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码。其中,首部信息 header 被封装到 Headers 帧中,而 body 将被封装到 Data 帧中。
我们已经了解到二进制分帧,这和流有什么关系呢?
HTTP/2 给每个 HTTP 的 request 都分配唯一的 streamId,虽然每个 request 切割出来的不同 frame 位于多个不同 stream 中,但是它们都共用这个 streamId,这样的话每个 stream 可以不用保证顺序乱序发送,只需在到达目标端后基于这个 streamId 将切割的信息还原重组即可,还可以根据优先级来优先处理哪个 stream。
http2这种同时处理多个 request 的方式模拟实现了流的传输。
上图中每个大的蓝色方块代表一个 HTTP 的 request,每个 request 被切割为多个 frame,并且被编号,我们用黄红绿三种颜色分别代表三个 stream 流,不同的颜色代表不同的 streamid,HTTP/2 接收到数据会根据其 streamId 自动还原数据,这样就实现了在一个 TCP 连接通道中的流式传输,多个request 都会复用这个 TCP 通道,实现了高效的复用。
我们已经知道了基于二进制分帧,HTTP 消息会被分解为独立的帧,帧以带有同一个 streamId 的 stream 流的形式被交错发出去,在另一端根据流标识符将他们重新组装起来。
基于以上两点,HTTP/2 中另一个概念随之出现——多路复用:
但这也会造成了两个问题:
- 因为多路复用没有限制同时请求数,导致服务器压力上升。虽然请求的数量与不采用 HTTP/2 相同,但是会遇到请求短暂爆发的情况,导致瞬时 QPS 暴增;
- 当 TCP 连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了,这种情况就叫TCP 队头阻塞。但是对于 HTTP/1.1 来说,可以开启多个 TCP连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
虽然 HTTP/2 的“三剑客”灭掉了 HTTP 对头阻塞问题,但是还存在另外一个问题——TCP 队头阻塞,这源于 TCP 的特性。
什么是 TCP 队头阻塞?
如果 HTTP/2 连接双方的网络中有一个数据包丢失(丢包),整个 TCP 连接就会暂停,丢失的数据包需要被重新传输。而且因为 TCP 是一个按序传输的链条,因此如果其中一个点丢失了,链路上之后的内容就都需要等待。
这种单个数据包造成的阻塞,就是 TCP 队头阻塞(head of line blocking),可以说只要是在传输层采用 TCP 协议都会面临队头阻塞问题。
HTTP/1.1 首部带有大量信息,而且每次都要重复发送,HTTP/2 要求通讯双方各自缓存一份首部字段表(静态字典与动态字典),起到减少传输数据的作用。
其中静态字典的作用有两个:1)对于完全匹配的头部键值对,例如 :method: GET,可以直接使用一个字符表示; 2)对于头部名称可以匹配的键值对,例如 cookie: xxxxxxx,可以将名称使用一个字符表示。
自从 SPDY 演变成为 HTTP2 后,谷歌的 Geek 们认为它仍然不够快。
因此,他们开始讨论 QUIC 这个项目。
当前简单接触过 HTTP/3 之后,或许会被 QUIC HTTP/3 HTTP-over-QUIC HTTP/2-over-QUIC搞得晕头转向、云里雾里,它们都是些什么东西,有什么区别呢?
让我们先从 QUIC 开始:
QUIC 的全称 Quick UDP Internet Connections,即快速 UDP 互联网连接,谐音 quick,意思就是快。它是 Google 提出来的一个基于 UDP 的传输协议,而上文提到的 TCP 队头阻塞正是谷歌决定放弃 TCP,拥抱 UDP 的原因之一。
为了与 QUIC 兼容, HTTP/2 在几个关键领域也进行了调整,这就形成了 HTTP-over-QUIC;
2018 年,IETF 的 QUIC 工作组主席 Mark Nottingham 提议将 HTTP-over-QUIC 更名为 HTTP/3;
紧接着,一个疑问被反复提出:既然 HTTP-over-QUIC 是在 HTTP/2 的基础上做出的更改,为什么不是更名为 HTTP/2-over-QUIC 呢,一时间,仿佛 HTTP/2-over-QUIC 才是众望所归,然并卵。
最终官方还是正式将 HTTP-over-QUIC 称为 HTTP/3,所以 HTTP-over-QUIC 也就是后来的 HTTP/3;
虽然 HTTP/2 支持了多路复用,但是 TCP 协议终究是没有这个功能的。而 QUIC 原生就实现了这个功能—— Real Multiplexing,即真正的多路复用。
同 HTTP/2一样,同一条 QUIC 连接上可以创建多个 stream,来发送多个 HTTP 请求。
但是,QUIC是基于 UDP 的,一个连接上的多个stream之间没有依赖。比如 Stream1 丢了一个UDP包,虽然该包需要重传,但是不会影响后面跟着 Stream2 和 Stream3,所以并不存在 TCP 队头阻塞的问题。
曾经人们也尝试通过升级TCP的方式去解决问题,但是很快,大家就意识到这已经是一件不可能完成的任务了,因为存在“中间设备僵化”的问题,即因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。
QUIC 协议虽然是基于 UDP 来实现的,但是它将 TCP 的重要功能都进行了实现和优化,否则使用者是不会买账的。
在 TCP 中,TCP 为了保证数据包的可靠性,使用了SYN 序号+ ACK 确认号机制来实现,一旦带有 synchronize sequence number 的包发送到服务器,服务器都会在一定时间内进行响应,如果过了这段时间没有响应,客户端就会重传这个包(超时重传),直到服务器收到数据包并作出响应为止。
TCP 是如何判断它的重传超时时间呢?
TCP 一般采用的是自适应重传算法,这个超时时间会根据往返时间 RTT 动态调整的。
但是由于每次超时重发的 syn 都是相同的,因为无法区分到底是回来的 ACK 是响应的哪个 syn 请求,所以无法精确区请求响应的延时,导致这个 RTT 的结果计算的不太准确。
而 QUIC 实现可靠性的机制则是使用了 Packet Number,这个序列号可以认为是 synchronize sequence number 的替代者,这个序列号也是递增的。
与 syn 所不同的是,不管服务器有没有接收到数据包,这个 Packet Number 都会 + 1,而 syn 是只有服务器发送 ack 响应之后,才会 + 1。
上面提到了 syn 的机制会导致重传算法动态时间计算不准确,QUIC 是如何准确计算的呢?
举个例子,有一个 PN = 10 的数据包在发送的过程中由于某些原因迟迟没到服务器,那么客户端会重传一个 PN = 11 的数据包,经过一段时间后客户端收到 PN = 10 的响应后再回送响应报文,此时的 RTT 就是 PN = 10 这个数据包在网络中的生存时间,这样计算相对比较准确。
UDP 是不可靠传输协议,那 QUIC 是如何实现或优化可靠传输的呢?
可靠传输有 2 个重要特点:
上图中:
- 客户端:发送 3 个数据包给服务器(PN = 1, 2,3)
- 服务器:通过 SACK 告知客户端已经收到了 1 和 3,没有收到 2
- 客户端:重传第 2 个数据包(PN=4)
先说结论,HTTP/3 是可以进行 0RTT 或 1RTT 加密传输的;
0RTT 是指通信双方发起通信连接时,第一个数据包便可以携带有效的业务数据。
你也许还记得,在文章开头,我们提到 TLS 一般是 2RTT,但是 TLS1.3 可以做到 1RTT 甚至 0RTT,而 UDP 本身就是 0RTT 的,所以这使得 HTTP/3 能够做到 0RTT 或 1RTT 加密传输。
由上图可以看到,TCP + TLS 在首次建立连接时需要经过 3RTT,而 QUIC 只需要经历 1RTT;
因为 HTTP/3 采用了 QUIC,而 QUIC 内置了使用 DH 算法的 TLS1.3 协议(迪菲赫尔曼算法,是一种秘钥协商/交换算法),该版本协议允许客户端无需等待 TLS 握手完成就开始发送应用程序数据的操作,在首次连接的时候,即第 3 步时,就已经开始发送实际的业务数据了,而第 1 步和第 2 步正好一去一回花费了 1RTT 时间,所以,首次连接的成本是 1RTT。
DH 算法依赖离散对数这一数学困难问题,过程简单描述为,选取一个质数 p 和 g 的一个生成元 g ,通讯双方分别根据自己私钥、 p 、 g 计算出对应的公钥,并将公钥发送给对方,双方再根据对方的公钥和自己的私钥计算出通讯密钥
TCP 的连接基于 4 元组:源 IP、源端口、目的 IP、目的端口,只要其中 1 个发生变化,就需要重新建立连接。如图,如果是 TCP 连接,则客户端通过 4 元组与网络基站 1 绑定交互,网络基站 1 与 云端服务绑定交互,此时客户端移动,网络由网络基站 1 切换为网络基站 2,则需要连接重连。
简单一点的理解就是:在 4G 网络切换到 WIFI 时,需要重新建立连接,体验并不良好,尤其是在频繁切换网络的场景中。
而 QUIC 的连接标识是一个 64 位的连接 ID,用户在 Wi-Fi 和 4G 网络切换时,无论是 IP 或者端口发生变化,QUIC 连接中的连接 ID 保持不变,因此不需要重新创建连接。
这种用户无感知的网络切换特性,叫连接迁移(Connection Migration)。
在短视频爆发和全民主播时代,QUIC 的连接迁移支持在 Wi-Fi 和 Cellular(蜂窝网络)无缝切换,给用户带来更好的体验。
TCP是在系统层面实现的,这意味着难以保持统一,但是 QUIC 是在应用层实现的拥塞控制,可以实现动态插拔;
TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。 但是 QUIC 的 packet
可以说是武装到了牙齿,除了个别报文外,几乎所有报文头部都是经过认证的,报文 Body 都是经过加密的。 这样只要对 QUIC
报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
基于 QUIC 的 HTTP/3 完美了吗?显然不是的。
很多企业、运营商和组织对 53 端口(DNS)以外的 UDP 流量会进行拦截或者限流,因为这些流常被滥用于攻击,所以,基于 UDP 的 QUIC 协议的传输可能会受到屏蔽。
IETF 贡献者、HTTP/3 和 QUIC 工作组成员 Robin Marx 在接受采访时说:“我担心,很多人最终会对 HTTP/3 感到失望。 之所以担心,是因为 HTTP/2 也经历过同样的处境。当初 HTTP/2 被誉为一场惊人的性能革命,并拥有服务器推送(server push)、并行流(parallel stream)和优先级(prioritization)等激动人心的新功能。五年后,我们知道,服务器推送在实际中并未起作用,并行流和优先级也经常无法得到很好的实现”。
不过 QUIC 已经展现了其强大的生命力,“你只管努力去做,剩下的交给时间”,让我们拭目以待吧。
本文写作时参考了较多资料,因当时整理不当,导致很多优秀的引用文章找不到了,如果发现本文引用了您的文章,可与我联系,我将进行引用添加。