为什么我们使用 UDP 发送会有最大的限制,为什么在 RTP 发送视频码流时会分包为小于 MTU 的包去发送?这一切都要从 IP 层的协议说起。
OSI 七层协议大家都已经非常清楚,其中比较典型的 TCP/IP 协议栈就实现了其中的传输层和网络层,IP (Internet Protocol)协议就是一个网络层的协议,它主要实现了对传输层 (TCP, UDP, RawIp)发送的数据进行分片发送,并决定路由,对收到的 IP 分片进行组帧。然后到下面的链路层。但问题来了,一般链路层都会有自己能承受的最大发送单元(MTU)这个限制,如果是以太网组成的链路,那么 MTU 一般为 1500,也就是说,链路层所承受的载荷最大不能超过 1500,否则发送将失败,也就是整个数据包在链路层,它会认为是由链路层的头部,以太网的为
14(6+6+2)字节的头,加上载荷,而这个载荷不能超过 1500,也就成为网络层,这里我们叫它 IP 层,它的头部加上自己的载荷不能超过 1500,这个 MTU 的限制,所以在 IP 层向链表层封包时,要确保自己的整个数据包不大于 MTU,那么不大于 MTU 的数据到了 IP 层也不用分片,在某种程序上就会使效率提交。
那么这个 MTU 是不是我们使用 UDP 发送时的最大单次发送限制呢?不是,为了让传输层能够使用方便,即使传输层发送的数据大于 MTU 也没有关系,因为 IP 层会对大于 MTU 的数据进行分片,我们来看看 IP 层协议的头信息就知道了。
struct iphdr { __u8 version:4, ihl:4; __u8 tos; __be16 tot_len; __be16 id; __be16 frag_off; __u8 ttl; __u8 protocol; __sum16 check; __be32 saddr; __be32 daddr; /*The options start here. */ };
我们关注一下几个与本文相关的字段, tot_len是本 IP分片的总长度,包括 IP 头,id 是一个唯一标识,标识相同的 IP 包的不同分片,frag_off 高 13 位表示本分片的 IP 载荷的偏移,它乘以 8 才是真正的字节偏移。 低 3 位表示一个 flag, 分别表示保留位, DF(Don‘t fragment), MF(More fragment)位,DF 位表示不对数据进行分片,MF 位表示,该分片不是最后一个分片,本 IP 包还有更多的分片,所以只有最后一个分片才会清该位。那么这些信息已经足够对一个
IP 包进行拆分和重组了。由于 tot_len 是 2 个字节,所以一个 IP 包最大只能表示 65535 个字节的数据,所以 UDP 最大一次能发送的数据其实应该为 65535 - 20(IP 头大小) - 8(UDP头大小) = 65507 个用户数据。而由于 frag_off 的低 3 位其实是当作标志来使用的,所以标识一个片的偏移只能是 8 的倍数,那么也就意味着上一个分片的数据大小,因为在 IP 层,数据其实是包括传输层头,但不包括 IP 头的,也就是传输层头加上传输层载荷,大小一定是要 8 的倍数,由于最后一个分片后面已经没有分片了,所以它的大小即使不是
8 的倍数也不会影响到后面的偏移。
我们以 ICMP 协议为例,做一个实验。
调用命令 ping -l 2000 172.16.73.92, 也就是带 2000 字节的数据去 ping 172.16.73.92 这台机器。
抓包截图为:
19518 和 19519 这两个数据包为同一个 IP 包的两个分片,是一次 ping 的请求,因为 2000 个用户字节肯定要分为两个 IP 分片,分两个以太网包发出去,上图为 19518 数据包的详细信息,其中 Total Length: 1500 表明本包数据总大小为 1500 包括 IP 头 (20) 和 IP 载荷(包括ICMP 头 8),即该包传送了 1500 - 20 个字节的 IP 载荷,那么下个分片的偏移应该从 1480 开始,Id 为 0xe1f8, MF 标志为 1,表示还有分片,下个包的详细信息为:
该分片 Id 与上个数据包相同,表明是同一个 IP 包,MF 标志为 0,表明是最后一包,偏移就是我们计算得来的 1480,总长度为 548,去除 IP 头,即 528, 第一个包中的 IP 载荷包含了 ICMP 头的 8 个字节,即,第一个包的用户数据为 1500 - 20 - 8 = 1472, 第二个包的用户数据为 548 - 20 = 528, 一共有 2000 用户数据,与理论一致。
协议栈分层的思想非常的清晰,每一层中只关心自己的头部信息与自己的载荷,而该层的载荷其实就是上一层的头部信息和上一层的载荷,每一层不会去关心上一层的头部信息与载荷的情况,这样就使得协议栈条理清晰并且易于扩展。