说说【Linux网络】深入理解网络层IP协议:从公网路由到IP分片与组装(学习笔记)

开发过程中有些细节容易被忽略,今天挑几个重点聊一聊。

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

文章目录

二. 路由:网络层的核心功能 三. IP 报文的分片与组装 四. Linux 内核 IP 分片与组装源码解读 结尾

前言

当你在浏览器输入www.baidu.com并按下回车时,数据是如何从你的电脑穿越层层网络到达百度服务器的?为什么不同局域网内的主机能使用相同的192.168.1.x地址却不会冲突?为什么大文件传输时不会因为数据包过大而被丢弃?这些麻烦的答案都藏在 TCP/IP 协议栈的网络层中。网络层的核心协议 ——IP 协议,负责为每一个数据包找到从源主机到目的主机的最佳路径,解决 “去哪里"和"怎么走” 的麻烦。这篇文章将从公网的本质讲起,深入解析路由转发的核心逻辑,最后拆解 IP 分片与组装的底层实现,带你彻底搞懂网络层的工作原理。

一. 尝试搞懂公网

1.1 NAT 技术:缓解 IP 地址枯竭的关键

我们都清楚 IPv4 地址只有 32 位,理论上最多只能给出约 43 亿个地址。但全球联网设备早已远超这个数字,这其中 NAT(网络地址转换) 技术功不可没。

  • 路由器的双 IP 结构:每个家用路由器都有两个 IP 地址
WAN 口 IP:运营商分配的公网 IP(或上级运营商的私有 IP)
  • LAN 口 IP:局域网网关 IP,通常为192.168.1.1/24
NAT 的工作原理:子网内的主机与外网通信时,路由器会将 IP 首部中的源私有 IP 替换为自己的 WAN 口 IP,并记录端口映射关系。回复报文到达时,再根据映射关系将目的 IP 替换回对应的私有 IP。私有 IP 地址段(RFC 1918 规定):
  • 10.0.0.0/8:前 8 位为网络号,共 1677 万个地址
  • 172.16.0.0/12:前 12 位为网络号,共 104 万个地址
  • 192.168.0.0/16:前 16 位为网络号,共 65536 个地址
核心结论:同一个子网内 IP 地址不能重复,不同子网 IP 地址能重复。NAT 技术让私有 IP 不会出现在公网中,变相极大地缓解了 IP 地址不足的麻烦。

1.2 公网的层级结构:从国际到家庭

真实的公网并不是一个扁平的网络,而是一个多层级的树形结构,我们能简化为以下四层:

1. 国际骨干网:由各国的国际路由器组成,通过海底光缆连接。每个国家向 ICANN 申请 IP 地址段,例如中国拥有5.0.0.0/8这样的大段地址。
2. 国内骨干网:国家将 IP 地址进一步划分给各个省份,例如陕西分配到5.1.0.0/16,河北分配到5.5.0.0/16
3. 市级骨干网:省份再将地址划分给各个城市,例如西安分配到5.1.16.0/20
4. 局域网:城市运营商不再使用公网 IP,而是为用户分配私有 IP,构建家庭或企业局域网。

1.3 报文的跨网传输流程

以 “俄罗斯用户访问中国西安的一台服务器” 为例,报文的传输过程如下:

1. 俄罗斯用户的报文首先到达俄罗斯的国际路由器
2. 国际路由器通过BGP 协议查询路由表,发现5.0.0.0/8属于中国,将报文转发给中国的国际路由器
3. 中国的国际路由器查询路由表,发现5.1.0.0/16属于陕西,转发给陕西的省间路由器
4. 陕西的省间路由器查询路由表,发现5.1.16.0/20属于西安,转发给西安的市级路由器
5. 西安的市级路由器通过 NAT 转换,将报文转发给对应的局域网主机

关键逻辑:报文转发的唯一依据是网络号。路由器将目的 IP 与路由表中的子网掩码进行按位与运算,得到网络号后,再转发到对应的下一跳地址。

二. 路由:网络层的核心功能

2.1 路由的基本概念

  • 路由:在复杂的网络结构中,找出一条通往终点的路线。
  • 一跳(Hop):数据链路层中的一个传输区间,在以太网中就是从源 MAC 地址到目的 MAC 地址的帧传输。
  • 点对点通信:IP 协议实现了从源主机到目的主机的端到端通信,而数据链路层只负责一跳内的通信。

2.2 路由表的结构与转发逻辑

每个主机和路由器都维护一张路由表,我们可以在 Linux 系统中使用route命令查看:

whb@iv-ye4ege8iyo5i3z3clix9:~$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    100    0        0 eth0
192.168.0.0     0.0.0.0         255.255.0.0     U     100    0        0 eth0
192.168.0.1     0.0.0.0         255.255.255.255 UH    100    0        0 eth0

路由表关键字段解释:

字段含义Destination目的网络地址Gateway下一跳地址(0.0.0.0 表示直连网络)Genmask子网掩码Flags标志位:U = 有效,G = 下一跳是路由器,H = 目的是主机Iface发送接口

转发逻辑(最长前缀匹配)

1. 将目的 IP 与路由表中每一条的子网掩码进行按位与运算
2. 得到网络号后,与对应的 Destination 字段算是
3. 选择前缀最长(子网掩码中 1 最多)的匹配条目
4. 按照该条目的下一跳地址和发送接口转发报文

转发示例

  • 目的 IP 为192.168.56.3:与255.255.255.0按位与得到192.168.56.0,匹配对应条目,从 eth1 接口直接发送到目的主机
  • 目的 IP 为202.10.1.2:与所有条目都不匹配,使用缺省路由0.0.0.0),转发给网关192.168.10.1

2.3 缺省路由

当目的 IP 与路由表中所有条目都不匹配时,报文会被转发到缺省路由指定的下一跳地址。缺省路由就像我们问路时遇到的 “扫地大妈”,她不清楚具体地址,但会告诉你去问更专业的人。

在路由表中,缺省路由的 Destination 为0.0.0.0,Genmask 为0.0.0.0,Flags 为UG

2.4 路由表生成算法

路由表的生成有两种方式:

  • 静态路由:由网络管理员手动配置,适用于小型网络
  • 动态路由:通过路由协议自动生成,适用于大型网络
距离向量算法(RIP)
  • 链路状态算法(OSPF)
  • 边界网关协议(BGP):用于互联网骨干网的路由协议

三. IP 报文的分片与组装

3.1 MTU 与分片的由来

以太网帧的数据部分有一个最大长度限制,称为MTU(最大传输单元),标准值为1500 字节。如果 IP 数据报的大小超过了 MTU,就需要进行分片,将一个大的数据报分割成多个小的分片,每个分片独立传输。

重要原则:IP 分片对传输层透明。传输层(TCP/UDP)不需要关心数据是否被分片,分片和组装的工作完全由 IP 层负责。

3.2 为什么 IP 分片不应该是主流

虽然 IP 协议支持分片,但分片会带来严重的性能问题:

1. 丢包率大幅上升:如果一个分片丢失,整个 IP 数据报都会被丢弃,需要重传所有分片。例如,将一个报文分成 4 个分片,每个分片的丢包率为 1%,则整个报文的丢包率为1 - 0.99^4 ≈ 3.94%,是原来的 4 倍。
2. 增加路由器负担:分片需要路由器进行额外的计算和处理。

3.3 MSS:TCP 避免 IP 分片的手段

为了避免 IP 分片,TCP 引入了 MSS(最大段大小) 的概念。MSS 是 TCP 报文段中数据部分的最大长度,计算公式为:

MSS = MTU - IP首部长度 - TCP首部长度

在标准以太网中,MTU=1500,IP 首部 = 20 字节,TCP 首部 = 20 字节,因此MSS=1460 字节。TCP 会将应用层数据分割成不超过 MSS 大小的段,这样 IP 层就不需要再进行分片了。

这也解释了为什么 TCP 的滑动窗口是一段段发送的,而不是一次性发送所有数据 —— 核心目的就是为了避免 IP 分片,降低丢包率。

3.4 IP 首部中的分片相关字段

IP 首部中有三个字段专门用于分片和组装:

字段长度作用16 位标识(ID)16 位唯一标识一个 IP 数据报。同一个数据报的所有分片具有相同的 ID。3 位标志(Flags)3 位第 1 位保留;第 2 位 DF=1 表示禁止分片;第 3 位 MF=1 表示还有更多分片,MF=0 表示最后一个分片。13 位片偏移(Fragment Offset)13 位表示当前分片的数据在原始数据报数据区中的偏移量,以 8 字节为单位

3.5 分片的具体过程

示例:假设 IP 层有一个大小为 3000 字节的报文(包含 20 字节 IP 首部和 2980 字节有效载荷),MTU=1500 字节,如何分片?

1. 计算分片数量
每个分片的最大有效载荷 = MTU - IP 首部 = 1500 - 20 = 1480 字节
2. 2980 字节有效载荷需要分成 3 片:1480 + 1480 + 20 = 2980 字节

每个分片的字段设置

1. 分片 1

  • 总长度:20 + 1480 = 1500 字节
  • 标识:111(与原始报文相同)
  • 标志位:MF=1(还有更多分片)
  • 片偏移:0(0×8=0 字节)
2. 分片 2
  • 总长度:20 + 1480 = 1500 字节
  • 标识:111
  • 标志位:MF=1
  • 片偏移:185(185×8=1480 字节)
3. 分片 3
  • 总长度:20 + 20 = 40 字节
  • 标识:111
  • 标志位:MF=0(最后一个分片)
  • 片偏移:370(370×8=2960 字节)

3.6 组装的具体过程

目的主机的 IP 层接收到分片后,按照以下步骤进行组装:

1. 识别分片:如果MF=1或者MF=0且片偏移>0,则该报文是一个分片。
2. 聚合分片:根据标识字段将属于同一个数据报的所有分片聚合在一起。
3. 检查完整性
务必收到片偏移为 0 的分片(第一片)
4. 务必收到 MF=0 的分片(最后一片)
5. 所有分片的片偏移务必连续(前一个分片的片偏移 + 数据长度 / 8 = 下一个分片的片偏移)

组装数据报:按照片偏移的顺序将所有分片的数据部分拼接起来,恢复成原始的 IP 数据报。传递给上层:将组装好的 IP 数据报传递给传输层处理。

3.7 分片内存对齐:协议设计的天才之处

很多人会有一个疑问:IP 首部的16 位总长度字段决定了 IP 报文最大可以达到2^16=65535字节,但片偏移只有 13 位,怎么可能完整表达最大 65535 字节报文的所有偏移量?这看似是协议的漏洞,实则是设计者精心设计的内存对齐机制,用最少的比特位实现了最大的表达能力。

3.7.1 协议解耦:IP 与底层链路无关

首先要明确一个核心设计原则:IP 协议要与底层数据链路层解耦

  • IP 协议本身规定最大报文长度为 65535 字节,这是由 16 位总长度字段决定的,与底层链路无关
  • 以太网的 MTU=1500 字节只是当前最常用的链路限制,如果未来换成无线 LAN、光纤等其他链路,MTU 可能会发生变化
  • IP 协议不应该因为底层链路的变化而修改自身的核心设计,这就是 “各管各的” 解耦思想
类比:高速路限速 120 码,但汽车的设计最高时速可以达到 200 码。你可以根据限速调整车速,但不能要求所有汽车都只能造到 120 码的最高时速。

3.7.2 8 字节对齐:用 13 位表达 16 位偏移

为了解决 13 位片偏移无法覆盖 16 位总长度的问题,协议设计者引入了8 字节对齐规则

除最后一个分片外,所有分片的有效载荷长度必须是 8 的整数倍。

这个规则的本质是:所有非最后分片的片偏移值乘以 8 后,低 3 位一定都是 0。既然低 3 位永远是 0,那就不需要在片偏移字段中存储它们,只需要存储高 13 位即可。

  • 存储时:将实际偏移量除以 8(右移 3 位),只存高 13 位
  • 读取时:将片偏移值乘以 8(左移 3 位),自动补上低 3 位的 0
这样一来,13 位片偏移最大可以表示2^13 * 8 = 65536字节的偏移量,刚好覆盖 IP 报文的最大长度 65535 字节。用 13 位就实现了原本需要 16 位才能做好的功能,这种极致的优化堪称协议设计的典范。

3.7.3 最后一个分片的特殊处理

为什么最后一个分片不需要遵守 8 字节对齐规则?

  • 最后一个分片后面没有其他分片,不需要通过片偏移来定位下一个分片的起始位置
  • 即使最后一个分片的长度不是 8 的整数倍,也不会影响整个报文的组装
  • 例如我们之前的 3000 字节示例:最后一个分片只有 20 字节有效载荷,不是 8 的整数倍,但完全不影响组装
补充:结合之前的 3 位标志位,我们只用了3位标志+13位片偏移共 16 个比特位,就同时实现了 “区分是否分片”、“标识分片位置”、“判断是否是最后一个分片” 三个核心功能,这也是为什么说 IP 分片的字段设计是天才之作。

四. Linux 内核 IP 分片与组装源码解读

Linux 内核中 IP 分片和组装的核心函数位于net/ipv4/ip_output.cnet/ipv4/ip_input.c中。

4.1 IP 分片函数:ip_fragment ()

int ip_fragment(struct sock *sk, struct sk_buff *skb,
                int (*output)(struct sock *, struct sk_buff *))
{
    struct iphdr *iph = ip_hdr(skb);
    int mtu = dst_mtu(skb_dst(skb));
    int hlen = iph->ihl * 4;
    int len = skb->len - hlen;  // 有效载荷长度
    int ptr = 0;
    struct sk_buff *skb2;

    // 检查是否禁止分片
    if (iph->frag_off & htons(IP_DF)) {
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
        kfree_skb(skb);
        return -EMSGSIZE;
    }

    // 循环分片
    while (ptr < len) {
        int frag_size = min(mtu - hlen, len - ptr);

        // 除了最后一个分片,其他分片的长度必须是8的整数倍
        if (frag_size < len - ptr)
            frag_size &= ~7;

        // 分配新的skb
        skb2 = alloc_skb(frag_size + hlen + LL_MAX_HEADER, GFP_ATOMIC);
        if (!skb2) {
            kfree_skb(skb);
            return -ENOMEM;
        }

        // 复制IP首部
        skb_reserve(skb2, LL_MAX_HEADER);
        skb_put(skb2, hlen + frag_size);
        skb_copy_to_linear_data(skb2, iph, hlen);

        // 复制数据部分
        skb_copy_bits(skb, hlen + ptr, skb2->data + hlen, frag_size);

        // 设置分片字段
        iph2 = ip_hdr(skb2);
        iph2->frag_off = htons((ptr / 8) | (ptr + frag_size < len ? IP_MF : 0));
        iph2->tot_len = htons(skb2->len);
        ip_send_check(iph2);

        // 发送分片
        output(sk, skb2);
        ptr += frag_size;
    }

    kfree_skb(skb);
    return 0;
}

核心逻辑解读

1. 首先检查 DF 标志位,如果禁止分片,则发送 ICMP"需要分片但 DF 置位" 错误
2. 计算每个分片的大小,确保除最后一个分片外,其他分片的长度是 8 的整数倍
3. 为每个分片分配新的 skb,复制 IP 首部和对应的数据部分
4. 设置片偏移和 MF 标志位,重新计算 IP 首部校验和
5. 调用输出函数发送每个分片

4.2 IP 组装函数:ip_defrag ()

struct sk_buff *ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
    struct iphdr *iph = ip_hdr(skb);
    struct ipq *qp;
    int err;

    // 查找或创建对应的IP队列
    qp = ip_find(net, iph->id, iph->saddr, iph->daddr, iph->protocol, user);
    if (!qp) {
        kfree_skb(skb);
        return ERR_PTR(-ENOMEM);
    }

    // 将分片加入队列
    err = ip_frag_queue(qp, skb);
    if (err) {
        ipq_put(qp);
        return ERR_PTR(err);
    }

    // 检查是否所有分片都已到达
    if (ip_frag_complete(qp)) {
        struct sk_buff *ret = ip_frag_reasm(qp);
        ipq_put(qp);
        return ret;
    }

    ipq_put(qp);
    return ERR_PTR(-EINPROGRESS);
}

核心逻辑解读

1. 根据 IP 标识、源 IP、目的 IP 和协议查找对应的分片队列
2. 如果队列不存在,则创建一个新的队列
3. 将当前分片加入队列,并按照片偏移排序
4. 检查是否所有分片都已到达且完整
5. 如果完整,则调用ip_frag_reasm()组装成完整的 IP 数据报并返回

核心考点总结:

1. 公网与 NAT
私有 IP 地址段:10.0.0.0/8172.16.0.0/12192.168.0.0/16
2. NAT 技术通过地址转换实现多个主机共享一个公网 IP

路由转发

1. 路由转发的核心逻辑:最长前缀匹配
2. 路由表关键字段:目的网络、子网掩码、下一跳、发送接口
3. 缺省路由用于处理所有不匹配的目的地址

IP 分片与组装

1. MTU=1500 字节,MSS=1460 字节(标准以太网)
2. 分片相关字段:16 位标识、3 位标志(DF/MF)、13 位片偏移(以 8 字节为单位)
3. 分片会大幅增加丢包率,TCP 通过 MSS 机制避免 IP 分片
4. 接收方根据标识字段聚合分片,根据片偏移排序并检查完整性

网络层是整个 TCP/IP 协议栈的 “导航系统”,它为每一个数据包指明了前进的方向。搞懂了网络层的工作原理,你就掌握了互联网通信的核心逻辑。


结尾

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

** 结语:IP 协议虽然已经诞生了 40 多年,但它依然是互联网的基石。从最初的 IPv4 到现在的 IPv6,虽然地址长度从 32 位扩展到了 128 位,但路由和分片的核心思想并没有改变。希望这篇文章能够帮助你彻底搞懂网络层 IP 协议的工作原理。

✨把这些内容吃透超牛的!放松下吧✨
>
ʕ˘ᴥ˘ʔ
>
づきらど

就写这么多吧,内容比较基础,适合入门回顾。有补充的地方欢迎留言一起完善。

评论 (0)

暂无评论