目录
- UDP报文格式
- udp通信
- udp通信的特征
- udp的发送
- udp 发送缓冲区
- udp 发送方丢包
- udp的接收
- udp接收缓冲区
- udp接收端丢包
- udp包在ip层的分片
- udp 丢包问题
- 提升udp的可靠性
- UDP冗余传输
- udp真的比tcp高效吗?
- 影响UDP高效因素
- udp使用场景
- 通信实时性和持续性
- 多点通信
- UDP使用原则
- 参考
UDP报文格式
源端口: 主机的应用程序使用的端口号
目的端口:目的主机的应用程序使用的端口号
长度:是指UDP头部和UDP数据的字节长度。因为UDP头 部长度为8字节,所以该字段的最小值为8
校验和:检测UDP数据报在传输中是否有错,有错则丢弃。
- udp 校验和
在计算校验和的时候,需要在UDP数据报之前增加12字节(sip/dip/ip proto/ip len)的伪首部,伪首部并不是UDP真正的首部。只是在计算校验和,临时添加在UDP数据报的前面,得到一个临时的UDP数据报。校验和就是按照这个临时的UDP数据报计算的。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验。
udp通信
UDP协议的程序设计框架如图所示,客户端和服务器之间的差别在于服务器必须使用bind()函数来绑定侦听的本地UDP端口,而客户端则可以不进行绑定,直接发送到服务器地址的某个端口地址。
UDP协议服务器与客户端之间的交互,与TCP协议的交互相比较,缺少了二者之间的连接。这是由于UDP协议的特点决定的,因为UDP协议不需要流量控制、不保证数据的可靠性收发,所以不需要服务器和客户端之间建立连接的过程。
因此,与TCP程序设计相比较,UDP缺少了connect()、listen()及accept()函数,这是用于UDP协议无连接的特性,不用维护TCP的连接、断开等状态。
udp通信的特征
- UDP的传输方式:一发一收/面向报文
在阻塞模式下,UDP的通信是以数据包作为界限的,即使server端的缓冲区再大也要按照client发包的次数来多次接收数据包,server只能一次一次的接收,client发送多少次,server就需接收多少次,即客户端分几次发送过来,服务端就必须按几次接收。
Q:client发送两次UDP数据,第一次 500字节,第二次300字节,server端阻塞模式下接包,第一次recvfrom( 1000 ),收到是 1000,还是500,还是300,还是800?
A: 由于UDP通信的有界性,接收到只能是500或300,又由于UDP的无序性和非可靠性,接收到可能是300,也可能是500,也可能一直阻塞在recvfrom调用上,直到超时返回(也就是什么也收不到)。
Q:在假定数据包是不丢失并且是按照发送顺序按序到达的情况下,server端阻塞模式下接包,先后三次调用:recvfrom( 200),recvfrom( 1000),recvfrom( 1000),接收情况如何呢?
A: 由于UDP通信的有界性,第一次recvfrom( 200)将接收第一个500字节的数据包,但是因为用户空间buf只有200字节,于是只会返回前面200字节,剩下300字节将丢弃。第二次recvfrom( 1000)将返回300字节,第三次recvfrom( 1000)将会阻塞。
- UDP数据包的无序性和非可靠性
client依次发送1、2、3三个UDP数据包,server端先后调用3次接收函数,可能会依次收到3、2、1次序的数据包,收包可能是1、2、3的任意排列组合,也可能丢失一个或多个数据包。
在本例的发送过程中,由于没有设置本地的IP地址和本地端口,而这些参数是网络协议栈发送数据时的必需条件,所以在UDP层网络协议栈会选择合适的端口。发送的网络数据经过IP层的时候,客户端会选出合适的本地IP地址进行填充,并且将客户端的目的IP地址填充到IP报文中。发送的数据到达数据链路层的时候,会根据硬件的情况进行发送。
细看两个系统调用的参数便知道,sendto比send的参数多2个,这就意味着每次系统调用都要多拷贝一些数据到内核空间。同时,参数到内核空间后,内核还需要初始化一些临时的数据结构来存储这些参数值(主要是对端Endpoint_S的地址信息),在数据包发出去后,内核还需要在合适的时候释放这些临时的数据结构。进行UDP通信的时候,如果首先调用connect绑定对端Endpoint_S的后,那么就可以直接调用send来给对端Endpoint_S发送UDP数据包了。
udp 发送缓冲区
任何UDP套接字都有发送缓冲区大小,但实际上不存在UDP发送缓冲区,其大小仅仅是可写到套接字的UDP数据报的大小上限。
如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。
- UDP不可靠
应用进程的数据在沿协议栈向下传递时,通常被复制到某种格式的一个内核缓冲区中,然而当该数据被发送之后,这个副本就被数据链路层丢弃了。
既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区。
UDP面向报文
面向报文的传输方式决定了UDP的数据发送方式是一份一份的,也就是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。UDP报文大小
UDP报文大小的影响因素:
[1] UDP协议本身,UDP协议中有16位的UDP报文长度,那么UDP报文长度不能超过2^16=65536.
[2] 以太网(Ethernet)数据帧的长度,数据链路层的MTU(最大传输单元)。
[3] socket的UDP发送缓存区大小
所以,udp的 socket 调用 send或sendto 可以发送的最大字节数为:min(2^16, /proc/sys/net/core/wmem_default);
- UDP数据包理想长度
理论上UDP报文最大长度是2^16字节(wmem_default足够大的情况下),实际上发送这么大的数据包效果最好吗?
UDP是不可靠的传输协议,为了减少UDP包丢失的风险,我们最好能控制UDP包在下层协议(IP 层)的传输过程中不要被切割。
MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64字节,最大不能超过1518字节,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。
由于以太网EthernetII最大的数据帧是1518字节,除去以太网帧的帧头(DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500字节这个值我们就把它称之为MTU。
在下层数据链路层最大传输单元是1500字节的情况下,要想IP层不分包,那么UDP数据包的最大大小应该是1500字节 – IP头(20字节) – UDP头(8字节) = 1472字节。
Q: 当我们发送的UDP数据大于1472的时候会怎样呢?
这也就是说IP数据报大于1500字节,大于MTU,这个时候发送方IP层就需要分片(fragmentation)。把数据报分成若干片,使每一片都小于MTU,而接收方IP层则需要进行数据报的重组。这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方无法重组数据报,将导致丢弃整个UDP数据报。
Internet编程时,建议将UDP数据控制在548字节以下。
原因如下:
- Internet上的路由器可能会将MTU设为不同的值。如果我们假定MTU为1500来发送数据,而途经的某个网络的MTU值小于1500字节,那么就可能会分片。
- ipv4协议规定ip层的最小重组缓冲区大小为576!所以,建议udp包不要超过这个大小。
因此,在进行Internet的UDP编程时,最好将UDP的数据长度控件在548字节(576-8-20)以内。
udp 发送方丢包
发送的包太大
发送的包比64K大会导致UDP协议sendto返回错误。可以考虑增大/proc/sys/net/core/wmem_max的值。
发送的包比MTU大,则需要在ip层进行分片。UDP包在接收端接收分片可能会丢包,可查看接收端的网卡统计。可考虑在应用层把数据切分到MTU以下再发送。发包速度太快
发包速度太快的话,可能有两个问题:
1.接收端来不及接收导致接收端丢包。
2.发送端网卡处理不过来。
这个时候sendto没有返回错误,但是用netstat -su/grep “^Udp:” /proc/net/snmp | column -t 查看会发现SndbufErrors不断上升,有可能是网卡的输出队列太小导致。可以考虑使用ifconfig命令把txqueuelen设置大一些。
udp接收缓冲区
udp接收端丢包
接收缓冲区小于发送客户端的包的大小
可以考虑增大接收缓冲区。接收客户端recvfrom速度太慢,导致接收缓冲区满丢弃数据
可以考虑将接收操作和业务处理操作分离到不同的线程来处理。
Q:如果MTU是1500,Client发送一个8000字节大小的UDP包,那么Server端阻塞模式下接包,在不丢包的情况下,recvfrom(9000)是收到1500,还是8000?如果某个IP分片丢失了,recvfrom(9000),又返回什么呢?
udp 丢包问题根据UDP通信的有界性,在buf足够大的情况下,接收到的一定是一个完整的数据包,UDP数据在下层的分片和组片问题由IP层来处理,提交到UDP传输层一定是一个完整的UDP包,那么recvfrom(9000)将返回8000。
如果某个IP分片丢失,udp里有个CRC检验,如果包不完整就会丢弃,也不会通知是否接收成功,所以UDP是不可靠的传输协议,那么recvfrom(9000)将阻塞。
在不考虑UDP下层IP层的分片丢失,CRC检验包不完整的情况下,造成UDP丢包的因素有哪些呢?
- UDP socket接收缓冲区满造成的UDP丢包
通过 cat /proc/sys/net/core/rmem_default 和cat /proc/sys/net/core/rmem_max可以查看socket缓冲区的缺省值和最大值。如果socket缓冲区满了,应用程序没来得及处理在缓冲区中的UDP包,那么后续来的UDP包会被内核丢弃,造成丢包。
临时解决:
在socket缓冲区满造成丢包的情况下,可以通过增大缓冲区的方法来缓解UDP丢包问题。但是,如果服务已经过载了,简单的增大缓冲区并不能解决问题,反而会造成滚雪球效应,造成请求全部超时,服务不可用
- UDP socket发送缓冲区过小造成的UDP丢包
如果Client发送的UDP报文很大,而socket缓冲区过小无法容下该UDP报文,那么该报文就会丢失。
- ARP缓存过期缓存数据包超限导致UDP丢包
ARP 的缓存时间约10分钟,APR 缓存列表没有对方的 MAC 地址或缓存过期的时候,会发送 ARP 请求获取 MAC 地址,在没有获取到 MAC 地址之前,用户发送出去的 UDP 数据包会被内核缓存到 arp_queue 这个队列中,默认最多缓存3个包,多余的 UDP 包会被丢弃。被丢弃的 UDP 包可以从 /proc/net/stat/arp_cache 的最后一列的 unresolved_discards 看到。
临时解决:
提升udp的可靠性当然我们可以通过 echo 30 > /proc/sys/net/ipv4/neigh/eth1/unres_qlen 来增大可以缓存的 UDP 包。
注:UDP 的丢包信息可以从 cat /proc/net/udp 的最后一列drops中得到,而倒数第四列 inode 是丢失 UDP 数据包的 socket 的全局唯一的虚拟i节点号,可以通过这个 inode 号结合 lsof ( lsof -P -n | grep 25445445)来查到具体的进程。
在外网通信链路不稳定的情况下,有什么办法可以降低UDP的丢包率呢?
UDP冗余传输
一个简单的办法来采用冗余传输的方式。如下图,一般采用较多的是延时双发,双发指的是将原本单发的前后连续的两个包合并成一个大包发送,这样发送的数据量是原来的两倍。这种方式提高丢包率的原理比较简单,例如本例的冗余发包方式,在偶数包全丢的情况下,依然能够还原出完整的数据,也就是在这种情况下,50%的丢包率,依然能够达到100%的数据接收。
相信很多同学都认为UDP无连接,无需重传和处理确认,UDP比较高效。然而UDP在大多情况下并不一定比TCP高效,TCP发展至今天,为了适应各种复杂的网络环境,其算法已经非常丰富,协议本身经过了很多优化,如果能够合理配置TCP的各种参数选项,那么在多数的网络环境下TCP是要比UDP更高效的。
影响UDP高效因素
- 无法智能利用空闲带宽导致资源利用率低
一个简单的事实是UDP并不会受到MTU的影响,MTU只会影响下层的IP分片,对此UDP一无所知。在极端情况下,UDP每次都是发小包,包是MTU的几百分之一,这样就造成UDP包的有效数据占比较小(UDP头的封装成本);或者,UDP每次都是发巨大的UDP包,包大小MTU的几百倍,这样会造成下层IP层的大量分片,大量分片的情况下,其中某个分片丢失了,就会导致整个UDP包的无效。
由于网络情况是动态变化的,UDP无法根据变化进行调整,发包过大或过小,从而导致带宽利用率低下,有效吞吐量较低。
而TCP有一套智能算法,当发现数据必须积攒的时候,就说明此时不积攒也不行,TCP的复杂算法会在延迟和吞吐量之间达到一个很好的平衡。
无拥塞控制机制无法动态调整发包
由于UDP没有确认机制,没有流量控制和拥塞控制,这样在网络出现拥塞或通信两端处理能力不匹配的时候,UDP并不会进行调整发送速率,从而导致大量丢包。在丢包的时候,不合理的简单重传策略会导致重传风暴,进一步加剧网络的拥塞,从而导致丢包率雪上加霜。更加严重的是,UDP的无秩序性和自私性,一个疯狂的UDP程序可能会导致这个网络的拥塞,挤压其他程序的流量带宽,导致所有业务质量都下降。改进UDP的成本较高
针对UDP的一些缺点,在用户态做些调整改进,添加上简单的重传和动态发包大小优化。然而,这样的改进并不简单的,UDP编程可是比TCP要难不少的,考虑到改造成本,为什么不直接用TCP呢?当然可以拿开源的一些实现来抄一下(例如:libjingle),或者拥抱一下Google的QUIC协议,然而,这些都需要不少成本的。
上面说了这么多,难道真的不该用UDP了吗?其实也不是的,在某些场景下,我们还是必须UDP才行的。
udp使用场景通信实时性和持续性
在分组交换通信当中,需要做到带宽和时延的平衡。协议栈的成本主要表现在以下两方面:
- 缓存带来的时间复杂度(缓存为了一次尽可能发的多)
- 封装带来的空间复杂度(有效载荷需要封装在头中)
以上两者是对立影响的。
如果想减少封装消耗,那么就必须缓存用户数据到一定量在一次性封装发送出去,这样每个协议包的有效载荷将达到最大化,这无疑是节省了带宽空间,带宽利用率较高,但是延时增大了。
如果想降低延时,那么就需要将用户数据立马封装发出去,这样显然会造成消耗更多的协议头等消耗,浪费带宽空间。
因此,我们进行协议选择的时候,需要重点考虑一下空间复杂度和时间复杂度间的平衡。通信的持续性对两者的影响比较大,根据通信的持续性有两种通信类型:
- [1] 短连接通信
- [2] 长连接通信。
对于短连接通信,一方面如果业务只需要发一两个包并且对丢包有一定的容忍度,同时业务自己有简单的轮询或重复机制,那么采用UDP会较为好些。在这样的场景下,如果用TCP,仅仅握手就需要3个包,这样显然有点不划算,一个典型的例子是DNS查询。
另一方面,如果业务实时性要求非常高,并且不能忍受重传,那么首先就是UDP了或者只能用UDP了,例如NTP 协议,重传NTP消息纯属添乱(为什么呢?重传一个过期的时间包过来,还不如发一个新的UDP包同步新的时间过来)。如果NTP协议采用TCP,撇开握手消耗较多数据包交互的问题,由于TCP受Nagel算法等影响,用户数据会在一定情况下会被内核缓存延后发送出去,这样时间同步就会出现比较大的偏差,协议将不可用。
多点通信
对于一些多点通信的场景,如果采用有连接的TCP,那么就需要和多个通信节点建立其双向连接,然后有时在NAT环境下,两个通信节点建立其直接的TCP连接不是一个容易的事情,在涉及NAT穿越的时候,UDP协议的无连接性使得穿透成功率更高。
一个典型的场景是多人实时音视频通信,这种场景下实时性要求比较高,可以容忍一定的丢包率。比如:对于音频,对端连续发送p1、p2、p3三个包,另一端收到了p1和p3,在没收到p2的保持p1的最后一个音(也是为什么有时候网络丢包就会听到嗞嗞嗞嗞嗞嗞…或者卟卟卟卟卟卟卟卟…重音的原因),等到到p3就接着播p3了,不需要也不能补帧,一补就越来越大的延时。对于这样的场景就比较合适用UDP了,如果采用TCP,那么在出现丢包的时候,就可能会出现比较大的延时。
UDP使用原则
通常情况下,UDP的使用范围是较小的,在以下的场景下,使用UDP才是明智的。
[1] 实时性要求很高,并且几乎不能容忍重传;
例子:NTP协议,实时音视频通信,多人动作类游戏中人物动作、位置。[2] TCP实在不方便实现多点传输的情况;
[3] 需要进行NAT穿越;
[4] 对网络状态很熟悉,确保udp网络中没有氓流行为,疯狂抢带宽;
[5] 熟悉UDP编程。