动图图解 | UDP就一定比TCP快吗?
文章持续更新,可以微信搜一搜「小白 debug」第一时间阅读,回复【面试】获面试题集。本文已经收录在 GitHub https://github.com/xiaobaiTech/golangFamily , 有大厂面试完整考点和成长路线,欢迎 Star。
话说,UDP 比 TCP 快吗?
相信就算不是八股文老手,也会下意识的脱口而出:"是"。
这要追问为什么,估计大家也能说出个大概。

但这也让人好奇,用 UDP 就一定比用 TCP 快吗?什么情况下用 UDP 会比用 TCP 慢?
我们今天就来聊下这个话题。
使用 socket 进行数据传输
作为一个程序员,假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 socket 进行编程。
socket 就像是一个电话或者邮箱(邮政的信箱)。当你想要发送消息的时候,拨通电话或者将信息塞到邮箱里,socket 内核会自动完成将数据传给对方的这个过程。
基于 socket 我们可以选择使用 TCP 或 UDP 协议进行通信。
对于 TCP 这样的可靠性协议,每次消息发出后都能明确知道对方收没收到,就像打电话一样,只要"喂喂"两下就能知道对方有没有在听。
而 UDP 就像是给邮政的信箱寄信一样,你寄出去的信,根本就不知道对方有没有正常收到,丢了也是有可能的。
这让我想起了大概 17 年前,当时还没有现在这么发达的网购,想买一本《掌机迷》杂志,还得往信封里塞钱,然后一等就是一个月,好几次都怀疑信是不是丢了。我至今印象深刻,因为那是我和我哥攒了好久的钱。。。
回到 socket 编程的话题上。
创建 socket 的方式就像下面这样。
fd = socket(AF_INET, 具体协议,0);
注意上面的"具体协议",如果传入的是SOCK_STREAM
,是指使用字节流传输数据,说白了就是TCP 协议。

如果传入的是SOCK_DGRAM
,是指使用数据报传输数据,也就是UDP 协议。

返回的fd
是指 socket 句柄,可以理解为 socket 的身份证号。通过这个fd
你可以在内核中找到唯一的 socket 结构。
如果想要通过这个 socket 发消息,只需要操作这个 fd 就行了,比如执行 send(fd, msg, ...)
,内核就会通过这个 fd 句柄找到 socket 然后进行发数据的操作。
如果一切顺利,此时对方执行接收消息的操作,也就是 recv(fd, msg, ...)
,就能拿到你发的消息。

对于异常情况的处理
但如果不顺利呢?
比如消息发到一半,丢包了呢?
丢包的原因有很多,之前写过的《用了 TCP 协议,就一定不会丢包吗?》有详细聊到过,这里就不再展开。
那 UDP 和 TCP 的态度就不太一样了。
UDP 表示,"哦,是吗?然后呢?关我 x 事"
TCP 态度就截然相反了,"啊?那可不行,是不是我发太快了呢?是不是链路太堵被别人影响到了呢?不过你放心,我肯定给你补发"
TCP 老实人石锤了。我们来看下这个老实人在背后都默默做了哪些事情。
重传机制
对于 TCP,它会给发出的消息打上一个编号(sequence),接收方收到后回一个确认(ack)。发送方可以通过ack
的数值知道接收方收到了哪些sequence
的包。
如果长时间等不到对方的确认,TCP 就会重新发一次消息,这就是所谓的重传机制。

流量控制机制
但重传这件事本身对性能影响是比较严重的,所以是下下策。
于是 TCP 就需要思考有没有办法可以尽量避免重传。
因为数据发送方和接收方处理数据能力可能不同,因此如果可以根据双方的能力去调整发送的数据量就好了,于是就有了发送和接收窗口,基本上从名字就能看出它的作用,比如接收窗口的大小就是指,接收方当前能接收的数据量大小,发送窗口的大小就指发送方当前能发的数据量大小。TCP 根据窗口的大小去控制自己发送的数据量,这样就能大大减少丢包的概率。

滑动窗口机制
接收方的接收到数据之后,会不断处理,处理能力也不是一成不变的,有时候处理的快些,那就可以收多点数据,处理的慢点那就希望对方能少发点数据。毕竟发多了就有可能处理不过来导致丢包,丢包会导致重传,这可是下下策。因此我们需要动态的去调节这个接收窗口的大小,于是就有了滑动窗口机制。
看到这里大家可能就有点迷了,流量控制和滑动窗口机制貌似很像,它们之间是啥关系?我总结一下。其实现在 TCP 是通过滑动窗口机制来实现流量控制机制的。

拥塞控制机制
但这还不够,有时候发生丢包,并不是因为发送方和接收方的处理能力问题导致的。而是跟网络环境有关,大家可以将网络想象为一条公路。马路上可能堵满了别人家的车,只留下一辆车的空间。那就算你家有 5 辆车,目的地也正好有 5 个停车位,你也没办法同时全部一起上路。于是 TCP 希望能感知到外部的网络环境,根据网络环境及时调整自己的发包数量,比如马路只够两辆车跑,那我就只发两辆车。但外部环境这么复杂,TCP 是怎么感知到的呢?
TCP 会先慢慢试探的发数据,不断加码数据量,越发越多,先发一个,再发 2 个,4 个...。直到出现丢包,这样 TCP 就知道现在当前网络大概吃得消几个包了,这既是所谓的拥塞控制机制。
不少人会疑惑流量控制和拥塞控制的关系。我这里小小的总结下。流量控制针对的是单个连接数据处理能力的控制,拥塞控制针对的是整个网络环境数据处理能力的控制。

分段机制
但上面提到的都是怎么降低重传的概率,似乎重传这个事情就是无法避免的,那如果确实发生了,有没有办法降低它带来的影响呢?
有。当我们需要发送一个超大的数据包时,如果这个数据包丢了,那就得重传同样大的数据包。但如果我能将其分成一小段一小段,那就算真丢了,那我也就只需要重传那一小段就好了,大大减小了重传的压力,这就是 TCP 的分段机制。
而这个所谓的一小段的长度,在传输层叫MSS(Maximum Segment Size),数据包长度大于 MSS 则会分成 N 个小于等于 MSS 的包。

而在网络层,如果数据包还大于MTU(Maximum Transmit Unit),那还会继续分包。

一般情况下,MSS=MTU-40Byte
,所以TCP 分段后,到了 IP 层大概率就不会再分片了。

乱序重排机制
既然数据包会被分段,链路又这么复杂还会丢包,那数据包乱序也就显得不奇怪了。比如发数据包 1,2,3。1 号数据包走了其他网络路径,2 和 3 数据包先到,1 数据包后到,于是数据包顺序就成了 2,3,1。这一点 TCP 也考虑到了,依靠数据包的sequence
,接收方就能知道数据包的先后顺序。
后发的数据包先到是吧,那就先放到专门的乱序队列中,等数据都到齐后,重新整理好乱序队列的数据包顺序后再给到用户,这就是乱序重排机制。

连接机制
前面提到,UDP 是无连接的,而 TCP 是面向连接的。
这里提到的连接到底是啥?
TCP 通过上面提到的各种机制实现了数据的可靠性。这些机制背后是通过一个个数据结构来实现的逻辑。而为了实现这套逻辑,操作系统内核需要在两端代码里维护一套复杂的状态机(三次握手,四次挥手,RST,closing 等异常处理机制),这套状态机其实就是所谓的"连接"。这其实就是 TCP 的连接机制,而 UDP 用不上这套状态机,因此它是"无连接"的。
网络环境链路很长,还复杂,数据丢包是很常见的。
我们平常用 TCP 做各种数据传输,完全对这些事情无感知。
哪有什么岁月静好,是 TCP 替你负重前行。
这就是 TCP 三大特性"面向连接、可靠的、基于字节流"中"可靠"的含义。
不信你改用 UDP 试试,丢包那就是真丢了,丢到你怀疑人生。
用 UDP 就一定比用 TCP 快吗?
这时候 UDP 就不服了:"正因为没有这些复杂的 TCP 可靠性机制,所以我很快啊"
嗯,这也是大部分人认为 UDP 比 TCP 快的原因。
实际上大部分情况下也确实是这样的。这话没毛病。
那问题就来了。
有没有用了 UDP 但却比 TCP 慢的情况呢?
其实也有。
在回答这个问题前,我需要先说下UDP 的用途。
实际上,大部分人也不会尝试直接拿裸 udp放到生产环境中去做项目。
那 UDP 的价值在哪?
在我看来,UDP 的存在,本质是内核提供的一个最小网络传输功能。
很多时候,大家虽然号称自己用了 UDP,但实际上都很忌惮它的丢包问题,所以大部分情况下都会在 UDP 的基础上做各种不同程度的应用层可靠性保证。比如王者农药用的KCP
,以及最近很火的QUIC(HTTP3.0)
,其实都在 UDP 的基础上做了重传逻辑,实现了一套类似TCP 那样的可靠性机制。
教科书上最爱提 UDP 适合用于音视频传输,因为这些场景允许丢包。但其实也不是什么包都能丢的,比如重要的关键帧啥的,该重传还得重传。除此之外,还有一些乱序处理机制。举个例子吧。
打音视频电话的时候,你可能遇到过丢失中间某部分信息的情况,但应该从来没遇到过乱序的情况吧。
比如对方打网络电话给你,说了:"我好想给小白来个点赞在看!"
这时候网络信号不好,你可能会听到"我....点赞在看"。
但却从来没遇到过"在看小白好想赞"这样的乱序场景吧?
所以说,虽然选择了使用 UDP,但一般还是会在应用层上做一些重传机制的。
于是问题就来了,如果现在我需要传一个特别大的数据包。
在TCP
里,它内部会根据MSS
的大小分段,这时候进入到 IP 层之后,每个包大小都不会超过MTU
,因此 IP 层一般不会再进行分片。这时候发生丢包了,只需要重传每个 MSS 分段就够了。

但对于UDP
,其本身并不会分段,如果数据过大,到了 IP 层,就会进行分片。此时发生丢包的话,再次重传,就会重传整个大数据包。

对于上面这种情况,使用 UDP 就比 TCP 要慢。
当然,解决起来也不复杂。这里的关键点在于是否实现了数据分段机制,使用 UDP 的应用层如果也实现了分段机制的话,那就不会出现上述的问题了。
总结
- TCP 为了实现可靠性,引入了重传机制、流量控制、滑动窗口、拥塞控制、分段以及乱序重排机制。而 UDP 则没有实现,因此一般来说 TCP 比 UDP 慢。
- TCP 是面向连接的协议,而 UDP 是无连接的协议。这里的"连接"其实是,操作系统内核在两端代码里维护的一套复杂状态机。
- 大部分项目,会在基于 UDP 的基础上,模仿 TCP,实现不同程度的可靠性机制。比如王者农药用的 KCP 其实就在基于 UDP 在应用层里实现了一套重传机制。
- 对于 UDP+重传的场景,如果要传超大数据包,并且没有实现分段机制的话,那数据就会在 IP 层分片,一旦丢包,那就需要重传整个超大数据包。而 TCP 则不需要考虑这个,内部会自动分段,丢包重传分段就行了。这种场景下,其实 TCP 更快。
最后
最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。
我有个不成熟的请求。

离开广东好长时间了,好久没人叫我靓仔了。
大家可以在评论区里,叫我一靓仔吗?
我这么善良质朴的愿望,能被满足吗?
如果实在叫不出口的话,能帮我点下关注和右下角的点赞+收藏吗?
如果评论区没人叫我靓仔,文章也没人点赞,我感觉我下篇文章要开始收费了,价钱我都想好了,8 块 8,毕竟男人都拒绝不了这种价格以 8 结尾的项目。
你说是吧,易峰。
别说了,一起在知识的海洋里呛水吧
关注公众号:【小白 debug】
不满足于在留言区说骚话?
加我,我们建了个划水吹牛皮群,在群里,你可以跟你下次跳槽可能遇到的同事或面试官聊点有意思的话题。就超!开!心!

