HTTP3。0彻底放弃TCP,TCP到底做错了什么?
从HTTP1。1到HTTP2,HTTP协议一直都是使用TCP作为传输协议。
然而,就在最新的HTTP3,HTTP就直接把TCP抛弃了,向孤立无援的UDP伸出了援手,基于UDP协议的基础上,在应用层实现了一个可靠的传输协议QUIC。
很多同学可能就好奇了,HTTP都用TCP都用了几十年了,而且TCP已经是那么完善的可靠传输协议了,又有超时重传、按序接收、流量控制、拥塞控制这些特性,怎么突然就把TCP抛弃了?到底是TCP哪里做的不够好?是不是鸡蛋里挑骨头了?
所以,今天就跟大家聊聊,TCP那些不够好的原因。TCP存在队头阻塞问题
TCP队头阻塞的问题要从两个角度看,一个是发送窗口的队头阻塞,另外一个是接收窗口的队头阻塞。
1、发送窗口的队头阻塞。
TCP发送出去的数据,都是需要按序确认的,只有在数据都被按顺序确认完后,发送窗口才会往前滑动。举个例子,比如下图的发送方把发送窗口内的数据全部都发出去了,可用窗口的大小就为0了,表明可用窗口耗尽,在没收到ACK确认之前是无法继续发送数据了。
接着,当发送方收到对第3236字节的ACK确认应答后,则滑动窗口往右边移动5个字节,因为有5个字节的数据被应答确认,接下来第5256字节又变成了可用窗口,那么后续也就可以发送5256这5个字节的数据了。
但是如果某个数据报文丢失或者其对应的ACK报文在网络中丢失,会导致发送方无法移动发送窗口,这时就无法再发送新的数据,只能超时重传这个数据报文,直到收到这个重传报文的ACK,发送窗口才会移动,继续后面的发送行为。
举个例子,比如下图,客户端是发送方,服务器是接收方。
客户端发送了第59字节的数据,但是第5字节的ACK确认报文在网络中丢失了,那么即使客户端收到第69字节的ACK确认报文,发送窗口也不会往前移动。
此时的第5字节相当于队头,因为没有收到队头的ACK确认报文,导致发送窗口无法往前移动,此时发送方就无法继续发送后面的数据,相当于按下了发送行为的暂停键,这就是发送窗口的队头阻塞问题。
2、接收窗口的队头阻塞。
接收方收到的数据范围必须在接收窗口范围内,如果收到超过接收窗口范围的数据,就会丢弃该数据,比如下图接收窗口的范围是3251字节,如果收到第52字节以上数据都会被丢弃。
接收窗口什么时候才能滑动?当接收窗口收到有序数据时,接收窗口才能往前滑动,然后那些已经接收并且被确认的有序数据就可以被应用层读取。
但是,当接收窗口收到的数据不是有序的,比如收到第3340字节的数据,由于第32字节数据没有收到,接收窗口无法向前滑动,那么即使先收到第3340字节的数据,这些数据也无法被应用层读取的。只有当发送方重传了第32字节数据并且被接收方收到后,接收窗口才会往前滑动,然后应用层才能从内核读取第3240字节的数据。
好了,至此发送窗口和接收窗口的队头阻塞问题都说完了,这两个问题的原因都是因为TCP必须按序处理数据,也就是TCP层为了保证数据的有序性,只有在处理完有序的数据后,滑动窗口才能往前滑动,否则就停留。停留发送窗口会使得发送方无法继续发送数据。停留接收窗口会使得应用层无法读取新的数据。
其实也不能怪TCP协议,它本来设计目的就是为了保证数据的有序性。HTTP2的队头阻塞
HTTP2通过抽象出Stream的概念,实现了HTTP并发传输,一个Stream就代表HTTP1。1里的请求和响应。
在HTTP2连接上,不同Stream的帧是可以乱序发送的(因此可以并发不同的Stream),因为每个帧的头部会携带StreamID信息,所以接收端可以通过StreamID有序组装成HTTP消息,而同一Stream内部的帧必须是严格有序的。
但是HTTP2多个Stream请求都是在一条TCP连接上传输,这意味着多个Stream共用同一个TCP滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的HTTP请求,这属于TCP层队头阻塞。
没有队头阻塞的QUIC
QUIC也借鉴HTTP2里的Stream的概念,在一条QUIC连接上可以并发发送多个HTTP请求(Stream)。
但是QUIC给每一个Stream都分配了一个独立的滑动窗口,这样使得一个连接上的多个Stream之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
假如Stream2丢了一个UDP包,也只会影响Stream2的处理,不会影响其他Stream,与HTTP2不同,HTTP2只要某个流中的数据包丢失了,其他流也会因此受影响。
TCP建立连接的延迟
对于HTTP1和HTTP2协议,TCP和TLS是分层的,分别属于内核实现的传输层、openssl库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先TCP握手(1RTT),再TLS握手(2RTT),所以需要3RTT的延迟才能传输数据,就算Session会话服用,也需要至少2个RTT,这在一定程序上增加了数据传输的延迟。
TCP三次握手和TLS握手延迟,如图:
HTTP3在传输数据前虽然需要QUIC协议握手,这个握手过程只需要1RTT,握手的目的是为确认双方的连接ID,连接迁移就是基于连接ID实现的。
但是HTTP3的QUIC协议并不是与TLS分层,因为QUIC也是应用层实现的协议,所以可以将QUIC和TLS协议握手的过程合并在一起,QUIC内部包含了TLS,它在自己的帧会携带TLS里的记录,再加上QUIC使用的是TLS1。3,因此仅需1个RTT就可以同时完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和QUIC握手信息(连接信息TLS信息)一起发送,达到0RTT的效果。
如下图右边部分,HTTP3当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到0RTT(下图的右下角):
升级TCP的工作很困难
TCP协议是诞生在1973年,至今TCP协议依然还在实现更多的新特性。
但是TCP协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级TCP协议,那么只能升级内核。
而升级内核这个工作是很麻烦的事情,麻烦的事情不是说升级内核这个操作很麻烦,而是由于内核升级涉及到底层软件和运行库的更新,我们的服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级也比较保守和缓慢。
很多TCP协议的新特性,都是需要客户端和服务端同时支持才能生效的,比如TCPFastOpen这个特性,虽然在2013年就被提出了,但是Windows很多系统版本依然不支持它,这是因为PC端的系统升级滞后很严重,WindowsXp现在还有大量用户在使用,尽管它已经存在快20年。
所以,即使TCP有比较好的特性更新,也很难快速推广,用户往往要几年或者十年才能体验到。
相反,QUIC是处于应用层的,所以如果升级QUIC协议的话,其实就是像升级软件一样轻松。而且,QUIC可以针对不同的应用设置不同的拥塞控制算法,这样灵活性就很高了,这是TCP做不到的,因为TCP更改拥塞控制算法是对系统中所有应用都生效,无法根据不同应用设定不同的拥塞控制策略。网络迁移需要重新建立TCP连接
基于TCP传输协议的HTTP协议,由于是通过四元组(源IP、源端口、目的IP、目的端口)确定一条TCP连接。
那么当移动设备的网络从4G切换到WIFI时,意味着IP地址变化了,那么就必须要断开连接,然后重新建立TCP连接。
而建立连接的过程包含TCP三次握手和TLS四次握手的时延,以及TCP慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
QUIC协议没有用四元组的方式来绑定连接,而是通过连接ID来标记通信的两个端点,客户端和服务器可以各自选择一组ID来标记自己,因此即使移动设备的网络变化后,导致IP地址变化了,只要仍保有上下文信息(比如连接ID、TLS密钥等),就可以无缝地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
总结
HTTP3抛弃TCP后,基于UDP实现的可靠传输QUIC协议,带来这四点好处:降低连接耗时:在客户端有缓存的情况下实现0RTT建立连接更灵活的拥塞控制:在用户态可以为每个请求配置不同的拥塞控制策略无队头阻塞的多路复用:每个请求流独立拥有滑动窗口,互不影响连接迁移:网络切换不会中断数据传输
不过,HTTP3也面临了一些挑战,QUIC基于UDP协议在用户空间实现的可靠传输协议,如果一些网络设备无法识别出QUIC协议,那么在这些网络设备的眼里它就是一个UDP协议。
而几乎所有的电信运营商都会歧视UDP数据包,原因也很容易理解,毕竟历史上几次臭名昭著的DDoS攻击都是基于UDP的。国内某城宽带在某些区域更是直接禁止了非53端口的UDP数据包,而其他运营商即使没有封禁UDP,也是对UDP进行严格限流的。
自2013年QUIC被正式公开以来,到2023年已经发展了差不多10年,目前网上已经有了不少热门开源的项目,除去带头大哥Google在完成了对自身搜索引擎的支持,还同时拉上了Gmail、YouTube等站点。但对于国内的绝大部分站点来说,大部分还是HTTP2协议,HTTP3之路,似乎还停留在东土大唐。