队头阻塞问题
HTTP/1.1的队头阻塞
假设浏览器基于 HTTP/1.1 协议请求 script.js 文件与style.css文件,浏览器使用 Content-Length 头部来得知每个响应的结束位置和下一个响应的开始文件(在本例中,script.js是1000字节,style.css只有600字节,我们假设 .js 文件比 .css 文件大得多)。按照TCP协议的特性,TCP packet会按照顺序进行发送,那么正常情况下,在下载整个.js文件之前,.css文件都必须等待,尽管它要小得多。使用数字形象化即是:
1111111111111111111111122 (1表示JS文件,2表示CSS文件)
而我们想要的是,让小文件能够更早地解析或使用。这里就能看到一个队头阻塞问题:前面一个大的或慢的响应(如本例的.js文件)会延迟后面的其他响应(如本例的.css文件)。
解决方法
1.多路复用
将每个文件切分成更小的片(pieces)或块(chunks),在网络上混合或交错这些块进行发送(可以理解为打乱TCP packets的顺序),这样有可能使较小的.css文件更早地被下载完,如下图所示。
使用数字形象化即是:
1212111111111111111111111
但这种多路复用在HTTP/1.1中是无法实现的。因为HTTP/1.1是一个纯文本协议,它只在每个文件片段中附加头部来识别交付下来的资源的信息(如:Content-Length头部来标识资源的大小)。假如我们在HTTP/1.1的基础上使用多路复用,在本例中,浏览器分析TCP packet1中的header之后,便期望后面有1000个字节的数据,首先它接收了TCP packet1中剩余的450个JS字节,然后读取TCP packet2中的header以及css部分,将它们解释为JS的一部分,甚至读取到TCP packet3中js代码的一部分才停止(凑齐1000个字节)。此时他,浏览器看不到有效的新报头,必须删除TCP packet3的剩余部分。最后,浏览器传递错误的内容到JS解析器,造成失败。
HTTP/2的多路复用有效解决了这个问题。
2.打开多个并行连接
为HTTP/1.1上的每个页面加载打开多个并行连接(通常为6个),让多个请求分布在这些单独的连接上,这样也不会造成队头阻塞。但这种方法的限制很严重,因为每个页面加载超过六个资源很常见,并且打开多个TCP连接需要相当大的开销(特别是HTTPS连接)。
HTTP/2的队头阻塞
HTTP/2如何解决HTTP/1.1中的队头阻塞?
HTPP/2在每个块前面添加一个数据帧,这些数据帧中包含两个关键的元数据:
- stream id:标识这个块属于哪个资源;
- length:标识块的大小为多少;
协议中还有其他帧类型,比如下图的头部帧(HEADERS frame)。
这样,浏览器首先处理script.js的头部帧,然后处理第一个JS块的数据帧。从数据帧中包含的块长度来看(length:450),浏览器知道它只延伸到TCP packet1的末尾。然后处理TCP packet2,在这里它找到了style.css的头部帧与下一个数据帧,但该数据帧的stream id与前面TCP packet1中数据帧的不同,因此浏览器知道这属于不同的资源。处理到TCP packet3时,拿到第二个数据帧并且得知该数据帧属于stream2,长度为300,与前面接收到的css文件正好凑齐了header标识的长度(Content-Length:600)。此时浏览器知道该资源已经接收完,可以先解析。(我们假设后面还有TCP packet4,包含还未接收完的属于script.js的块)
HTTP/2的队头阻塞
HTTP/2只解决了“应用层”级别的队头阻塞。在网络的传输过程中,不可避免地会发生数据包丢失或延迟的情况,而TCP的可靠性——保证数据的传输顺序,正是造成“传输层”级别队头阻塞的原因。
假设上图中的TCP packet2在网络中丢失,而TCP packet1与TCP packet3已经到达。虽然我们知道TCP packet3中stream id为1的数据帧与TCP packet1中的数据帧已经完整接收,可以传递给浏览器进行解析。但TCP并不知道它正在承载HTTP/2,它并不关心它传输的是什么数据,它只知道它被赋予了一系列“字节流”,需要按顺序从一端传递到另一端。因此,TCP发现TCP packet1与TCP packet3之间存在间隙,需要将TCP packet3放到接收缓存区中,等待TCP packet2的重传副本(TCP packet1是可以直接传递给浏览器的)。最后,它才可以按照正确的顺序将这两个数据包传递给浏览器。
这里可以看到一个队头阻塞问题:丢失的TCP packet2阻塞了TCP packet3!
HTTP/3
很容易想到,我们只要让传输层知道每个块中的数据帧属于哪一个流,便可以解决HTTP/2中的队头阻塞问题。因此,由于很难改变TCP本身具有的流意识,HTTP/3以QUIC的形式实现了一个全新的传输层协议。它受HTTP/2帧方式的启发,添加了流帧(stream frame),将原来在HTTP/2数据帧中的stream id下移到传输层的QUIC流帧中。
在本例中,当QUIC packet2丢失时,对于QUIC packet3,QUIC 可以比TCP更聪明。它首先查看 stream id为1的流帧的字节范围,发现这个流帧与前面QUIC packet1中的流帧没有字节间隙(该帧的字节起始点450紧跟QUIC packet1中的流帧的结束点449),因此可以将它们提供给浏览器进行处理。而对于QUIC packet3中stream id为2的流帧,由于还没有接收到字节0~299,该流帧将保存在缓存区直到QUIC packet2的重传副本到达。
因此,QUIC确实有效地解决了TCP的队头阻塞问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?