前言
UDP协议是User Datagram Protocol的缩写,它是无连接,不可靠的网络协议。一般使用它进行实时性数据的传输,主要是因为它快,但因为它是不可靠的一种传输协议,所以不可避免的会出现丢包现象。本文就具体讨论导致UDP传输数据包丢失的原因以及一些基本的规避方法:
- 路由器转发造成的数据包丢失
- 数据链路层MTU造成的数据包丢失
- 缺少滑动窗口导致的数据包丢失
- 收发缓存区大小造成的数据包丢失
(一)数据链路层MTU造成的数据包丢失
1.数据链路层的以太网帧结构
以太网帧结构由四个字段组成,各字段含义为:
- 目的地址:该地址指的是MAC地址,指该数据要发送至哪里
- 源地址:MAC地址,填本地MAC地址,指该数据从哪里来
- 类型:值该数据要交给上层(网络层)的那个协议(IP协议,ARP协议…)
- 数据:要传输的数据,不过该数据有长度的要求,是在46–1500字节之间,该长度称为最大传输单元即MTU
- 若数据长度不够46字节,则需要填充内容;若数据长度超过1500字节,则需要分片传输。
2.MTU
MTU maximum transmission unit,最大传输单元,由硬件规定,如以太网的MTU为1500字节,是指在传输数据过程中允许报文的最大长度。
3.MTU对IP协议的影响
- IP报文在超过MTU后需要分片,接收端需要组装;
- 一旦分片后的IP报文有一部分丢失,则接收端组装会失败,对于整个IP报文而言相当于传输失败,而IP协议不会负责重新传输数据;
- 由于MTU影响的IP报文的分片和组装会加大报文丢失的可能性;
- 报文的分片和组装由IP层自己做,会加大传输的成本,降低性能。
4.MTU对UDP协议的影响
- UDP协议的报头为固定的20字节;
- 若UDP数据的长度超过(1500-20)1480字节,则数据在网络层会分片;
- 数据的分片会加大数据丢失的可能性。
5.MTU对TCP协议的影响
- TCP协议的报头长度为20–60字节;
- 若TCP报文的总长度超过1500字节,则数据同样在网络层会分片;
- TCP单个数据报的最大长度称为最大段尺寸MSS;
- 在TCP三次握手建立连接的时候,双方会商量传输中MSS的大小;
- 与UDP相同的是,分片越多数据丢包的可能性越大,可靠性也就越差。
6.实际测试结果
我们可以发现由于MTU的存在,对于传输的报文长度有限制而导致的分片,会增加数据丢包的可能性,也会降低数据传输的性能;所以在网络中传输数据时尽量将数据的大小控制在不造成分片的最大长度。
(二)收发缓存区大小造成的数据截断
每个Socket在Linux中都映射为一个文件,并与内核中两个缓冲区(读缓冲区、写缓冲区)相关联。
或者说,每个Socket拥有两个内核缓冲区。
有时,我们需要修改缓冲区的内核限制的最大值,使其符合我们的实际需求。
1.系统设置
biao@ubuntu:~$ uname -a
Linux ubuntu 4.8.0-36-generic #36~16.04.1-Ubuntu SMP Sun Feb 5 09:39:57 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
biao@ubuntu:~$ cat /proc/sys/net/core/rmem_max
212992
biao@ubuntu:~$ cat /proc/sys/net/core/wmem_max
212992
biao@ubuntu:~$ cat /proc/sys/net/core/rmem_default
212992
biao@ubuntu:~$ cat /proc/sys/net/core/wmem_default
212992
biao@ubuntu:~$
- rmem_max:一个Socket的读缓冲区可由程序设置的最大值,单位字节;
- wmem_max:一个Socket的写缓冲区可由程序设置的最大值,单位字节;
- rmem_default:一个Socket的被创建出来时,默认的读缓冲区大小,单位字节;
- wmem_default:一个Socket的被创建出来时,默认的写缓冲区大小,单位字节;
2.应用程序级修改缓冲区大小
我们可以在程序中动态地修改(通过setsockopt系统调用)持有的有效Socket的读写缓冲区大小。
setsockopt.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: %s $RCFBUFSIZE\n", argv[0]);
goto error;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
goto error;
}
// 查看系统默认的socket接收缓冲区大小
int defRcvBufSize = -1;
socklen_t optlen = sizeof(defRcvBufSize);
if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &defRcvBufSize, &optlen) < 0)
{
printf("getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
goto error;
}
printf("OS default udp socket recv buff size is: %d\n", defRcvBufSize);
// 按照执行参数设置UDP SOCKET接收缓冲区大小
int rcvBufSize = atoi(argv[1]);
if (rcvBufSize <= 0)
{
printf("rcvBufSize(%d) <= 0, error!!!\n", rcvBufSize);
goto error;
}
printf("you want to set udp socket recv buff size to %d\n", rcvBufSize);
optlen = sizeof(rcvBufSize);
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
{
printf("setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
goto error;
}
printf("set udp socket(%d) recv buff size to %d OK!!!\n", sockfd, rcvBufSize);
// 查看当前UDP SOCKET接收缓冲区大小
int curRcvBufSize = -1;
optlen = sizeof(curRcvBufSize);
if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &curRcvBufSize, &optlen) < 0)
{
printf("getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
goto error;
}
printf("OS current udp socket(%d) recv buff size is: %d\n",sockfd,curRcvBufSize);
close(sockfd);
exit(0);
error:
if (sockfd >= 0)
close(sockfd);
exit(1);
}
编译 && 运行:
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 10240
OS default udp socket recv buff size is: 212992
you want to set udp socket recv buff size to 10240
set udp socket(3) recv buff size to 10240 OK!!!
OS current udp socket(3) recv buff size is: 20480
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 40960
OS default udp socket recv buff size is: 212992
you want to set udp socket recv buff size to 40960
set udp socket(3) recv buff size to 40960 OK!!!
OS current udp socket(3) recv buff size is: 81920
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 1024
OS default udp socket recv buff size is: 212992
you want to set udp socket recv buff size to 1024
set udp socket(3) recv buff size to 1024 OK!!!
OS current udp socket(3) recv buff size is: 2304
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 1024000
OS default udp socket recv buff size is: 212992
you want to set udp socket recv buff size to 1024000
set udp socket(3) recv buff size to 1024000 OK!!!
OS current udp socket(3) recv buff size is: 425984
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$
我们通过setsockopt系统调用成功地修改了sock的接收缓冲区大小。
但是,代码级的修改缓冲区大小,不是万能的,其受限于系统配置。
可见,我们希望设置接收缓冲区大小为1024*1024B(1MB),但实际并未达到我们的效果,虽然setsockopt成功了!
我们可以通过修改系统运行时的配置(/proc),来动态地“释放权限”,让应用程序可以设置更大的内核读写缓冲区。
3.系统配置级修改缓冲区大小
biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ su
Password:
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer#
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# echo 262144 > /proc/sys/net/core/rmem_default
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# echo 1048576 > /proc/sys/net/core/rmem_max
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# cat /proc/sys/net/core/rmem_default
262144
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# cat /proc/sys/net/core/rmem_max
1048576
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# ls
a.out setsockopt.c
root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# ./a.out 1048576
OS default udp socket recv buff size is: 262144
you want to set udp socket recv buff size to 1048576
set udp socket(3) recv buff size to 1048576 OK!!!
OS current udp socket(3) recv buff size is: 2097152
我们在root下,修改了系统运行时的配置:
/proc/sys/net/core/rmem_default
/proc/sys/net/core/rmem_max
我们设置读缓冲区默认值为256KB,最大值为1MB。程序运行时,我们希望设置读缓冲区为1MB。通过输出信息,我们可以验证,修改/proc中的配置文件,我们使得一个socket默认的读缓冲区为256KB,读缓冲区最大值为1MB。
setsockopt系统调用级设置受限于系统运行时配置,可以通过修改系统配置,使得程序设置更大的读写缓冲区。
4.需要注意的两点:
-
当系统关机重启时,对/proc的修改,是否依然存在?
不会。这就比较重要,若服务器由于异常宕机,重启后失去了原有的设置,就有可能导致接收缓冲区过小,出现UDP丢包的可能。 -
为什么我通过setsockopt设置读缓冲区值为rcvBufSize,但实际getsockopt获取的读缓冲区大小是2*rcvBufSize?
这个是和源码有关系:
case SO_SNDBUF:
if (val > sysctl_wmem_max)
val = sysctl_wmem_max;
if ((val * 2 ) < SOCK_MIN_SNDBUF)
sk->sk_sndbuf = SOCK_MIN_SNDBUF;
else
sk->sk_sndbuf = val * 2 ;
系统这么做,猜测可能是由于UDP解包封包需要的额外的空间。所以,我称r/wmem_max为:可由程序设置的缓冲区最大值。
5.缓存大小不一致导致UDP数据包丢失分析
《linux 网络编程》书中说,当发送端的缓存大于接收端的缓存时,发送端发送的数据包长度大于接收端缓存时,接收端会造成数据截断的情况,也就是说它只能接收接收端缓存大小的数据,其余会自动丢弃。
但是,我在即在两台Ubuntu设备设备上测试的时候发现,结果并不是这样,如果发送端发送的数据包大于接收端缓存大小的时候,接收端的应用层根本就接收不到数据,一个字节的数据也接收不到。
出现这种情况,我的个人分析是:但发送端的数据包大于接收端的缓存时,这个数据包是通过分片的方式发送到接收端,接收端进行分片包组装的时候,由于空间不够,不能成功组包数据报,最终导致应用层接收不到数据。从这个结果上来也可以看出,发送一个大于MTU的数据包,在接收端应用层只有接收整个数据包和一个字节也接收不到 ,不存在只接收一个分片的数据包和数据截断的可能。
(三)缺少滑动窗口导致的数据包丢失
未完.......
致谢:
博文内容大部分引用自下面文章,真诚感谢~
- 《数据链路层——最大传输单元MTU_HXiaoFan的博客-CSDN博客_最大传输单元mtu》
- 《UDP:Socket缓冲区大小修改与系统设置_test1280-CSDN博客_udp发送缓冲区大小设置》