真正“搞”懂HTTP协议07之队头阻塞真的很烦人

  这一篇文章,我们核心要聊的事情就是HTTP的对头阻塞问题,因为HTTP的核心改进其实就是在解决HTTP的队头阻塞。所以,我们会讲的理论多一些,而实践其实很少,要学习的头字段也只有一个,我会在最开始就讲完这个头字段,然后我们安心的去学习接下来的理论知识,嗯……这些理论知识很重要。

  那我们就先来看看我们本篇要学的这个唯一的头字段是什么吧。

一、Connection头字段及其示例

  其实在聊这个字段之前,我们要先学一些前置知识才好,但是我想先讲这个字段,后面再带着疑问去学理论,嗯……就这样。

  Connection字段其实很好理解,就是用来开启长链接的,长链接的意思就是会重复利用TCP开启的通道,不会在请求一次后关闭。长链接可以这样开启Connection: keep-alive。这个东东在HTTP/1.1中是默认开启的,也就是说你啥也不干它就开启,当然,如果你想关闭的话可以这样Connection: close

  不仅仅如此,客户端和服务器都可以通过Keep-alive: timeout=value来限定长链接的超时时间,但是服务器和客户端往往都不一定遵守,约束力并不是很强。大家了解下就好了,另外,一些代理服务器比如Nginx,也会针对该字段有一些特殊的策略,比如该通道多长时间没有发送数据就关闭,比如该通道发送了多少次数据后就关闭等等。

  那么下面我们就来看看具体的例子,来实践一下。客户端和服务器的代码都很简单,基本的代码在上一篇文章中都接触过,我就不再复制代码了,可以在这里看。我们直接看下请求的结果。

  关于Connection的有三个字段,其中Proxy-Connection从词意上来讲就是指代理的通道连接方式。然后你看,Connection是Keep-alive,Keep-Alive字段的超时时间设置为4,也就是默认设置了四秒没有在该通道上传输数据就关闭TCP通道。那怎么验证呢?我们还得用下Wireshark。先请求下数据,然后看看四秒后会不会有TCP的四次挥手断开连接。

   我们可以清楚的看到三次握手后(也就是96、97、98三个id的tcp)的HTTP请求,过了四秒,就四次挥手(就是344、345、346、347四次)断开连接了,大家有兴趣自行尝试体验一下。

  例子就这么简单~我们接下来要学习理论知识了,这些理论知识很重要,这个例子就当个开胃小菜吧。

二、长短连接说短长

  我在上面的例子中说到,HTTP/1.1会默认开启长链接,那为什么要开启长链接?什么是长链接?那既然有长链接是不是还有短连接呢?嗯……你听我慢慢说。

一)短连接是什么? 

   我们知道,HTTP/0.9和HTTP/1.0都是十分简单的协议,它的底层是基于TCP的,在每次请求发送前都需要通过三次握手来和服务器建立连接,响应结束后会通过四次挥手断开连接。这就是短连接,在早期的时候也会被称为是无连接的。而这种操作是十分浪费资源的,效率就很低下。

  为什么早期的HTTP会是这个样子呢?因为在当时,大家知道大多数的网站都是静态页面,一个页面上能放几个gif图那都是很酷炫的事情了。所以,在这样的场景下,没有那么多的请求需要,这样设计似乎也无可厚非,但是随着互联网页面的极速发展,一个页面可能有几个甚至几十个请求,几百个外部资源文件,每次都开启、关闭、开启、关闭,浪费的资源可不是一点半点。

  所以,我们就需要对短连接进行改进,于是长连接出现了。

二)时代的宠儿:长连接

  因为短连接实在是无法适应时代的需要,太浪费了,所以为了解决短连接带来问题,在HTTP/1.1中就增加了持久链接的方法,它的特点就是可以在一个TCP连接上传输多个HTTP请求,只要浏览器或者服务器没有明确断开,那么就会一直保持连接状态。虽然这样做并没有改善TCP的连接效率,但是由于开启和断开的次数少了,把整个开启和断开的时间平均到了多次请求中,每个请求和应答的无效时间就少了很多,从而增加了整体传输的效率。

  在目前的浏览器中,同一个域名可以开启六个TCP连接,不过这里稍微要注意的是,其实HTTP规范约定的TCP连接数量是2个,但是各大浏览器厂商觉得肯定不够用,所以在实现层面上来说,每个域名可以开启6个TCP连接,HTTP规范在新的版本RFC7230中也就顺水推舟,约定可以是6到8个连接。

  那如果六个TCP连接还是不够用呢?嗯……我们可以多开几个域名,比如a.zaking.com,b.zaking.com,c.zaking.com,每一个域名都指向同一台服务器,说白了就是用数量来解决质量的方式,而这种解决思路也有个高大上的名词,叫做“域名分片”。

三、初识队头阻塞

  队头阻塞是本篇的重点,也是一件比较有趣的事情,有趣在哪里呢?因为它解决不了。我们下面就来看看什么是队头阻塞。

  因为HTTP是基于“请求—应答”模型的,在这个模型的基础上,HTTP规定报文必须是一发一收的,这就形成了一个先进先出的串行队列,如果你不知道什么是队列的话,请看这里。既然是队列,就存在一个这样的问题,队列里的请求没有优先级,谁先进来谁就先出去。但是假如某一个排在前面的请求卡住了,没有返回,那后面的所有队列中的请求都要等着那个卡住的请求结束,结果就是我分担了本来不应该由我来承担的时间损耗。

  那要怎么解决这个问题呢?诶?你不是说这个问题是解决不了的么?嗯……从规范上,从设计上来说确实无法解决,既然是队列就必然是这样的,但是上有政策下有对策,我大不了多开几个域名呗,多开几个队列,让它堵的可能性小点,你是不是想到了啥?嗯,就是我们上面说到的“域名分片”技术。

  其实很好理解,就好像我们在一个汽车在单车道上跑,堵车的可能性就很大,堵车了我也没办法,只能等前面解决了继续走,但是假设我是6车道,18车道,是不是就能在一定程度上解决这个问题了(其实用车道比喻HTTP的队头阻塞并不是十分恰当,比喻TCP的队头阻塞更恰当一点,但是这样好理解)。

  当然,你并没有从根本解决队头阻塞。只是使了点小手段罢了。

  我在demo代码里写了点小例子,大家可以点击试试。坦白说我并不知道底层的实现是什么,但是大概能猜到原因。

 

  当你发送很多无响应的HTTP请求后,等一会,再点有响应的HTTP请求,你会发现卡死了,我猜就是因为那些无响应的HTTP请求占用全部六个TCP连接。当然,你也可以通过Wireshark来验证这一点。不多说啦~

  完了嘛?还没……

  在HTTP/1.1中,也曾试图通过“管线化” 的技术来解决队头阻塞的问题,管线化就是指将多个HTTP请求整批发送给服务器的技术,虽然可以整批发送,但是服务器还是要按照队列的顺序返回结果,得~~~白玩了。所以最后这玩意没啥用,大家了解下就行了

四、多路复用的HTTP/2

   我们回顾一下上面的三个部分,发现HTTP/1.1为了优化做了哪些努力,一个是长连接,一个是每个域名可以同时维护6个TCP长连接,一个是就是域名分片技术。一共三种,但是这些手段都没有从根本上解决队头阻塞的问题,HTTP数据报文在传输的某一条连接上堵塞了,还是要等待,没办法。

  虽然使用这些手段一定程度上缓解了HTTP/1.0和HTTP/0.9所带来的问题,但是其实问题还是不少的,性能还可以进一步的压缩。

  其中关于TCP的问题有慢启动问题,以及带宽竞争问题。在TCP进入到传输数据的状态时,会处于一种递增的状态,就像开车一样,缓慢的从0加速到多少时速,这样做是为了减少网络拥塞,但是有些数据本身就很小,等着你慢慢启动就很浪费时间。

  而带宽竞争,则是指当带宽充足的时候,每条连接都会缓慢的增加发送速度,而一旦带宽不足时,这些连接传输数据的速度则会减慢,这样就回带来一个问题,就是优先级的问题,重要资源随着普通资源一起减慢了,真的是很苦恼。

  这两个问题是TCP引起的,HTTP想改变也改变不了,只能接受,所以HTTP/3的时候干脆不用TCP了。

  但是,队头阻塞的问题,则是HTTP可以进一步优化和解决的,想办法在一定程度上规避TCP的这两个问题,什么意思呢?

  HTTP/2的思路是一个域名只采用一个TCP连接,这样就能尽可能的减少TCP的慢启动和带宽竞争问题,就一个TCP连接,你也不用竞争了,就一个TCP连接,你就算启动的很慢,平分到每一个连接好像也还可以。你看,好像所有的解决思路都类似,解决不了就平分。

  基于这样的思路,HTTP/2针对队头阻塞的问题提出了多路复用的解决方案。什么意思呢,HTTP/2实现了资源的并行请求,也就是你在任何时候都可以发送请求,不用管前一个请求是否堵塞,服务器会在处理好数据后就返回给你。

  那,核心的问题来了,多路复用是如何实现的呢?

多路复用的实现

  HTTP/2在HTTP和TCP的中间又加了一层,也就是二进制分帧层:

 

   就像上图这样,其实二进制分帧层属于应用层,这个二进制分帧层做了什么呢?就是把发送的HTTP数据包拆成一个一个带有id的帧,服务器收到这些帧后,会把有同一个id的帧合并成一条完整的信息,那么同样的,服务器发送给客户端的数据也要这样经过二进制分帧层的分帧处理,浏览器会根据对应的id发送给请求的数据源头。

  HTTP/2就是通过这样的形式,引入了多路复用的机制,来解决队头阻塞的问题。

  看起来似乎很美好了是吧,HTTP/2可以说是HTTP目前为止最完美的解决方案了。但是故事并没有就此停止。

五、TCP也有队头阻塞

   虽然,HTTP/2解决了HTTP的队头阻塞,但是TCP也有队头阻塞,虽然你把HTTP数据包拆分成了一个又一个的帧,但是你还是传输在一条通道上,一旦某一个数据帧丢失了,那TCP就得等丢失的数据包重新传过来才行,卧槽,问题又回到了原点。那咋整?我们改一改TCP协议?

  抱歉,你改不了,主要的原因在于僵化,一个是中间设备的僵化,一个是操作系统的僵化。

  中间设备其实就是指数据在互联网中传输的过程中,所遇到的各种设备,比如路由器,网关,代理服务器,服务器等等等等,很多很多,这些东西比较硬性,一旦安装软件后很少升级,所以你改了客户端的TCP,这一连串的设备,甚至说全球的设备都要改,你想想,是不是很夸张。

  而操作系统僵化,则是因为TCP的核心实现是由操作系统底层来处理的,所以你看,要改TCP就要改操作系统,想想就头大。

  所以,由于僵化的原因,TCP改不了。那咋整?嗯……那就不用他了呗,我们用UDP好了。

  HTTP/3选择用UDP作为传输协议,并且在UDP和HTTP/3中又加了QUIC层。QUIC层则针对UDP区别于TCP的一些特性进行了处理,从而让UDP的传输像TCP一样完整和安全,并且像HTTP/2那样采用多路复用机制,来解决TCP的队头阻塞。

  关于QUIC或者HTTP/3的更多内容,会在后面HTTP/3的部分详细讲解,本篇就不再过多的阐述了。

  嗯……本篇结束了~

六、总结

  这篇文章并不长,理论知识稍微多一点,而其中最核心的点就是队头阻塞和多路复用,大家一定要着重学习。那么在本篇的最后,留给大家两个小问题。

  1. 长连接和短连接是啥?长连接出现的原因是什么?解决了什么问题呢?
  2. 关于HTTP的队头阻塞,你都有哪些了解?HTTP解决了队头阻塞的问题么?如果解决了,又是如何解决的?如果没解决,为什么没解决呢?
  3. HTTP的队头阻塞和TCP的对头阻塞有什么区别?都是怎么解决的?
posted @ 2023-01-10 21:53  Zaking  阅读(1112)  评论(2编辑  收藏  举报