可靠的UDP

本文深入探讨了UDP为何不可靠,并介绍了如何通过添加重传定时器、RTT计算、拥塞控制等机制实现可靠的UDP通信。详细阐述了UDP中bind和connect的作用,以及在FPS游戏等实时性应用场景中,为何需要UDP而非TCP。同时,讨论了如何通过自定义协议头、拥塞控制算法(如BBR)来优化传输效率,并概述了连接的建立与断开过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

UDP为什么是不可靠的
UDP只有一个socket接收缓冲区,没有socket发送缓冲区,即只要有数据就发,不管对方是否可以正确接收。而对方的socket接收缓冲区满了之后,新来的数据报无法进入到socket接收缓冲区,此数据报就会被丢弃,因此UDP不能保证数据能够到达目的地,此外UDP也没有流量控制和重传机制,故UDP的数据传输是不可靠的

bind和connect对于UDP的作用是什么

和TCP建立连接时采用三次握手不同,UDP中调用connect只是把对端的IP和端口号记录下来,并且UDP可多次调用connect来指定一个新的IP和端口号,或者断开旧的IP和端口号。和普通的UDP相比,调用connect的UDP会提升效率,并且在高并发服务中会增加系统的稳定性

当UDP的发送端调用bind函数时,就会将这个套接字指定一个端口,若不调用bind函数,系统内核会随机分配一个端口给该套接字。当手动绑定时,能够避免内核来执行这一操作,从而在一定程度上提高性能

既然有了TCP,为什么还需要UDP可靠呢
TCP是强制的可靠性传输,其在IP协议的基础上,发送端对所有的数据进行定时重传,接收端对所有的数据进行排序,以此来实现发送端是什么样子,接收端就能接收什么样子的数据。但是现实中有一些场景,我们并不需要如此固执的可靠性

对于FPS游戏这种时效性要求非常高的游戏中,玩家最关心的是自己的射击结果和角色的存活与否,当有一个手雷扔过来时,对于被炸死的玩家而言,这颗手雷是必须要接收到的消息,而其他没有被炸到的玩家,仅需要看到手雷的爆炸动画或者声音,其更关心的是自己现在的子弹有没有打到敌人,我们不希望因为受累的消息重传排队,而把射击玩家的结果确认消息延后了,这对于TCP而言就力所不及了

如何实现
首先,为了保证可靠性,我们需要在发送数据的时候添加重传定时器,来保证丢失的数据会被重传。重传定时器可以定时回调发送重传的数据,也支持将接收到的ACK数据从定时器中取出

现在有了重传定时器,那每次发送数据的时候,应该给定时器设置多长的时间呢?最简单的可以设定一个固定的重传时间,最合理的应该针对每条传输链路的不同设置每个连接的合理时间–rto,为了找到rto时间,我们需要获取到每个数据包发送确认时间,即rtt时间,即数据从发送到接收到ACK确认之间的时间间隔。我们参考TCP的实现策略,可以给每个消息记录一个发送时间,当接收到ACK确认时,将此事的时间减去记录的发送时间就获取到了rtt时间,但这样有一个问题,当发生数据重传时接收到ACK,无法判断这个ACK是对初次发送数据的确认还是对重传数据的确认,此时只能将发生重传的数据测量得到的rtt时间丢弃。所以又有第二种rtt计算策略,我们可以将发送时间记录在数据头发送出去,接收端在发送确认ACK时,将这个时间戳抄下来顺着ACK返回,这样发送端接收到ACK确认时,就能准确地知道要确认数据的发送时间,由此来计算rtt时间,有了rtt时间,我们可以按照TCP标准方法来计算rto时间

当接受到ACK确认时,我们需要将确认的数据从定时器中移除。

为了提高网络链路利用率,接收端不能每次接收到数据时都立即发送ACK确认,为什么呢?传输的数据量越小,控制头占比越高,而且网络中到处都是只携带一个ACK的包在飞,会造成路由器排队。这里可以接着参考TCP的实现策略。一种是延时ACK,即接收端接收到消息时定制一个pending time,当超时时将这段时间内所有要发送的ACK组合在一起发送,还有一种是捎带ACK,即pending time未到,但恰好也有数据要发送给对端,那么就将ACK捎带在这个数据包中一起发送出去。由于接收端ACK发送都不是瞬时的,所以在上文说到的RTT计算时也需要考虑引起的计算误差。

有同学要问了,你这整半天UDP可靠,还不是TCP都一样的策略?那接下来就说点和TCP不一样的东西了。

我们之前都只说了一个数据包的发送接收策略,当大量数据到来时如何发送呢?不可能一下子将所有数据都发送出去。所以我们需要一个发送窗体来控制发送数据的个数,当允许发送时就拿出下一个数据包发送,这发生在接收到新的ACK确认或者发送窗体大小调整时。这里和TCP的实现不同,TCP将所有的数据平铺在一个buffer里,然后通过移动滑动窗体来控制发送数据流动。我在这里没有用到滑动窗体,而是将所有的数据包都放到一个权限队列中,按照发送两个高一级权限数据包一个低一级数据包的规则来调整发送顺序,发送窗体中只有inflight的数据包,当可以发送下一个数据包时,再从权限队列中获取。发送窗体负责对发送后的数据缓存,确认,权限队列负责给发送的数据按优先级排序。

对接收端而言,也需要一个接收队列对接收到的数据包进行整理,这里我们可以根据需求的不同实现多种排队策略。如果是想得到TCP的效果,数据即有序,又可靠,那我们需要给所有到达的数据包发送ACK确认且排队,只有前一个数据包排好队,无乱序时,才能将数据反回给上层;如果只实现可靠性,不需要有序,那可以接收到一个数据包时,直接反回给上层,但是要发送ACK确认。如果只需要有序性,不需要可靠性,那可以记录目前收到最大的数据包序号,比这个序号大的数据包返回给上层,比这个序号小的直接丢弃,也不需要发送ACK,因为发送端也不会重传数据。以上就是三种不同的可靠性传输。

前面我们说了很多,数据包即要携带时间戳,又要携带确认ACK,我们需要给上层发下来的数据添加一个自己的协议头以使双端来识别必要的消息,这里我们可以通过不同的控制标识组合来实现一个变长的协议头,有效利用数据包的传输数据量。
在这里插入图片描述
其中Flag是必须的,占用4字节的长度,通过位标识后面每个块是否携带,这可以通过手动二进制序列来实现。

现在对于一个传输连接而言,我们有了重传机制,确认机制,协议头封装,传输接收控制,但是网络是公共交通,我们如何遵守交通规则,不引起网络阻塞的同时有效利用网络带宽呢?这就要引入拥塞控制算法,我在这里目前使用的是BBR,还没有实现TCP的CUBIC。那为什么使用BBR算法呢?这可就是小孩儿没娘,说来话长了。简单来讲,常规的CUBIC算法通过检测丢包来判断网络拥塞,然后通过控制发送窗体的大小来控制传输在网络中的数据量。大家想一下网络中的传输情况:

网络中的数据包极少,此时由于应用发送的数据量小,链路中的路由器数据队列为空,每个数据都能快速发送,并且得到确认。
应用层发送数据量增大,链路中的数据开始增多,路由器的数据包队列已经开始有排队的情况出现,但是还没有发生丢包,这对发送端来看,表现为RTT时间变长。
发送的数据量继续增大,链路中路由器队列已经出现排队排满,这时开始有丢包的情况出现。
CUBIC算法在第3阶段检测到丢包,开始减小发送窗体的大小,以收缩网络中传输的数据量,消化路由器排队队列,但是这时已经晚了!在没有发生丢包之前,链路中已经被数据包压的苦不堪言,发送数据的RTT时间已经非常慢。什么时候是链路利用率最高的时候呢?即在2阶段路由器即将有排队情况出现的时候,这时RTT时间最小,但是链路上已经有足够的数据在飞。CUBIC算法还有一个问题是其控制的输出变量只有发送窗体的大小,当发送窗体增大,应用层有数据到来时,会一股脑的将可发送的数据量全部发送出去。现实生活中的十字路口,大家都会遵守交通规则,通过红绿灯控制来保证每个路口都定时的可以让一些车辆通过,然而到了网络世界里一切都变的蛮不讲理,在此路口车很多的情况下,所有车都一个接一个的驶出,而不管交叉路口还有没有别的车等待,这是不对的。所以BBR算法不仅需要控制发送窗体的大小来控制发送的数据量,还通过RTT时间和传输的数据量来计算一个数据的发送速度,通过控制数据流发送的时间间隔,来实现按一定速率发送数据。具体BBR算法是如何实现的,那就是另外一篇长篇大论了,本文不再细说。

到目前为止,关于可靠性和传输效率的机制我们基本已经介绍完成,接下来说下传输的建立和连接。传输建立时并没有参考TCP的三次握手,依照UDP的简单粗暴,发送端只管发送数据,接收端能收到算建立了连接,没有接收到则发送端超时。因为我们的协议实现在应用层,没有进程启动的时候也无法发送RST给对端。连接断开时基本参考了TCP的四次挥手实现,继续保留了TIME_WAIT状态来保证网络中上一个连接的数据包不会发送到现任连接上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值