UDP socket编程的知识点总结
- Udp是无连接的、不可靠的传输协议,面向消息的数据传输协议
- 与TCP相比,有 两个致命的缺点,一是数据包容易丢失,二是数据包无序
- 建立在udp上层的协议,需要自己定义流控制、超时、重传等。
- 进行udp传输有两个问题,一个是数据包大小,一个是发数据时间间隔,这两个因素是发送端影响udp丢包率的主要因素,而丢包率直接影响了传输效率
-
UDP没有真正的发送缓冲区,因为它是不可靠无连接的,不需要保存应用进程的数据备份,应用进程的数据沿着协议栈往下传递,拷贝到内核缓冲区,在数据链路层把数据发送出去后便丢弃该拷贝
-
UDP没有流量控制,当发送端发送速率过快,很容易淹没接收端,导致接收端丢弃一些数据包
-
与TCP基于字节流的传输方式不同,UDP套接字的缓冲区是以一个个报文为单位进行排队的,调用一次recvfrom表示提取一个报文
UDP长度
TCP-IP详解卷一第11章的udp数据包的包头可以看出,udp的最大包长度是2^16-1的个字节,由于udp包头占8个字节,而在ip层进行封装后的ip包头占去20字节,所以udp数据包的最大理论长度是2^16-1-8-20=65507字节。 但TCP/IP通常被认为是一个四层协议系统,包括链路层、网络层、传输层、应用层。UDP属于传输层,在传输过程中,udp包的整体是作为下层协议的数据字段进行传输的,它的长度大小还受到下层ip层和数据链路层协议的制约。
MTU
以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的。这个1500字节被称为链路层的MTU(Maximum Transmission Unit,最大传输单元)。因特网协议允许IP分片,这样就可以将数据包分成足够小的片段以通过那些最大传输单元小于该数据包原始大小的链路。分片过程发生在网络层,它使用的是将分组发送到链路上的网络接口的最大传输单元的值。这个最大传输单元的值就是MTU,它是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位),通常与通信接口有关(网络接口卡、串口等)。在因特网协议中,一条因特网传输路径的“路径最大传输单元”被定义为从源地址到目的地址所经过“路径”上的所有IP跳的最大传输单元的最小值。从下图可知,本地回环lo的MTU不受限制。
如上所述,由于网络接口卡的制约,MTU的长度被限制在1500字节,这个长度指的是链路层的数据区。对于大于这个数值的分组可能被分片,否则无法发送,而分组交换的网络是不可靠的,存在着丢包。IP 协议的发送方不做重传。接收方只有在收到全部的分片后才能 reassemble(重组)并送至上层协议处理代码,否则在应用程序看来这些分组已经被丢弃。假定同一时刻网络丢包的概率是均等的,那么较大的IP datagram(数据包)必然有更大的概率被丢弃,因为只要丢失了一个fragment,就导致整个IP datagram接收不到。不超过MTU的分组是不存在分片问题的。
MTU的值并不包括链路层的首部和尾部的18个字节。所以,这个1500字节就是网络层IP数据报的长度限制。因为IP数据报的首部为20字节,所以IP数据报的数据区长度最大为1480字节。而这个1480字节就是用来放TCP传来的TCP报文段或UDP传来的UDP数据报的。又因为UDP数据报的首部8字节,所以UDP数据报的数据区最大长度为1472字节。这个1472字节就是我们可以使用的字节数。
当我们发送的UDP数据大于1472,这也就是说IP数据报大于1500字节,大于MTU。这个时候发送方IP层就需要分片(fragmentation)。把数据报分成若干片,使每一片都小于MTU。而接收方IP层则需要进行数据报的重组。而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报。将导致丢弃整个UDP数据报。因此,在普通的局域网环境下,将UDP的数据控制在1472字节以下为好。进行Internet编程时则不同,因为Internet上的路由器可能会将MTU设为不同的值。如果我们假定MTU为1500来发送数据的,而途经的某个网络的MTU值小于1500字节,那么系统将会使用一系列的机制来调整MTU值,使数据报能够顺利到达目的地。鉴于Internet上的标准MTU值为576字节,所以在进行Internet的UDP编程时,最好将UDP的数据长度控件在548字节(576-8-20)以内。
以太网帧长(不包括以太网头部和尾部) = IP数据报长度限制 = MTU = 1500 Byte
IP报头长度 = 20 Byte
UDP报头长度 = 8 Byte
Internet标注MTU=576 Byte
UDP丢包
Linux 系统接收网络报文的过程:
- 首先网络报文通过物理网线发送到网卡
- 网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与
- 内核从 ring buffer 中读取报文进行处理,执行 IP 和 TCP/UDP 层的逻辑,最后把报文放到应用程序的 socket buffer 中
- 应用程序从 socket buffer 中读取报文进行处理
其中,发送数据流程和接收类似,方向相反。发送流程报文丢失的概率比接收小,当应用程序发送的报文速率大于内核和网卡处理速率时才会发生。在接收 UDP 报文的过程中,上述任何一个过程都可能会主动或者被动地把报文丢弃,因此丢包可能发生在网卡和驱动,也可能发生在系统和应用。
- 网卡或驱动丢包、linux系统丢包、防火墙导致丢
- udp报文错误:在网卡包后接收到数据,linux内核的tcp/ip协议栈在udp数据包处理过程中发生丢包的可能原因是:udp数据包格式错误或校验和检查失败;应用程序来不及处理udp数据包。对于前者,可以通过netstat -su 进行udp丢包检测,如果packet receive errors不断增加,说明系统在接收数据时发生了丢包:
- 系统负载过高丢包:系统 CPU、memory、IO 负载过高都有可导致网络丢包。如果CPU负载过高,系统没有时间进行报文的 checksum 计算、复制内存等操作,从而导致网卡或者 socket buffer 处丢包;memory 负载过高,会应用程序处理过慢,无法及时处理报文;IO 负载过高,CPU 都用来响应 IO wait,没有时间处理缓存中的 UDP 报文。linux 系统本身就是相互关联的系统,任何一个组件出现问题都有可能影响到其他组件的正常运行。对于系统负载过高,要么是应用程序有问题,要么是系统不足。对于前者需要及时发现,debug 和修复;对于后者,也要及时发现并扩容。
- 应用丢包(udp buffer size不足):当网络协议栈发送数据时,其会将数据放置到socket buffer中,并通过调用驱动功能抽象层的发送函数将缓冲区中的数据发送给网卡设备,网卡设备接收到数据后就将其置于发送缓冲区中等待数据的发送。接收数据时,linux 内核把网卡接收到的报文放到 socket buffer 中,应用程序从 socket buffer 中不断地读取报文。所以socket buffe size 大小以及应用程序读取报文的速度都会影响应用层的丢包率。修改内核的socket缓冲区大小,某些情况下可以提高网络的传输性能。
Linux修改Socket缓存
// 套接字接收缓冲区大小的缺省值, 可参考的优化值:1746400/3492800/6985600
/proc/sys/net/core/rmem_default
// 套接字接收缓冲区大小的最大值
/proc/sys/net/core/rmem_max
// 套接字发送缓冲区大小的缺省值
/proc/sys/net/core/wmem_default
// 套接字发送缓冲区大小的最大值
/proc/sys/net/core/wmem_max
- /proc是一个很特殊的文件系统,其并非真实存在于物理磁盘,而是当前系统运行状态的一个映射,存在于RAM中。
- 这里的linux系统缺省值和最大值都是208KB
- 这里的rmem_max和wmem_max是可由程序设置的缓冲区最大值,实际的最大值是它的2倍
- 系统重启后,设置回归默认值
如果我们不通过setsockopt套接字选项来修改socket buffer 长度,就会使用 rmem_default 和 wmem_default缺省值来作为默认的接收和发送的 socket buffer 长度。如果修改socket option,由rmem_max 和 wmem_max 来限定上限。修改socket缓存的大小的方法如下:
通过socket设置修改
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET, SO_RCVBUF, (const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=3*1024*1024;//设置为3M
setsockopt(s,SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBuf,sizeof(int));
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd: 标识一个套接字的描述字
level: 选项定义的层次:支持SOL_SOCKET, IPPROTO_TCP, IPPROTO_IP,和IPPROTO_IPV6
optname:需设置得选项 SO_RCVBUF(接收缓冲区),SO_SNDBUF(发送缓冲区)
optval:指针,指向存放选项待设置的新值的缓冲区
optlen:optval的大小
或者通过配置sysctl.conf修改
vi /etc/sysctl.conf // 在最后一行添加
net.core.rmem_max=1048576 // 1024*1024 = 1MB
/sbin/sysctl -p // 保存退出,输入-p生效
sysctl -a | grep rmem_max //查看设置是否生效
socket buffer设置成多大合适?
从原理上看,网络延时不应该影响其带宽利用率,但大延时网络上的带宽利用率低,是因为延时变大后,发送方发的数据不能及时到达接收方。导致发送缓存满之后,不能再持续发送数据。而接收方则受到接收方剩余缓存大小的影响,接收缓存小的话,则会产生大量丢包。所以,这里的调优思路应该是,发送方调大udp_wmem,接收方调大udp_rmem。如果服务器的性能压力不大,对处理时延也没有很严格的要求,设置为1M左右即可。如果服务器的性能压力较大,或者对处理时延有很严格的要求,则必须谨慎设置rmem_default 和rmem_max,如果设得过小,会导致丢包,如果设得过大,会出现滚雪球。
socket buffer缓存到底在什么情况下会影响读写性能?
在读写的相关环节之间有较大的性能差距时,缓存会有比较大的影响。比如,进程要把数据写到硬盘里。因为硬盘写的速度很慢,而内存很快,所以可以先把数据写到内存里,然后应用程度写操作就很快返回,应用程序此时觉得很快写完了。后续这些数据将由内核帮助应用把数据从内存再写到硬盘里。无论如何,当写操作产生数据的速度,大于实际要接受数据的速度时,buffer才有意义。在我们当前的测试环境中,数据下载时,web服务器是数据发送方,客户端是数据接收方,中间通过虚拟机的网络传输。在计算机上,一般原则上讲,读数据的速率要快于写数据的速率。所以此时两个虚拟机之间并没有写速率大于度速率的问题,此时,调整socket缓存对tcp、udp不存在性能影响。当client向server做udp传输发送数据时,client的socket buffer可能会存在拥塞,这是因为cpu写buffer的速度可能会大于网卡读数据发送速度,因此调整client端的socket send buffer可能会提升网络性能。
UDP编程中的问题解决方案
1. 发送端丢包
适用条件: ①发送端是可以控制的.②微秒数量级的延迟可以接受
解决方法:控制发送端的发送频率和发包大小,一方面防止超出接收端处理能力,另一方面防止超出发送端负载能力(udp socket数据报大小限制、网卡发送能力限制).
2.接收端丢包1
适用条件:①无法控制发送端发送数据的频率
解决方法: 用recvfrom函数收到数据之后尽快返回,进行下一次recvfrom。可以通过多线程+队列来解决。收到数据之后将数据放入队列中,另起一个线程去处理收到的数据
3.接收端丢包2
适用条件:①使用方法2依然出现大规模丢包的情况,需要进一步优化
解决方法:使用setsockopt修改接收端的缓冲区大小(修改rmem_max和setsockopt())
4.程序丢包造成阻塞
使用条件:recvfrom是阻塞函数,如果不想在没有数据时服务器一直阻塞下去,需要设置超时
解决方法:①设置selete进行超时设置,超过规定时长没有数据到来也返回
struct timeval send_interval;
send_interval.tv_sec = 0; // 设置超时时间1ms
send_interval.tv_usec = INTERVAL;
int temp = select(0, NULL, NULL, NULL, &send_interval);
if(temp == -1)
{
exit(1);
}
②使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。
5.UDP报文乱序
解决方法:发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报
6.UDP流量控制
问题描述:TCP有滑动窗口进行流量控制和拥塞控制,反观UDP因为其特点无法做到。UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失(因为内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。
解决方法:增大UDP缓冲区、增加接收方的接收能力、减少发送方的发送能力