队头阻塞问题

 


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的队头阻塞问题。

 

posted @   ˙鲨鱼辣椒ゝ  阅读(145)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示