IP分片浅析

一、IP分片原理

在这里插入图片描述
IP分片是网络上传输IP报文的一种技术手段。
IP协议在传输数据包时,将数据报文分为若干分片进行传输,并在目标系统中进行重组。
不同的链路类型规定有不同最大长度的链路层数据帧,称为链路层MTU(最大传输单元)。
常见以太网的MTU为1500,若IP协议在传输数据包时,IP报文长度大于转发接口的MTU,则将数据报文分为若干分片进行传输,分片报文到达接收方时,由接收方完成重组。
对于不同的传输层协议,在IP层上,需不需要进行分片是不同的:
TCP协议:
对于TCP来说,它是尽量避免分片的。因为当在IP层进行了分片后,如果其中的某片数据丢失,则需对整个数据报进行重传。因为IP层本身没有超时重传机制,当来自TCP报文段的某一片丢失后,TCP在超时后重发整个TCP报文段,该报文段对应于一份IP数据报,没有办法只重传数据报中的一个数据报片。而且如果对数据报分片的是中间路由器,而不是起始端系统,那么起始端系统就无法知道数据报是如何被分片的,因此基于这种原因,TCP是经常要避免分片的。
那么TCP层是如何避免IP层的分片呢?首先,TCP在建立连接时会进行3次握手,而在这3次握手中,客户端和服务端通常会协商一个值,那就是MSS(最长报文大小),用来表示本段所能接收的最大长度的报文段。MSS=MTU-TCP首部大小-IP首部大小,MTU值通过查询链路层得知。
当两端确认好MSS后进行通信,TCP层往IP层传输数据时,如果TCP层缓冲区的大小大于MSS,那么TCP层都会将其中的数据分组进行传输,这样就避免了在IP层进行分片。
UDP协议:
对于UDP而言,由于UDP是不需要保证可靠性的,没有超时和重传机制,这使得UDP很容易导致IP分片。
那么数据报被分片后是怎样的呢,又是以何种算法进行分片呢?首先我们回忆IP首部:
对于每份IP数据报来说,其16位标识字段都包含一个唯一值。在数据报被分片时,这个值同时被复制到每个片中。
在IP首部中,我们看到有一个占了3位的标志字段(上图中Flags)。
其中第1位作为保留字段;
第2位分段用来表示一个数据报是否允许在IP层被分片;
而第3位更多分片字段,则是当数据报被分片时,让接收端知道在什么时候完成所有的分片组装,除了最后一片外,其他每个组成数据报的片都要把该比特置1。
而如果将其中的分段标志比特位置1,表示不允许IP层对数据报进行分片。例如当路由器收到一份需要分片的数据报,而在IP首部又设置了不分片(DF)的标志比特,路由器会丢弃数据报并发送一个ICMP差错报文(“需要进行分片但设置了不分片比特”)。
IP首部中13位的偏移量字段表示IP分片在整个数据流中的位置,第一个数据报分片的偏移量置为0,而后续的分片偏移量则是根据网络的MTU大小设置,且必须为8的整数倍。
需要注意的是,任何传输层的首部只出现在第一片数据中,后续报文均不带传输层信息。
这是IP分片的第一个数据分片,可以看出其IP的上层协议为ICMP协议,而在后续的分片报文中却无法看到上层协议的具体信息。
接收方在收到经过IP层分片的数据报文后,首先根据分片标志中的更多分段位判断是否是最后一个分片报文,如果是,则根据分片偏移量计算各个分片报文在原始数据报中的位置,进行重组。如果不是最后一个分片,则需等待所有分片到达后再完成重组。

二、分片在生产中的实际问题案例

在实际生产环境中遇到了UDP大包分片的问题:
在openstack云环境中,我们使用Linux bridge的iptables规则来实现安全组,但是当UDP的包大于MTU(1500)时,当数据包从tap口发到qbr(linux bridge),qbr会先将分片包进行重组(因为netfiler要针对有状态的数据包进行连接跟踪),当处理完毕后,再将数据包UDP封装发出(分片中都携带UDP报头,这种封装方式是错误的),导致客户端重组UDP报文出错,连接无法建立。
分片数据包的大小:
UDP包在MTU1500下的大小:(1500 Bytes) - IP头(20Bytes)- UDP头(8Bytes)= 1472(Bytes)
示例:数据包length为2730的报文,如何分两片:
length 2730 = 1472(1500-28) + 1258(1278-20因为除了一个分片外其余的分片不包含udp数据头)
第一片(既有IP头,也有传输层头):length 1500 = 1472 + 8 (UDP头) + 20 (IP头)
第二片(只有IP头,不带传输层头):length 1278 = 1258 + 20 (IP头)
在这里插入图片描述
qbr的错误重组后不再进行分片转发,直接封装UDP:
在这里插入图片描述在这里插入图片描述#https://bugs.launchpad.net/neutron/+bug/1542032

三、分片带来的问题

1.分片带来的性能消耗
分片和重组会消耗发送方、接收方一定的CPU等资源,如果存在大量的分片报文的话,可能会造成较为严重的资源消耗;
分片对接收方内存资源的消耗较多,因为接收方要为接收到的每个分片报文分配内存空间,以便于最后一个分片报文到达后完成重组。
2.分片丢包导致的重传问题
如果某个分片报文在网络传输过程中丢失,那么接收方将无法完成重组,如果应用进程要求重传的话,发送方必须重传所有分片报文而不是仅重传被丢弃的那个分片报文,这种效率低下的重传行为会给端系统和网络资源带来额外的消耗。
3.分片攻击
黑客构造的分片报文,但是不向接收方发送最后一个分片报文,导致接收方要为所有的分片报文分配内存空间,可由于最后一个分片报文永远不会达到,接收方的内存得不到及时的释放(接收方会启动一个分片重组的定时器,在一定时间内如果无法完成重组,将向发送方发送ICMP重组超时差错报文,,只要这种攻击的分片报文发送的足够多、足够快,很容易占满接收方内存,让接收方无内存资源处理正常的业务,从而达到DOS的攻击效果。
4.安全隐患
由于分片只有第一个分片报文具有四层信息而其他分片没有,这给路由器、防火墙等中间设备在做访问控制策略匹配的时候带来了麻烦。
如果路由器、防火墙等中间设备不对分片报文进行安全策略的匹配检测而直接放行IP分片报文,则有可能给接收方带来安全隐患和威胁,因为黑客可以利用这个特性,绕过路由器、防火墙的安全策略检查对接收方实施攻击;
如果路由器、防火墙等中间设备对这些分片报文进行重组后在匹配其安全策略,那么又会对这些中间设备的资源带来极大的消耗,特别是在遇到分片攻击的时候,这些中间设备会在第一时间内消耗完其所有内存资源,从而导致全网中断的严重后果。

<think>我们正在讨论UDP协议中recvfrom函数接收数据时的数据量限制和行为。根据用户提供的引用和问题,我们将从以下几个方面进行详细解析: 1. **UDP数据包大小限制**: - UDP协议本身规定单个数据报的最大长度为65535字节(包括8字节的UDP头部)[^1]。但在实际网络中,由于底层数据链路层的限制(如以太网的MTU通常为1500字节),发送过大的UDP数据包会导致分片传输,增加丢包风险。 - 在应用程序中,使用`recvfrom`接收数据时,需要指定缓冲区大小。如果接收缓冲区小于到达的数据报长度,多余的部分会被丢弃(在Linux中会截断数据报并丢弃剩余部分,同时不通知应用程序)[^3]。 2. **接收缓冲区与排队机制**: - UDP套接字有一个接收缓冲区,所有到达该套接字的数据报都进入此缓冲区排队,遵循FIFO(先进先出)原则[^3]。 - 当应用程序调用`recvfrom`时,从缓冲区中取出下一个数据报。如果缓冲区为空,则进程阻塞(默认行为)或返回错误(非阻塞模式)。 3. **高性能处理建议**: - 对于高速UDP数据流(如引用[^4]的场景),建议使用多线程:一个线程专用于调用`recvfrom`接收数据并放入队列,另一个线程从队列中取出数据处理。这能避免因处理耗时导致的数据丢失[^4]。 - 设置适当的套接字接收缓冲区大小(通过`setsockopt`调整`SO_RCVBUF`),防止缓冲区溢出。 4. **超时设置**: - 引用[^5]提到,可通过设置套接字选项`SO_RCVTIMEO`为`recvfrom`添加超时限制,避免永久阻塞[^5]。 ### 总结: - **行为**:`recvfrom`每次调用返回一个完整UDP数据报(除非缓冲区过小导致截断)。 - **限制**:单次接收的数据量受两个因素约束: 1. 应用程序指定的接收缓冲区大小(如`recvfrom`的`bufsize`参数)。 2. 操作系统网络栈的UDP接收缓冲区大小(可通过`SO_RCVBUF`调整)。 ### 代码示例(C语言): ```c // 设置接收缓冲区大小为2MB int rcvbuf_size = 2 * 1024 * 1024; setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size)); // 设置接收超时为5秒 struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); // 接收数据 char buffer[65535]; // 足够容纳最大UDP数据报 ssize_t num_bytes = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &addr_len); if (num_bytes == -1) { // 处理超时或错误 } ``` ### 注意事项: - 若预期接收大数据报,应确保应用缓冲区足够大(如≥65535字节),并检查`num_bytes`是否等于`sizeof(buffer)`以判断是否发生截断。 - 在Linux中,可通过`cat /proc/sys/net/core/rmem_max`查看系统允许的最大接收缓冲区大小。 §§相关问题§§ 1. 如何优化UDP协议传输大文件? 2. UDP分片传输的原理是什么? 3. 如何检测UDP数据包在传输过程中被截断? 4. 在多线程UDP服务器中,如何避免数据包处理顺序错乱? [^1]: UDP协议 sendto 和 recvfrom 浅析与示例。UDP是面向非连接的协议,它不与对方建立连接,而是直接把数据报发给对方。UDP无需建立类如三次握手的连接,使得通信效率很高。因此UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景。 [^3]:(UNIX网络编程学习笔记):UDP概述、recvfrom和sendto函数及案例。【注意】 UDP层中隐含有排队发生。每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区。当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO(先入先出)顺序返回给进程。在进程能够读该套接字中任何已排好队的数据报之前,如果有多个数据报到达该套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中(这个缓冲区的大小是有限的)。 [^4]: UDP接收大量高速数据并处理。 // 接收数据 data.client_address_len = sizeof(data.client_address); ssize_t num_bytes = recvfrom(sock, data.buffer, sizeof(data.buffer), 0, (struct sockaddr*)&data.client_address, &data.client_address_len); if (num_bytes > 0) { std::lock_guard<std::mutex> lock(queueMutex); dataQueue.push(data); queueCondition.notify_one(); } } } [^5]: linux系统设置udp超时时间,UDP recvfrom超时设置问题。从昨天到今天一真在搞这个网络断线检测的问题,一直没搞好,网上大多都说用ping 可以搞定,不过我就是搞不定,没办法之下想了个不是办法的办法,在服务器上开了一个 UDP端口,监听数据,然后回发,不用TCP是因为TCP建立连接太慢了,我发送一个测试数据,数据量小用不着TCP。</think>### UDP中`recvfrom`接收数据的行为与限制分析 在UDP协议中使用`recvfrom`接收数据时,其行为和数据量限制主要受以下因素影响: #### 1. **单次接收的最大数据量** - **协议层限制**:单个UDP数据报的最大理论长度为 **65,535字节**(包括8字节UDP头部),计算公式为: $$最大数据长度 = 2^{16} - 1 - 8 = 65,527 \text{字节}$$ 这是由IP数据报的16位总长度字段决定的[^1][^2]。 - **应用层限制**:`recvfrom`函数的缓冲区大小决定了实际能接收的数据量: ```c char buffer[BUFFER_SIZE]; // 接收缓冲区 recvfrom(sockfd, buffer, BUFFER_SIZE, 0, ...); ``` 如果`BUFFER_SIZE`小于到达的数据报长度: - 在Linux中:**截断数据报**,多余部分被丢弃 - 在Windows中:返回`WSAEMSGSIZE`错误 - 引用[^3]证实此行为:*"当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO顺序返回给进程"* #### 2. **接收队列行为** - **排队机制**:UDP套接字维护一个接收缓冲区(队列): - 数据报按**FIFO(先进先出)** 顺序处理[^3] - 多个同时到达的数据报在缓冲区排队 - 缓冲区大小可通过`SO_RCVBUF`套接字选项调整: ```c int size = 1024 * 1024; // 1MB setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); ``` - **高并发处理**:如引用[^1][^4]所示: - 顺序执行的`recvfrom`仍能处理并发请求 - 推荐使用**生产者-消费者模型**避免阻塞: ```cpp // 接收线程(生产者) while (true) { recvfrom(...); // 接收数据 std::lock_guard lock(queueMutex); dataQueue.push(data); // 入队 } // 处理线程(消费者) while (true) { auto data = dataQueue.pop(); // 出队 process(data); } ``` #### 3. **超时控制** - 通过`SO_RCVTIMEO`设置超时: ```c struct timeval tv; tv.tv_sec = 5; // 5秒超时 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); ``` 超时后`recvfrom`返回-1并设置`errno=EAGAIN`[^5]。 #### 4. **性能优化建议 1. **缓冲区设置**: - 至少设置为 **65,535字节** 避免截断 - 通过`getsockopt`检查系统允许的最大值 2. **多线程处理**: - 单接收线程 + 多处理线程(如引用[^4]) - 避免在`recvfrom`中执行耗时操作 3. **流量控制**: - 监控接收队列长度 - 动态调整处理能力 ### 总结 `recvfrom`单次接收上限为 **65,527字节**(应用层数据),实际接收量受缓冲区大小限制。其核心行为特点是**无连接、基于数据报、FIFO队列处理**。在高负载场景下,需结合队列管理和多线程优化[^1][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值