HTTP系列-HTTP/3篇
2023-01-18·6min
type
Post
summary
status
Published
category
tags
slug
date
Jan 18, 2023
password
icon
HTTP/2和TCP的缺陷
HTTP/2有着二进制报文、头部压缩、多路复用等特点,基本解决了HTTP/1.1中的队头堵塞问题,大幅提升了传输效率。
但它仍然存在以下问题:
- 建立连接时间长
- 弱网环境下,TCP层面的队头堵塞问题相较于HTTP/1.1更加严重。
HTTP/2建立连接时间?
要想计算HTTP/2建立连接的花费,先理解一个叫做RTT的术语。
RTT(Round-Trip Time)往返时间表示从发送端发送数据开始到接收到确认数据总共经历的时间,也就是通信一个来回的时间。
由于HTTP/2事实上都是加密的,也就是采用HTTPS的协议名,所以一个HTTP/2建立连接要经历以下过程:
- TCP三次握手:也就是两去,一回,花费1.5个RTT,由于第三次握手可以携带数据,所以节省 0.5 RTT,最后花费1RTT。
- TLS握手:TLS/1.2需要花费2RTT,TLS/1.3需要花费1RTT,恢复连接花费0RTT。具体查看HTTP系列-HTTPS篇。
总结来说一个HTTP/2首次建立连接最少要花费2个RTT,如果是TLS/1.2则要花费3个RTT。
如果服务器距离客户端很近,一个RTT时间 < 10ms,那么建立连接时间不会超过30ms,用户不会感知。但如果距离较远,相隔上万公里,一个RTT时间通常在200ms以上,那么建立连接就要花费600ms甚至1s以上,这就会影响到用户体验了。
TCP队头阻塞
因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1.1 了。
因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
QUIC协议为什么基于UDP?
不难发现以上两个问题都是由TCP协议导致的,那么就有人想去修改TCP协议了。但这几乎是不可能的了,因为它已经存在了太久,充斥在各种设备中,并且这个协议是由操作系统实现的,更改起来不太现实。
但基于UDP又会带来新的问题,那就是UDP本身并不可靠,不能直接用。
于是谷歌决定在UDP基础上改造一个具备TCP协议优点的新协议,这么做有以下几点好处:
- 基于TCP开发的设备和协议非常多,兼容困难
- TCP协议是Linux内部的重要部分,修改和升级成本很大
- UDP是面向无连接的协议,没有三次握手和四次挥手的成本
- UDP没有队头阻塞的问题
- UDP改造成本小
这个新协议就是QUIC协议(Quick UDP Internet Connection),并且使用在了HTTP/3上,所以HTTP/3之前叫做HTTP-over-QUIC。
QUIC新特性

QUIC协议是一个基于UDP,具有TCP优点的协议,它在UDP的基础上实现了很多新功能,这里我们主要了解几个主要的。
多路复用,解决队头阻塞
虽然HTTP/2支持多路复用,但TCP协议是没有这个功能的,QUIC原生实现了多路复用。
QUIC协议基于UDP协议,同一个QUIC连接上可以创建多个stream(数据流)来发送多个HTTP请求,并且,多个stream之间没有依赖,传输的单个stream可以保证有序交付且不影响到其他的stream。
所以,假如单个stream中的某个包丢了,不会影响到其他的stream被处理,也就解决了之前TCP存在的队头阻塞问题。
并且QUIC在移动端的表现也比TCP好,因为TCP是基于IP和端口来识别连接的,这种方式在多变的移动网络下很脆弱。QUIC则通过ID的方式去识别一个连接,不管网络环境如何变化,只要ID不变,就可以迅速重连上。
0RTT

通过这张图可以看到左边的HTTPS需要2-3个RTT才能开始传输数据,而右边的QUIC协议在第一个包时就能够携带有效的应用数据。
当然QUIC实现0RTT也是有条件的,准确来说,首次连接是1RTT,后续建连才是0RTT。
首次连接过程:
- 客户端对于首次连接的服务端先发送clent Hello请求。
- 服务端生成一个素数p和一个整数g,同时生成一个随机数K_s_pri为服务端私钥,然后通过这三个数计算出服务端公钥K_s_pub。随后将服务端公钥K_pub,p,g这三个数打包,称之为config,一起发送给客户端。
- 客户端生成一个随机数K_c_pri为客户端私钥,再从config中取出p和g,然后通过这三个数计算出客户端公钥K_c_pub。再利用config中的服务端公钥K_s_pub和自己的私钥K_c_pri生成后续加密用的密钥K。最后利用密钥K加密业务数据,连同公钥一起发送给服务端。
- 服务端根据自己的私钥K_s_pri和客户端公钥K_c_pub生成和客户端一样的密钥K,然后解密数据包。
- 为了保证安全,上述生成的密钥K只会使用1次。后续服务端会根据第1步的规则重新生成一套全新的服务端公钥和私钥,并用这组新的公私钥计算出密钥M。然后将新公钥和用新密钥M加密的数据一起发送给客户端。
- 客户端根据新的服务端公钥和自己原来的私钥计算出新密钥M,然后解密数据包。
- 后续的数据交互都是用密钥M来加密解密,密钥K只用一次。

可以看到,首次连接时,在第三步就开始发送实际的应用数据了,1-3步正好花费1RTT,所以QUIC首次连接的成本是1RTT。
后续连接时,会使用类似TCP快速打开的技术,实现0RTT。
向前纠错
QUIC有个独特的特性,称为向前纠错 (Forward Error Correction,FEC)。
每个数据包除了本身的内容外,还包括部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。
向前纠错极致牺牲了每个数据包可以发送数据的上限,但减少了因为丢包导致的数据重传的时间(包括确认数据包丢失、请求数据包重传、等待新数据包)。
假如说要发送三个包,那么协议会算出这三个包的异或值并单独发出一个校验包,也就是总共发出了四个包。
当出现其中的非校验包丢包的情况时,可以通过另外两个包和校验包计算出丢失的数据包的内容。
当然这种技术只能使用在丢失一个包的情况下,如果出现丢失多个包就不能使用纠错机制了,只能使用重传的方式了。
加密认证的报文
TCP头部没有经过任何加密和认证,所以在传输的过程中很容易被中间网络设备篡改,注入和窃听。比如修改包序列号、滑动窗口等。
QUIC的数据包可以说是武装到了牙齿。除了个别数据包外,所有数据包首部都是经过认证的,传输数据体都是经过加密的。
这样只要对QUIC数据包任何修改,接收端都能够及时发现,有效地降低了安全风险。
总结
QUIC 协议虽然是基于 UDP 来实现的,但它将 TCP 的重要功能都进行了实现和优化,同时在加密传输方向的尝试也推动了TLS1.3的发展。
只是现在 TCP 协议的势力过于强大,很多网络设备甚至对于UDP数据包做了很多不友好的策略,所以现在暂时还是 TCP 的天下。