Skip to content

第1章:网络分层模型


元数据卡

维度
难度
前置任意语言基础、基本系统编程概念
关键词分层、封装、OSI、TCP/IP、协议栈
代码语言Python (主) / Java (差异窗口)

你在哪

"你从法师塔的地窖爬出来,站在一片魔雾笼罩的原野上。法术卷轴村、咒语森林、魔力核心城——这些世界都在这片荒野上。但它们各不相通。你需要一条路——魔法驿道,就是连接各个世界的通信管线。"

你一直待在你的法师塔里。你写过咒语调用的法术参数传递,知道魔力怎么在法术回响空间和核心法阵之间搬运,理解魔法卷轴怎么把法术能量块抽象成流动的魔力——这些都是法师塔的生活。你脚下的法阵核心是一座孤立城市,所有法术能量都在城墙内流转。

现在该出城了。

法术能量要从你的法师手杖出发,穿过护城法阵,钻出信标塔尖,跳上一道魔力光束或法术光纤,最终抵达另一座法师塔的法阵核心,再回到它的施法者。问题是:这条驿道根本不存在。两座法师塔之间没有法术同步,没有共享法阵能量,没有共同的法术共振频率,只有一根魔导缆线。

你没法直接 send(源塔坐标, 目标塔坐标, 法术信函) —— 那是神灵视角。现实是:你必须自己修建驿道、架设信标塔、点亮传送阵,一层一层把法术信函送出城。

这就是分层要做的事。

本章分层

  • 必读:分层模型的直觉(为什么分层、每层做什么)、封装与解封装过程、用浏览器请求串联四层的故事
  • 选读:OSI 七层 vs TCP/IP 四层的差异、Socket 类型与分层的关系
  • 深水区:手动构造协议帧(ProtocolStack 类)

本章不会要求你掌握

  • Scapy 抓包分析
  • Raw socket 编程
  • 协议头的每比特细节

你的任务

理解为什么网络非要分成好几层不可,每一层干什么、相互怎么样的关系。然后建立一个心智模型:当你在浏览器里敲下一个 URL,数据是怎么踩过整整四层地板,走出网卡的。

你不需要记住每个协议的每个字段。你需要的是:当问题出现时,知道该去找哪一层。

遭遇战:写一个不用分层的聊天程序

假设你有一根网线直连两台机器的串口。你想让 A 机往 B 机发一句话。

python
# 第零层:你们以为的通信
def send_via_serial(data: str, port: str):
    # 打开串口,字节逐个发送
    with open(port, 'wb') as f:
        f.write(data.encode('utf-8'))

def recv_via_serial(port: str) -> str:
    with open(port, 'rb') as f:
        return f.read().decode('utf-8')

以上。发出去的字符串在另一端被原样收到。这不就是通信吗?

但你很快遇到问题:

  1. 数据损坏了怎么办? —— 电缆被踩了一脚,某个 bit 翻转了。你收到的 "hello" 变成了 "hallo"。你不知道。
  2. 发太快对方来不及读? —— 串口缓冲满了,丢掉新数据。你不知道。
  3. 两台机器 IP 不一样怎么路由? —— 串口直连没有寻址,一根线只有一个对端。
  4. 程序崩溃重启后数据怎么恢复? —— 没有会话状态。
  5. 中文字符编码各不同怎么办? —— 一个用 GBK,一个用 UTF-8,发出去的 "你好" 变成乱码。
  6. 一堆程序都等着读串口,谁来管理? —— 你没做多路复用。

如果硬把这些问题全塞到一个程序里,你的代码会在两周内变成一团乱麻。

当前状态检查

问题你的程序有处理吗?
bit 错误检测
丢包/重传
流量控制
多路寻址
会话管理
应用数据格式硬编码

获得技能:分层——分而治之

核心认知:没人能把所有这些问题一次性解决。我们做的是分层次协商

分层不是技术方案——它是社会契约。每一层只和一个层面的问题打交道,向上一层提供服务,向下一层露出标准接口。任何一层都可以被替换,只要协议不变。

记忆锚点:分层 = 职责分离。下游不用懂上游数据含义,上游不用管下游物理介质。


常见陷阱:OSI 七层 vs TCP/IP 四层

OSI 七层模型(教科书常用)

名称任务比喻
7应用层给用户看的,HTTP/FTP/SMTP法术信函上的咒语名
6表示层编码/加密/压缩给信函加上封印咒
5会话层建立/维持/结束会话谁先施法、谁等待回音
4传输层端到端可靠/不可靠传输法术信鸽跟踪咒
3网络层路由和寻址信函该走哪座信标塔
2数据链路层相邻信标间的帧传输从一座驿站到下一座驿站
1物理层比特流在介质上传输魔力脉冲在魔导缆线中跑

现实世界:TCP/IP 四层模型

实际互联网用的不是 OSI(那是 ISO 标准组织做的,概念清晰但应用太少)。现实是 TCP/IP 的:

┌──────────────────────────────────┐
│  4  应用层 (Application)          │  ← HTTP, DNS, SSH, TLS
│  3  传输层 (Transport)            │  ← TCP, UDP
│  2  网络层 (Internet)              │  ← IP, ICMP, ARP
│  1  网络接口层 (Network Access)    │  ← 以太网、Wi-Fi
└──────────────────────────────────┘

你看,OSI 的 5/6 层在 TCP/IP 里被合到应用层了——现实中没那么多协议需要独立表示层。

Python 里证明这个分层最直观的方式是 socket 类型

python
import socket

# 传输层:TCP 流 → socket(AF_INET, SOCK_STREAM)
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 传输层:UDP 数据报 → socket(AF_INET, SOCK_DGRAM)
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 网络层:原始 IP 包 → socket(AF_INET, SOCK_RAW, proto)
# (需要 root 权限,且 macOS/Windows 上有限制)
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)

你只需要改一个 SOCK_STREAMSOCK_DGRAM,传输层行为就彻底变了。应用层代码不需要知道底层走的是以太网还是 Wi-Fi——这是分层给你带来的抽象红利

💡 核心操作:封装与解封装

数据从上层往下走时,每一层加一个头

应用层:  [HTTP HEADER | HTTP BODY]                           ← HTTP 请求
传输层:  [TCP HEADER | HTTP HEADER | HTTP BODY]              ← 加端口号、序号
网络层:  [IP HEADER | TCP HEADER | HTTP HEADER | HTTP BODY]  ← 加源/目标 IP
链路层:  [ETH HEADER | IP HEADER | TCP HEADER | HTTP BODY | ETH TRAILER]
         ↑                                       ↑
         48-bit MAC 地址                          CRC 校验尾

Python 里你可以手动模拟这个过程——写一个简单的协议封装函数

python
import struct
import random
import hashlib

class ProtocolStack:
    """玩具协议栈:演示封装过程"""

    def __init__(self, mac: str, ip: str):
        self.mac = mac
        self.ip = ip
        self.seq = random.randint(1000, 9999)

    def _eth_pack(self, payload: bytes, dst_mac: str) -> bytes:
        """第2层:以太网帧封装"""
        ethertype = b'\x08\x00'  # IPv4
        header = bytes.fromhex(dst_mac.replace(':', '')) + \
                 bytes.fromhex(self.mac.replace(':', '')) + \
                 ethertype
        frame = header + payload
        # CRC32 尾部
        crc = struct.pack('>I', zlib.crc32(frame) & 0xFFFFFFFF)
        return frame + crc

    def _ip_pack(self, payload: bytes, dst_ip: str) -> bytes:
        """第3层:IP 包封装"""
        ver_ihl = 0x45  # IPv4, header len=20
        tos = 0
        total_len = 20 + len(payload)
        ident = random.randint(0, 65535)
        flags_frag = 0
        ttl = 64
        protocol = 6  # TCP
        # checksum 先填零
        header = struct.pack('!BBHHHBBH', ver_ihl, tos, total_len,
                             ident, flags_frag, ttl, protocol, 0)
        header += bytes(int(x) for x in self.ip.split('.'))
        header += bytes(int(x) for x in dst_ip.split('.'))
        # 计算 IP 头校验和
        checksum = self._checksum(header)
        header = header[:10] + struct.pack('!H', checksum) + header[12:]
        return header + payload

    @staticmethod
    def _checksum(data: bytes) -> int:
        if len(data) % 2 == 1:
            data += b'\x00'
        s = sum(struct.unpack('!%dH' % (len(data)//2), data))
        s = (s >> 16) + (s & 0xFFFF)
        return (~s) & 0xFFFF

    def tcp_pack(self, data: bytes, dst_ip: str, dst_port: int) -> bytes:
        """第4层:TCP 段封装"""
        src_port = random.randint(49152, 65535)
        seq_num = self.seq
        self.seq += len(data)
        ack_num = 0
        offset_res = 0x50  # 20字节头
        flags = 0x018  # PSH + ACK
        window = 65535
        # TCP 校验和需要伪头部
        pseudo = self._pseudo_header(dst_ip, 6, 20 + len(data))
        tcp_header = struct.pack('!HHIIBBHHH', src_port, dst_port,
                                 seq_num, ack_num, offset_res, flags,
                                 window, 0, 0)
        checksum = self._checksum(pseudo + tcp_header + data)
        tcp_header = tcp_header[:16] + struct.pack('!H', checksum) + tcp_header[18:]
        # 返回完整 TCP 段
        return tcp_header + data

    def _pseudo_header(self, dst_ip: str, proto: int, tcp_len: int):
        src = bytes(int(x) for x in self.ip.split('.'))
        dst = bytes(int(x) for x in dst_ip.split('.'))
        return src + dst + struct.pack('!BBH', 0, proto, tcp_len)

    def send_http(self, body: str, dst_mac: str, dst_ip: str, dst_port: int):
        """完整封装过程:HTTP → TCP → IP → 以太网"""
        print(f"[应用层] HTTP 数据: {body}")
        tcp_seg = self.tcp_pack(body.encode(), dst_ip, dst_port)
        print(f"[传输层] TCP 段: {len(tcp_seg)} 字节")
        ip_pkt = self._ip_pack(tcp_seg, dst_ip)
        print(f"[网络层] IP 包: {len(ip_pkt)} 字节")
        eth_frame = self._eth_pack(ip_pkt, dst_mac)
        print(f"[链路层] 以太帧: {len(eth_frame)} 字节(含 CRC)")
        return eth_frame

# 看看一个 HTTP GET 请求走了多少层
import zlib

stack = ProtocolStack("00:1a:2b:3c:4d:5e", "192.168.1.10")
frame = stack.send_http(
    body="GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n",
    dst_mac="aa:bb:cc:dd:ee:ff",
    dst_ip="93.184.216.34",
    dst_port=80
)

预期输出

[应用层] HTTP 数据: GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n
[传输层] TCP 段: 40 字节
[网络层] IP 包: 60 字节
[链路层] 以太帧: 86 字节(含 CRC)

差异窗口:Java 的封装抽象

Java 的分层抽象更显性——它用 java.net 包层级暗示协议分层:

java
// 应用层:HTTP URL
URL url = new URL("http://example.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

// 传输层:TCP Socket(URL 底层自动创建)
Socket sock = new Socket("example.com", 80);

// 更底层的 Socket 选项
sock.setSoTimeout(5000);       // 传输层超时
sock.setTcpNoDelay(true);      // 禁用 Nagle 算法(传输层优化)

关键差异:Java 把物理层/链路层的差异隐藏在 SocketImpl 工厂里,你在应用层根本看不到 MAC 地址。Python 的 socket 模块更底层——你能选择 AF_INET vs AF_INET6 vs AF_PACKET,相当于你在选择走 IPv4 还是 IPv6 的路。


通关挑战

实验附录:用 scapy 抓包观察分层

权限说明:以下操作需要 root/管理员权限(因为 raw socket 访问需要特权级)。在 macOS 上,Scapy 可能需要额外配置 BPF。在 Windows 上,建议使用 Wireshark 或 WSL 内运行。如果你当前环境无法满足,跳过本章安全无妨——理解分层模型才是主线。

bash
pip install scapy  # 安装后可能需要 sudo 运行

写一个脚本抓取本机 curl http://example.com 的包,打印每一层的协议头部摘要:

python
# 提示
from scapy.all import sniff, Ether, IP, TCP, Raw

def packet_analysis(pkt):
    if Ether in pkt:
        print(f"[L2] 源 MAC: {pkt[Ether].src} → 目标 MAC: {pkt[Ether].dst}")
        if IP in pkt:
            print(f"[L3] {pkt[IP].src}{pkt[IP].dst} (TTL={pkt[IP].ttl})")
            if TCP in pkt:
                print(f"[L4] TCP {pkt[TCP].sport}{pkt[TCP].dport} "
                      f"[SYN={pkt[TCP].flags & 0x02 > 0}, ACK={pkt[TCP].flags & 0x10 > 0}]")
                if Raw in pkt:
                    print(f"[L7] 数据: {pkt[Raw].load[:80]}")

# 拦截一次 curl(需要 root)
sniff(filter="host example.com", count=3, prn=packet_analysis)

挑战 2:分层故障排查

你的直播间用户反馈"看视频卡"。已知:

  • Wi-Fi 信号满格(物理层 OK)
  • ping 8.8.8.8 正常(网络层 OK)
  • DNS 能解析域名(应用层部分 OK)
  • 视频流速率远低于标称带宽

问题:问题在哪一层?怎么验证?

(提示:检查 UDP 丢包率 → 检查 TCP 窗口缩放 → 检查路由器 QoS 策略)

挑战 3:协议栈深度对比

tcpdump -X 或 Wireshark 抓一个包,找到:

  1. 以太网帧头部的 MAC 地址
  2. IP 头的 TTL、总长度、校验和
  3. TCP 头的源端口、序号、确认号、窗口大小

验收标准

阅读本章后,你能回答以下问题:

  • [ ] 为什么需要分层而不是一个单体通信程序?
  • [ ] OSI 七层的哪三层在 TCP/IP 模型里被合并了?
  • [ ] "封装"是什么意思?数据从应用层到底层,每层分别加什么?
  • [ ] 你的浏览器发出的请求,在第四层走的是哪个协议?第三层呢?
  • [ ] 如果遇到"视频卡顿",你知道该怀疑哪层的哪个组件吗?

常见卡点

卡点澄清
"封装就是套娃,太浪费"每个头也就 20-60 字节,相比 MTU 1500 字节是 ~4% 开销,值
"TCP/IP 和 OSI 哪个正确?"都正确,抽象层次不同。OSI 更细,TCP/IP 是实际标准
"应用层为什么没有"协议头"?"有——HTTP 头本身就是 L7 头,只是我们不叫它"头"而已
"分层是软件架构,跟硬件没关系"有关系——以太网卡固件实现了 L2 的帧校验和寻址,这叫 L2 offloading

现在不需要理解

  • TCP 三次握手的序号同步细节 → 下章会讲
  • IP 分片与重组策略 → Vol 5 安全章节会涉及
  • ARP、ICMP 等辅助协议 → 遇到具体场景再展开
  • VLAN、MPLS 等高级封装 → 那是数据中心网络的事
  • MTU 发现与路径 MTU → 调优阶段再深入

旅人笔记

数据走出城门的方式,从来不是一次跳跃,而是一级一级脱掉鞋套、换上头盔、钻进车厢、挂上车牌。每一层都在说同一句话:"上面的事我不懂,下面的事我不催,我只管把这段路走完。"

我在实验室用短接网线直连两台笔记本,第一次看到 tcpdump 里 IP 头的 TTL=64 时,有一种奇妙的熟悉感——这跟内核 task_struct 里的 time_slice 太像了。计算机的设计者一直在用同一套思维模式:给每件事一个边界,让它在边界内死掉或重试。 分层就是这些边界的具象化。

记住:分层不是完美方案,它是可工作的方案。网络工程师真正的工作,是知道哪一层在撒谎,然后去那一层抓包。


→ 下一章预告

你现在知道数据出了城、怎么打包了。问题是:数据到了另一端,我怎么等它、收它? 下一章走进 Socket 编程世界——你会在 Python 里亲手建立一条从端到端的连接通道。

Built with VitePress | Software Systems Atlas