为什么 HTTP/3 基于UDP,可靠么?

15,885 阅读10分钟

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

对网络技术有一定研究的同学一定知道,HTTP/3竟然是基于UDP的。在很多同学的印象里,UDP属于一种非常低级的协议:传输不可靠,没有拥塞机制,把它用在准确可靠的万维网传输上,是一件不可想象的事情。

然而,真的是这样吗?我们先把吃惊的嘴合上,一起来看一下,为什么HTTP/3可以基于UDP,并且这还是一种非常聪明的选择。

要明白这个选择,我们首先就得消除一下对于UDP的误解。

1. UDP是最纯洁的传输层协议

实际上,UDP并不是像大家想象中的那样不可信,它只是因为简单,才让你有这样的认知。从另一个角度来说,其实是最纯洁的传输层协议。

那么UDP在网络传输中,到底处于一个什么位置呢?我们需要简单看一下典型的网络分层。

img

物理层和连接层

  • 物理层处于网络的最下层,它处理的消息,全部是1和0,这是硬件层面的东西,保证了我们的信息能够正常交流
  • 连接层对这些1和0进行了初步的整合,组成了固定长度的信息帧(frame),每个信息帧里面,都包含SRCDST(这里是mac地址),得以让我们的两台机器进行点对点通信

这时候网络数据包还不能逃离局域网,想要更大规模的传输,就需要网络层的帮助。

网络层

为了让这些地址有意义, 网络层加入了IP协议,通过IP地址进行目标机器的定位,最后经过路由器的转换,由连接层负责具体信息的传输。比如我在下面的这个wireshark的抓包。

img

  • EthernetII就是我们所说的连接层以太网,传输的信息是Frame
  • 下面的是IPv4协议,表明了是一个IP包。

如果你去看一下IP的报文,会发现它的格式是非常怪异的,中间节点在路由的时候,需要走先解包然后再封包的过程。这是由于IP协议是网络层协议,它的头信息,比如IP地址,其实是连接层协议的报文体(playload)。

到了IP协议,我们已经离UDP很近了。

传输层

IP协议只是解决了计算机到计算机之间的通信问题,但我们每台机器上还会有不同的进程,如何区分它们呢?这就是传输层协议要干的事。

UDP和TCP协议,通过加入了端口号,来识别某一个进程。IP地址加上端口号,就是我们写代码需要面对的socket。现在的大多数网络通信,包括HTTP/1,HTTP/2等,都是基于TCP。

从协议层面来看,UDP其实是最干净的协议,它完完全全的暴露了IP协议的所有内容。相比较IP协议,它仅仅多了一个端口号,所以UDP协议拥有IP协议的所有特点。比如无序性,不保证可靠性(Best Effort)。至于拥塞机制这种更高级的流量协商控制,UDP根本就不管这些。

UDP这种没有特性的特性,使得应用面比TCP窄的多。我们通常把**TCP/IP**协议作为一个整体去介绍,以至于忽略了最原始最纯洁的UDP协议。

img

我们来看一下一个典型的UDP协议。如上图,相对于IP协议,UDP协议仅仅多了两个端口,一个长度,一个checksum,确实是无与伦比的纯洁。

2. TCP作为基础协议太复杂

你可能会问,TCP和UDP都是传输层协议,那为什么HTTP/3不是基于TCP呢?那是因为TCP本身就已经非常复杂了,有太多历史遗留的包袱。

为了保证信息的可靠传输,顺序传输,同时兼顾吞吐量,TCP做了大量工作。相比较UDP,我们可以看一下TCP的协议都多了哪些内容。如图,是一个wireshark抓取的,典型的TCP协议包。

img

我们常说的三次握手(四次合并成三次),四次挥手,就是Flags和序列号逻辑组合的结合体。如果加入了TLS安全协议,这个握手的过程会更长。

连接建立后,由于采用了一问一答的ACK确认模式,TCP的效率其实是不怎么高的。它要传输很多无用信息,还得等待。

为了提高网络传输效率,TCP使用滑动窗口来解决批量发送,同时解决顺序性问题。为了解决网络拥塞,TCP使用慢启动、拥塞避免、快速重传、快速恢复等机制,使得网络吞吐量保持在一定的水平。

我们可以看一下wikipedia上TCP协议的一张细节图,在《TCP/IP详解 卷一:协议》中,对此进行了非常详细的介绍。可以看到细节问题还是非常多的。

img

那什么叫做协议?协议,就是规定了大家都遵守的标准,所有协议都是利益共同体共同商定的结果。比如我的分布式数据库使用了MySQL的接入层协议,那么就要遵循MySQL协议所制定的一系列标准,否则就跑不起来。

**协议越底层,对稳定性要求就越高。**TCP协议,目前已经被编码到了操作系统,不论是协议升级,还是BUG修复,都是伤筋动骨的。

3. 为什么UDP可行?

为了抛开历史的包袱,HTTP/3选择了UDP,主要是为了解决对头阻塞问题。它的底层协议,就是大名鼎鼎的QUIC,一个运行在传输层(也可以说是应用层)的协议。与TCP不同的是,QUIC代码并没有硬编码在操作系统内核中,而是完全运行在用户空间的,默认集成了TLS。

img

如上图所示,HTTP/3基于QUIC,而QUIC是完全基于UDP的。

但UDP不是号称无连接的么?它怎么去实现可靠性等一些额外的功能呢?

其实,连接这个词,是一个虚拟的概念,在网络中根本就没有连接这么一条线,连接只是为了方便你逻辑上的理解。考虑到你现在的客户端服务器是client,服务端是server。那么client和server的数据包交互,就可能会有下面这种行走路径。

img

在没有数据交互的时候,server和client就是单纯的两个点。即使你拔了网线重新插上(期间无交互),它们依然可以继续相互间发送数据。

UDP号称的无连接,其实和TCP的连接没什么两样。唯一的区别是,UDP把数据包发送之后,就什么也不管了,这些信息对端可能收到了,也可能没收到。而当每次都对发送的数据进行ACK确认,它就变成了TCP。

至于这部分确认代码,是放在传输层,还是放在应用层,这都关系不大;但代码是放在操作系统,还是可以独立升级的包中,那关系可就大了。

QUIC是可以独立于操作系统发行的,避免了操作系统缓慢的更新换代问题。QUIC的实现,依然要面对消息的可靠性、滑动窗口、拥塞控制等场景,你可以认为它就是一个TCP,但它与TCP有本质的区别。

这些区别,我们对比一下HTTP的各个版本,在数据传输方面的表现就知道了了。

  1. 在比较早的HTTP1.0实现中,如果需要从服务端获取大量资源,会开启N条TCP短链接,并行的获取信息。但由于TCP的三次握手和四次挥手机制,在连接数量增加的时候,整体的代价就变得比较大
  2. 在HTTP/1.1中,通过复用长连接,来改善这个情况,但问题是,由于TCP的消息确认机制和顺序机制以及流量控制策略的原因,资源获取必须要排队使用。一个请求,需要等待另外一个请求传输完毕,才能开始
  3. HTTP/2采用多路复用,多个资源可以共用一个连接。 但它解决的只是应用层的复用,在TCP的传输上依然是阻塞的,后面的资源需要等待前面的传输完毕才能继续。这就是队头阻塞现象(Head-of-line blocking)
  4. QUIC抽象出了一个stream(流)的概念,多个流,可以复用一条连接,那么滑动窗口这些概念就不用作用在连接上了,而是作用在stream上。由于UDP只管发送不管成功与否的特性,这些数据包的传输就能够并发执行。协议的server端,会解析并缓存这些数据包,进行组装和整理等。由于抽象出了stream的概念,就使得某个数据包传输失败,只会影响个stream的准确性,而不是整个连接的准确性

End

了解了以上内容,相信你一定能得出结论:HTTP/3基于UDP,是非常靠谱的。它不仅实现了可靠性传输,而且能够获得较大的性能提升。我们来总结一下QUIC的这些改进:

  1. QUIC能够实现TCP协议的所有功能性需求,并集成了TLS,功能上赶超了TCP
  2. 一条连接,多个stream并发传输,真正的多路复用,有效解决队头阻塞现象,性能上超越了TCP
  3. 减少握手次数,尤其是带TSL传输的握手次数,有更低的握手延迟
  4. 由于易于升级,为协议的更新和发展,提供了巨大的想象空间

QUIC产生的原因,主要是由于TCP的限制所引起的。连接是一种非常宝贵的资源,创建、销毁,以及其上的传输,都是非常耗时的。TCP的可靠性机制,在计算机网络发展的早期,确实是非常有效的,但随着硬件的升级,它的ACK传输模式,在效率上制约了更高性能的发展。由于TCP标准的概念深入人心,它的代码甚至直接存在于内核上,使得协议升级困难。

随着网络基础设施的提升,TCP的这种可靠传输模式,反而成了制约。如果我们的信息处理,能够全部在一条连接上完成,那就太好了。这样,在一些密集的资源传输时,比如批量小图片、视频点播、弱网传输时,就不会受到RTT的影响。另外我的数据缓存和拥塞控制等,也会更加灵活。举个极端的例子,我的内存足够大能把stream缓存起来,我甚至能够忍受某个1MB大小的文件,10秒钟后文件的第一个字节到达,而不是像TCP一样一直重传重传(因为它受限于TCP窗口)。

为什么能够这么做呢?还是得益于UDP纯洁的属性,它只是IP协议的一个编程接口,它真的是一张白纸,什么都没有。如果你愿意,你甚至可以在UDP的基础上,完全复刻TCP的所有功能,只要你能把server端和client端对应起来。这就是QUIC所做的。

⚠️本文为掘金社区首发签约文章,未获授权禁止转载