第一阶段:源主机内部的封装与决策
数据只要还在你的电脑里,它主要发生两件事:封装 和 决策
1. 应用层
一切始于代码。
- 代码行为: 你创建了一个 Socket,并执行了
send(sockfd, "Hello", 5, 0);。 - 数据形态: 此时数据仅仅是内存里的 5 个字节:
H-e-l-l-o。 - 深层动作 (System Call):
-
- 应用层位于用户态。
- 当你调用
send时,发生了一次系统调用 。CPU 从用户态切换到内核态。 - 操作系统内核会将这 5 个字节从你的程序内存缓冲区,拷贝 到内核网络协议栈的发送缓冲区中。
- 注意: 此时还没有任何网络报头,只是纯粹的有效载荷 (Payload) 。
2. 传输层
数据进入内核后,首先到达传输层(假设使用 TCP 协议)。
- 核心任务: 识别"是谁发的"以及"发给谁(哪个进程)"。
- 封装动作: 内核在
"Hello"前面加了一个 TCP 报头 (TCP Header)。
-
- 关键字段:
-
-
- 源端口 (Source Port):
54321(系统随机分配给你的客户端程序的)。 - 目的端口 (Destination Port):
80(你在代码里指定的服务器端口)。 - 序列号 (Sequence Number): 用于保证数据可靠到达,不丢包、不乱序。
- 源端口 (Source Port):
-
数据形态变化: 此时这坨数据被称为 TCP 段 (Segment)。
[TCP报头 | Hello]
3. 网络层
TCP 段继续向下传递,来到 IP 层。这里发生了极其重要的一步,很多人容易忽略。
- 核心任务: 确定"终点"在哪里,并决定"第一步"往哪走。
- 封装动作: 内核在 TCP 段前面加了一个 IP 报头 (IP Header)。
-
- 关键字段:
-
-
- 源 IP (Src IP):
192.168.1.5(主机A自己)。 - 目的 IP (Dst IP):
1.2.3.4(主机B)。 - 协议号 (Protocol): 6
- 源 IP (Src IP):
-
数据形态变化: 此时数据被称为 IP 数据报 (Packet/Datagram)。
[IP报头 | TCP报头 | Hello]
- 🔴 核心讲解:路由决策 (Routing Decision) 在封装完 IP 头之后,主机A必须思考一个问题:"我要去的主机B (1.2.3.4),是在我的局域网里,还是在外面?"
主机A会查自己的路由表 (Routing Table)(没错,普通电脑里也有路由表):
-
- 主机A 用自己的子网掩码 (如
255.255.255.0) 计算1.2.3.4。 - 判断: 发现目标网络
1.2.3.0不等于自己的网络192.168.1.0。 - 结论: 目标在外网。
- 决策: 既然在外网,我不能直接发给它,我必须发给我的默认网关 (Gateway) ,也就是路由器
192.168.1.1。
- 主机A 用自己的子网掩码 (如
关键点: 此时,IP 层的"下一跳 " IP 地址被确定为 192.168.1.1。
4. 数据链路层
数据包准备离开网卡,进入网线。这时需要加上 MAC 地址。
-
核心任务: 解决"下一跳 IP 对应的硬件是谁"的问题。
-
遇到的问题: IP 层告诉链路层:"把这个包送给下一跳
192.168.1.1" -
但是以太网卡听不懂 IP,它只认 MAC 地址 。 此时,需要查 ARP 缓存表。
-
🔴 核心讲解:ARP 解析过程 主机A 检查自己的 ARP 表:
-
- 场景一(表里有): 找到了
192.168.1.1对应的 MAC 是RR:RR:RR:RR:RR:RR。直接使用。 - 场景二(表里没有):
- 场景一(表里有): 找到了
-
-
- 主机A 暂停发送数据。
- 主机A 发送一个 ARP 请求 (Broadcast) :"谁是
192.168.1.1?请告诉我你的 MAC!" - 局域网内的路由器收到后,回复 ARP 应答 :"我是
192.168.1.1,我的 MAC 是RR:RR:RR:RR:RR:RR"。 - 主机A 将这对映射写入 ARP 表。
-
- 封装动作: 拿到 MAC 地址后,内核在 IP 包前后加上 以太网帧头 (Ethernet Header) 和 帧尾 (FCS)。
-
- 关键字段:
-
-
- 目的 MAC (Dst MAC):
RR:RR:RR:RR:RR:RR - 源 MAC (Src MAC):
AA:AA:AA:AA:AA:AA(主机A自己)。 - 帧类型:
0x0800(代表里面装的是 IPv4 数据)。
- 目的 MAC (Dst MAC):
-
数据形态变化: 此时数据被称为 以太网帧 (Ethernet Frame)。
[以太网头(Dst:Router, Src:HostA) | IP头(Dst:HostB) | TCP头 | Hello | 帧尾]
5. 物理层
- 动作: 网卡 (NIC) 驱动程序将这一长串二进制数字(帧),通过数模转换,变成网线上的电压高低变化(电信号)或者光纤里的光亮灭(光信号)。
- 结果: 信号顺着网线,冲向了路由器。
第二阶段:路由器内部的中转与重封装
1. 物理接收与链路层校验
当电信号到达路由器的网卡接口时,网卡芯片会首先还原出二进制数据。
- 身份核对(MAC 过滤):
路由器网卡检查帧头里的 目的 MAC 地址。
-
- 发现写的是
RR:RR:RR:RR:RR:RR(路由器自己的 MAC)。 - 判定: "这是给我的信。"
- 动作: 接收 (Accept) 并触发中断,通知 CPU 处理。如果是发给别人的(MAC 不匹配),直接丢弃。
- 发现写的是
- 拆包 (Decapsulation) ------ 第一次手术:
路由器将以太网的 帧头 和 帧尾 全部剥离/撕掉 1。
-
- 结果: 此时,原本的 MAC 地址信息(主机A 和 路由器入接口的 MAC)彻底消失了。
- 保留: 剩下的部分是 IP 数据报(包含 IP 头 + TCP 头 + Data)。
2. 网络层路由决策
现在,路由器手里拿着这个裸露的 IP 数据报。它需要决定下一步怎么走。
- 读取 IP 头:路由器提取出 目的 IP 地址:1.2.3.4。
- 查路由表 :路由器搜索自己的路由表,寻找匹配 1.2.3.4 的条目。
-
- 路由表逻辑: "要去
1.2.3.4,请走WAN接口(出口),下一跳网关是 ISP(运营商)的路由器202.10.1.1。"
- 路由表逻辑: "要去
3. 构建新的链路层封装
路由器已经决定把包从 WAN 口扔出去,发给 ISP 路由器(IP: 202.10.1.1)。
但是,WAN 口连接的是另一段物理链路(可能是光纤或另一根网线),数据要发出去,必须再次封装成 帧。这就需要新的 MAC 地址。
- 查 ARP 表 (ARP Cache Lookup):
路由器查询自己的 ARP 表(针对 WAN 口的):
-
- 问: "下一跳
202.10.1.1的 MAC 是什么?" - 答: "是 ISP 路由器的 MAC
II:II:II:II:II:II。" - (如果没查到,路由器会在 WAN 口发起 ARP 请求,就像第一阶段主机A做的那样)。
- 问: "下一跳
- 重新封装 (Re-encapsulation):
路由器给 IP 数据报穿上一件全新的"衣服"(新的帧头):
-
- 新的 源 MAC (Src MAC):
Router_WAN_MAC(路由器出口的身份证)。 - 新的 目的 MAC (Dst MAC):
ISP_Router_MAC(下一跳设备的身份证)。
- 新的 源 MAC (Src MAC):
注意: 此时的 MAC 地址已经完全变了!旧的(主机A -> 路由器内网口)已经被扔掉了。
4. 物理发送
路由器将这个崭新的帧,推送到 WAN 口的发送队列,转换成信号发往互联网。
总结:数据包的"变"与"不变"
这是理解网络传输最关键的对照表,请务必仔细对比:
|------------|------------------|--------------------|-------------------------------------|---------------|
| 字段 | 在主机A发出时 | 在路由器转发出去时 | 变化情况 | 原因 |
| 源 IP | 192.168.1.5 | 192.168.1.5 | 不变 | 我是谁(发件人)不能变 3 |
| 目的 IP | 1.2.3.4 | 1.2.3.4 | 不变 | 我去哪(收件人)不能变 4 |
| 源 MAC | HostA_MAC | Router_WAN_MAC | <font color="red">变了</font> | 这一程的起点变了 5 |
| 目的 MAC | Router_LAN_MAC | ISP_Router_MAC | <font color="red">变了</font> | 这一程的终点变了 6 |
| TTL | 64 (假设) | 63 | 变了 | 每过一关脱一层皮 |
第四阶段:从"目标路由器"到"目标主机"
1. 路由器接收与查表
路由器从 WAN 口收到数据包,拆掉外网的帧头,露出 IP 包。
- 读取目的 IP:
1.2.3.4。 - 查路由表: 路由器发现:
1.2.3.4这个 IP 属于1.2.3.0/24网段。
-
- 关键发现: 路由表显示,
1.2.3.0/24网段是 直连 (Directly Connected) 在我的LAN接口上的。 - 决策: "目标就在我的眼皮子底下(内网里),我不需要再转发给其他路由器了,我要直接交给它!"
- 关键发现: 路由表显示,
2. 关键的地址解析 (ARP again)
路由器现在知道要给 1.2.3.4,但它需要知道 1.2.3.4 的 MAC 地址 才能在局域网里发帧。
- 查 ARP 缓存表 (路由器内部):
-
- 问: "我的内网里,谁是
1.2.3.4?" - 场景 A (表中已有): 找到了!对应 MAC 是
BB:BB:BB:BB:BB:BB。 - 场景 B (表中没有):
- 问: "我的内网里,谁是
-
-
- 路由器在 LAN 口发起 ARP 广播:"Who has 1.2.3.4? Tell 1.2.3.1"
- 局域网里的所有机器都收到广播。
- 主机B 惊醒:"是我!"
- 主机B 回复单播:"我是 1.2.3.4,我的 MAC 是
BB:BB..."。 - 路由器将这条记录写入 ARP 表。
-
3. 最后的封装 (Final Encapsulation)
拿到主机B 的 MAC 后,路由器进行最后一次换头手术。它构建一个新的以太网帧:
- 新的 源 MAC (Src MAC):
GG:GG:GG:GG:GG:GG(路由器的 LAN 口 MAC)。 - 新的 目的 MAC (Dst MAC):
BB:BB:BB:BB:BB:BB(主机B 的 MAC)。 - 有效载荷: 依然是那个原封不动的 IP 数据报(Src: 192.168.1.5, Dst: 1.2.3.4)。
4. 局域网传输 (Switching)
路由器通过 LAN 口将这个帧发送出去。 如果路由器和主机B之间还连着一个 交换机 (Switch):
- 交换机收到帧,看一眼目的 MAC (
BB:BB...)。 - 交换机查自己的 MAC 地址表 ,知道
BB:BB...在哪个物理端口(比如 Port 3)。 - 交换机把帧精准地从 Port 3 弹送给 主机B。
第四阶段:终点站的接收与分用
1. 数据链路层 (Data Link Layer)
数据包(电信号)撞击到主机B的网卡。
- MAC 地址校验: 网卡芯片检查帧头的 目的 MAC。
-
- 这一次,目的 MAC 终于写的是 主机B 自己的 MAC (例如
BB:BB:BB:BB:BB:BB)。 - 动作: 网卡接收数据,产生硬中断,告诉 CPU:"有货到了,快来处理!"
- 这一次,目的 MAC 终于写的是 主机B 自己的 MAC (例如
- 拆包与分发: 操作系统内核(驱动程序)接管数据,剥掉以太网的帧头和帧尾。
-
- 关键问题: 剩下的数据给谁?
- 查看帧头字段: 帧头里有一个
Frame Type(帧类型)字段,写着0x0800(IPv4)。 - 分用决策: 内核知道,要把剩下的 payload 交给 网络层 (IP 协议栈) 处理 。
2. 网络层 ------ 身份确认
现在,IP 协议栈拿到了数据包。
- IP 地址校验: 内核检查 IP 头里的 目的 IP。
-
- 发现写的是
1.2.3.4,正是 本机 IP。 - 判定: "这是给我的,不用再转发了。"
- (注:如果目的 IP 不是本机,且本机没开启路由转发功能,内核会直接丢弃并可能发送 ICMP 报错)。
- 发现写的是
- 拆包与分发: 内核剥掉 IP 报头。
-
- 关键问题: 剩下的数据给 TCP 还是 UDP?
- 查看 IP 头字段: IP 头里有一个
Protocol(协议号)字段,写着6(TCP)。 - 分用决策: 内核知道,要把剩下的 payload 交给 传输层 (TCP 协议栈) 处理 。
3. 传输层 ------ 寻找接收人
这是最关键的一步,对应你关心的 Socket 和 端口 。 TCP 协议栈拿到了 TCP 段。
- 端口映射 (Port Mapping): 内核检查 TCP 头里的 目的端口 (Destination Port) ,发现是
80。 - 查找 Socket 表: 操作系统内核里维护着一张 Open File Table 或者专门的 Socket 哈希表。
-
- 内核查询:"当前有没有哪个进程,正在监听 (Listen) 或者绑定 (Bind) 了
80端口?" - 结果: 找到了!有一个 Web 服务器进程(比如 Nginx 或你自己写的 C++ Server),它的 Socket 对应着端口 80。
- 内核查询:"当前有没有哪个进程,正在监听 (Listen) 或者绑定 (Bind) 了
- 数据放入缓冲区: 内核剥掉 TCP 报头,将剩下的 有效载荷 ("Hello") ,复制到该 Socket 对应的 接收缓冲区 (Receive Buffer) 中 。
-
- 同时,TCP 协议栈会根据 TCP 头里的序列号,回复一个 ACK 确认包 给主机A(表示"我收到了,请放心"),但这属于幕后工作,应用程序不知道。
4. 应用层 ------ 终于见面
数据现在已经在操作系统内核的内存里躺好了,就等应用程序来拿。
- 唤醒进程: 如果你的服务器程序正在调用
recv()或read()并且因为没数据而阻塞(睡眠),此时内核会把它唤醒。 - 数据拷贝:
recv()函数返回,数据从 内核空间 (Kernel Space) 的接收缓冲区,被拷贝到 用户空间 (User Space) 的 buffer 里(比如你代码里的char buf[1024])。 - 处理数据: 你的代码终于打印出了:
"Received: Hello"。