前言

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.系统设置

  1. biao@ubuntu:~$ uname -a
  2. 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
  3. biao@ubuntu:~$ cat /proc/sys/net/core/rmem_max
  4. 212992
  5. biao@ubuntu:~$ cat /proc/sys/net/core/wmem_max
  6. 212992
  7. biao@ubuntu:~$ cat /proc/sys/net/core/rmem_default
  8. 212992
  9. biao@ubuntu:~$ cat /proc/sys/net/core/wmem_default
  10. 212992
  11. biao@ubuntu:~$
  • rmem_max:一个Socket的读缓冲区可由程序设置的最大值,单位字节;
  • wmem_max:一个Socket的写缓冲区可由程序设置的最大值,单位字节;
  • rmem_default:一个Socket的被创建出来时,默认的读缓冲区大小,单位字节;
  • wmem_default:一个Socket的被创建出来时,默认的写缓冲区大小,单位字节;

2.应用程序级修改缓冲区大小

我们可以在程序中动态地修改(通过setsockopt系统调用)持有的有效Socket的读写缓冲区大小。

setsockopt.c

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. int main(int argc, char **argv)
  8. {
  9. if (argc != 2)
  10. {
  11. printf("Usage: %s $RCFBUFSIZE\n", argv[0]);
  12. goto error;
  13. }
  14. int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  15. if (sockfd < 0)
  16. {
  17. printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
  18. goto error;
  19. }
  20. // 查看系统默认的socket接收缓冲区大小
  21. int defRcvBufSize = -1;
  22. socklen_t optlen = sizeof(defRcvBufSize);
  23. if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &defRcvBufSize, &optlen) < 0)
  24. {
  25. printf("getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
  26. goto error;
  27. }
  28. printf("OS default udp socket recv buff size is: %d\n", defRcvBufSize);
  29. // 按照执行参数设置UDP SOCKET接收缓冲区大小
  30. int rcvBufSize = atoi(argv[1]);
  31. if (rcvBufSize <= 0)
  32. {
  33. printf("rcvBufSize(%d) <= 0, error!!!\n", rcvBufSize);
  34. goto error;
  35. }
  36. printf("you want to set udp socket recv buff size to %d\n", rcvBufSize);
  37. optlen = sizeof(rcvBufSize);
  38. if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
  39. {
  40. printf("setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
  41. goto error;
  42. }
  43. printf("set udp socket(%d) recv buff size to %d OK!!!\n", sockfd, rcvBufSize);
  44. // 查看当前UDP SOCKET接收缓冲区大小
  45. int curRcvBufSize = -1;
  46. optlen = sizeof(curRcvBufSize);
  47. if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &curRcvBufSize, &optlen) < 0)
  48. {
  49. printf("getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
  50. goto error;
  51. }
  52. printf("OS current udp socket(%d) recv buff size is: %d\n",sockfd,curRcvBufSize);
  53. close(sockfd);
  54. exit(0);
  55. error:
  56. if (sockfd >= 0)
  57. close(sockfd);
  58. exit(1);
  59. }

编译 && 运行:

  1. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 10240
  2. OS default udp socket recv buff size is: 212992
  3. you want to set udp socket recv buff size to 10240
  4. set udp socket(3) recv buff size to 10240 OK!!!
  5. OS current udp socket(3) recv buff size is: 20480
  6. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 40960
  7. OS default udp socket recv buff size is: 212992
  8. you want to set udp socket recv buff size to 40960
  9. set udp socket(3) recv buff size to 40960 OK!!!
  10. OS current udp socket(3) recv buff size is: 81920
  11. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 1024
  12. OS default udp socket recv buff size is: 212992
  13. you want to set udp socket recv buff size to 1024
  14. set udp socket(3) recv buff size to 1024 OK!!!
  15. OS current udp socket(3) recv buff size is: 2304
  16. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ ./a.out 1024000
  17. OS default udp socket recv buff size is: 212992
  18. you want to set udp socket recv buff size to 1024000
  19. set udp socket(3) recv buff size to 1024000 OK!!!
  20. OS current udp socket(3) recv buff size is: 425984
  21. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$

我们通过setsockopt系统调用成功地修改了sock的接收缓冲区大小。

但是,代码级的修改缓冲区大小,不是万能的,其受限于系统配置。

可见,我们希望设置接收缓冲区大小为1024*1024B(1MB),但实际并未达到我们的效果,虽然setsockopt成功了!

我们可以通过修改系统运行时的配置(/proc),来动态地“释放权限”,让应用程序可以设置更大的内核读写缓冲区。

3.系统配置级修改缓冲区大小

  1. biao@ubuntu:~/test/udp_client/002_udp_rw_buffer$ su
  2. Password:
  3. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer#
  4. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# echo 262144 > /proc/sys/net/core/rmem_default
  5. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# echo 1048576 > /proc/sys/net/core/rmem_max
  6. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# cat /proc/sys/net/core/rmem_default
  7. 262144
  8. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# cat /proc/sys/net/core/rmem_max
  9. 1048576
  10. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# ls
  11. a.out setsockopt.c
  12. root@ubuntu:/home/biao/test/udp_client/002_udp_rw_buffer# ./a.out 1048576
  13. OS default udp socket recv buff size is: 262144
  14. you want to set udp socket recv buff size to 1048576
  15. set udp socket(3) recv buff size to 1048576 OK!!!
  16. 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?

    这个是和源码有关系:

  1. case SO_SNDBUF:
  2. if (val > sysctl_wmem_max)
  3. val = sysctl_wmem_max;
  4. if ((val * 2 ) < SOCK_MIN_SNDBUF)
  5. sk->sk_sndbuf = SOCK_MIN_SNDBUF;
  6. else
  7. sk->sk_sndbuf = val * 2 ;

系统这么做,猜测可能是由于UDP解包封包需要的额外的空间。所以,我称r/wmem_max为:可由程序设置的缓冲区最大值。

5.缓存大小不一致导致UDP数据包丢失分析

《linux 网络编程》书中说,当发送端的缓存大于接收端的缓存时,发送端发送的数据包长度大于接收端缓存时,接收端会造成数据截断的情况,也就是说它只能接收接收端缓存大小的数据,其余会自动丢弃。

但是,我在即在两台Ubuntu设备设备上测试的时候发现,结果并不是这样,如果发送端发送的数据包大于接收端缓存大小的时候,接收端的应用层根本就接收不到数据,一个字节的数据也接收不到。

出现这种情况,我的个人分析是:但发送端的数据包大于接收端的缓存时,这个数据包是通过分片的方式发送到接收端,接收端进行分片包组装的时候,由于空间不够,不能成功组包数据报,最终导致应用层接收不到数据。从这个结果上来也可以看出,发送一个大于MTU的数据包,在接收端应用层只有接收整个数据包和一个字节也接收不到    ,不存在只接收一个分片的数据包和数据截断的可能。

(三)缺少滑动窗口导致的数据包丢失

未完.......

致谢:

博文内容大部分引用自下面文章,真诚感谢~

  1. 《数据链路层——最大传输单元MTU_HXiaoFan的博客-CSDN博客_最大传输单元mtu》
  2. 《UDP:Socket缓冲区大小修改与系统设置_test1280-CSDN博客_udp发送缓冲区大小设置》

​​

---------------------------End---------------------------
长按识别二维码
关注 liwen01 公众号

udp编程及udp常见问题处理的更多相关文章

  1. [C# 网络编程系列]专题七:UDP编程补充——UDP广播程序的实现

    转自:http://www.cnblogs.com/zhili/archive/2012/09/03/2666974.html 上次因为时间的关系,所以把上一个专题遗留下的一个问题在本专题中和大家分享 ...

  2. 转:【专题七】UDP编程补充——UDP广播程序的实现

    上次因为时间的关系,所以把上一个专题遗留下的一个问题在本专题中和大家分享下,本专题主要介绍下如何实现UDP广播的程序,下面就直接介绍实现过程和代码以及运行的结果. 一.程序实现 UDP广播程序的实现代 ...

  3. 专题七:UDP编程补充——UDP广播程序的实现

    一.程序实现 UDP广播程序的实现代码: using System; using System.Net; using System.Net.Sockets; using System.Text; us ...

  4. 【网络编程1】网络编程基础-TCP、UDP编程

    网络基础知识 网络模型知识 OSI七层模型:(Open Systems Interconnection Reference Model)开放式通信系统互联参考模型,是国际标准化组织(ISO)提出的一个 ...

  5. 37 - 网络编程-UDP编程

    目录 1 UDP协议 2 UDP通信流程 3 UDP编程 3.1 构建服务端 3.3 常用方法 4 聊天室 5 UDP协议应用 1 UDP协议 UDP是面向无连接的协议,使用UDP协议时,不需要建立连 ...

  6. Linux学习四:UDP编程(上)

    关于UDP和TCP对比优缺,这里就不说了. 使用UDP代码所掉用的函数和用于TCP的函数非常类似,这主要因为套接口库在底层的TCP和UDP的函数上加了一层抽象,通过这层抽象使得编程更容易,但失去了一些 ...

  7. JAVA--网络编程(UDP)

    上午给大家简单介绍了一下TCP网络通信的知识,现在就为大家补充完整网络编程的知识,关于UDP的通信知识. UDP是一种不可靠的网络协议,那么还有什么使用价值或必要呢?其实不然,在有些情况下UDP协议可 ...

  8. [C# 网络编程系列]专题六:UDP编程

    转自:http://www.cnblogs.com/zhili/archive/2012/09/01/2659167.html 引用: 前一个专题简单介绍了TCP编程的一些知识,UDP与TCP地位相当 ...

  9. Socket编程实践(12) --UDP编程基础

    UDP特点 无连接,面向数据报(基于消息,不会粘包)的传输数据服务; 不可靠(可能会丢包, 乱序, 反复), 但因此普通情况下UDP更加高效; UDP客户/服务器模型 UDP-API使用 #inclu ...

  10. 【Socket编程】通过Socket实现UDP编程

    通过Socket实现UDP编程 UDP通信: 1.UDP协议(用户数据报协议)是无连接.不可靠.无序的. 2.UDP协议以数据报作为数据传输的载体. 3.使用UDP进行数据传输时,首先需要将要传输的数 ...

随机推荐

  1. 如何屏蔽各大AI公司爬虫User Agent

    罗列各大AI公司Scraper爬虫Crawler使用的User Agent,教您如何在robots.txt里面屏蔽这些爬虫的访问,禁止它们下载您的网站内容以训练 AI 模型,保护数据,降低带宽,防止宕 ...

  2. ElasticSearch之Exists API

    检查指定名称的索引是否存在. 命令样例如下: curl -I "https://localhost:9200/testindex_002?pretty" --cacert $ES_ ...

  3. IOS关闭锁屏状态下左滑相机

    IOS 锁屏状态下,左滑就会打开相机,还不能关闭.这种功能说真的,没有啥用,还很麻烦.看了一圈教程,写的也是没写全.自己再写一个,以后换手机还用得上. 注:此方法会导致微信的扫一扫不可用 1.找到&q ...

  4. Windows Server2016 默认使用英文输入法或默认使用中文输入法

    1.确认是Server2016操作系统及以后版本 2.打开开始菜单"设置"--"时间和语言" 3.添加英文输入法(已存在可以跳过) 找到"区域与语言& ...

  5. Docker安装 配置

    Docker的技术原理: 1. Linux 命名空间(Namespaces) 进程命名空间:使得每个容器拥有独立的进程空间,互相隔离,不受其他容器影响. 网络命名空间:提供独立的网络栈,每个容器有自己 ...

  6. dart的语法

    dart的语法 main方法 main(){ print("有返回值"); } void main() { print("没有返回值"); } 字符串的定义的方 ...

  7. vue全屏

    <template> <div> <img src="../assets/fangda.png" @click="toggleFullscr ...

  8. Java 设置Excel页面背景

    本文介绍通过Java 程序在Excel表格中设置页面背景的方法,可设置颜色背景(即指定单一颜色作为背景色).图片背景(即加载图片设置成页面背景).程序中需要使用免费版Excel类库工具 Free Sp ...

  9. 9个GaussDB常用的对象语句

    摘要:本文介绍了9个GaussDB常用的对象语句,希望对大家有帮助. 本文分享自华为云社区<GaussDB对象相关语句>,作者:酷哥. 1. 常用函数 pg_database_size() ...

  10. 一文带你熟知ForkJoin

    摘要:ForkJoin将复杂的计算当做一个任务,而分解的多个计算则是当做一个个子任务来并行执行. 本文分享自华为云社区<[高并发]什么是ForkJoin?看这一篇就够了!>,作者:冰 河. ...