前言

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的数据包,在接收端应用层只有接收整个数据包和一个字节也接收不到    ,不存在只接收一个分片的数据包和数据截断的可能。

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

未完.......

致谢:

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

  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. 从零玩转前后端加解密之SM2-sm2

    title: 从零玩转前后端加解密之SM2 date: 2022-08-21 19:42:00.907 updated: 2023-03-30 13:28:48.866 url: https://ww ...

  2. Asp .Net Core 集成 FluentValidation 强类型验证规则库

    目录 入门程序 安装 案例:登录 验证器 内置验证器 自定义验证器 编写自定义验证器 可重复使用的属性验证器 本地化 DI 自动验证 官网:https://docs.fluentvalidation. ...

  3. Luogu P5515 [MtOI2019]灵梦的计算器

    简化题意 给定三个实数 \(n, a, b\),求方程 \(\left \lfloor {x ^ a + x ^ b} \right \rfloor = \left \lfloor {n ^ a + ...

  4. MySQL系列:索引(B+Tree树、构建过程、回表、基本操作、执行计划、应用)

    介绍 https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html 作用 优化查询 算法 索引的算法包括 BTree Hash R ...

  5. three.js中帧缓存的使用

    目录 1. 概述 2. 示例 2.1. 代码 2.2. 解析 3. 参考 1. 概述 在网上查阅了一下three.js关于帧缓存的使用,感觉很多都是关于three.js中后处理通道的使用的.后处理通道 ...

  6. 浅学GoF23种设计模式

    long long ago 买了设计模式的书,一直没看,平常工作虽然涉及到,但是不够系统,工作之余抽空学习一下. 一.创建型模式 01.单例(Singleton) 02.工厂方法(Factory Me ...

  7. 第四部分_Shell脚本数组和其他变量

    数组定义 ㈠ 数组分类 普通数组:只能使用整数作为数组索引(元素的下标) 关联数组:可以使用字符串作为数组索引(元素的下标) ㈡ 普通数组定义 可以切片 一次赋予一个值 #数组名[索引下标]=值 ar ...

  8. 详解ZooKeeper在微服务注册中心的应用

    本文分享自华为云社区<SpringCloud ZooKeeper 详解,以及与Go.Rust等非Java服务的集成>,作者: 张俭. ZooKeeper,是一个开源的分布式协调服务,不仅支 ...

  9. PreSTU:一个专门为场景文本理解而设计的简单预训练模型

    摘要:在视觉与语言(V&L)模型中,阅读和推理图像中的文本的能力往往是缺乏的.我们如何才能学习出强大的场景文本理解(STU)的V&L模型呢? 本文分享自华为云社区<场景文本理解预 ...

  10. vue2升级vue3: 全局变量挂载与类型声明

    全局变量挂载 vue2 Vue.prototype.$lm = {} vue3 const app = Vue.createApp({}) app.config.globalProperties.$l ...