第1章:网络分层模型
元数据卡
维度 值 难度 前置 任意语言基础、基本系统编程概念 关键词 分层、封装、OSI、TCP/IP、协议栈 代码语言 Python (主) / Java (差异窗口)
你在哪
"你从法师塔的地窖爬出来,站在一片魔雾笼罩的原野上。法术卷轴村、咒语森林、魔力核心城——这些世界都在这片荒野上。但它们各不相通。你需要一条路——魔法驿道,就是连接各个世界的通信管线。"
你一直待在你的法师塔里。你写过咒语调用的法术参数传递,知道魔力怎么在法术回响空间和核心法阵之间搬运,理解魔法卷轴怎么把法术能量块抽象成流动的魔力——这些都是法师塔的生活。你脚下的法阵核心是一座孤立城市,所有法术能量都在城墙内流转。
现在该出城了。
法术能量要从你的法师手杖出发,穿过护城法阵,钻出信标塔尖,跳上一道魔力光束或法术光纤,最终抵达另一座法师塔的法阵核心,再回到它的施法者。问题是:这条驿道根本不存在。两座法师塔之间没有法术同步,没有共享法阵能量,没有共同的法术共振频率,只有一根魔导缆线。
你没法直接 send(源塔坐标, 目标塔坐标, 法术信函) —— 那是神灵视角。现实是:你必须自己修建驿道、架设信标塔、点亮传送阵,一层一层把法术信函送出城。
这就是分层要做的事。
本章分层
- 必读:分层模型的直觉(为什么分层、每层做什么)、封装与解封装过程、用浏览器请求串联四层的故事
- 选读:OSI 七层 vs TCP/IP 四层的差异、Socket 类型与分层的关系
- 深水区:手动构造协议帧(ProtocolStack 类)
本章不会要求你掌握
- Scapy 抓包分析
- Raw socket 编程
- 协议头的每比特细节
你的任务
理解为什么网络非要分成好几层不可,每一层干什么、相互怎么样的关系。然后建立一个心智模型:当你在浏览器里敲下一个 URL,数据是怎么踩过整整四层地板,走出网卡的。
你不需要记住每个协议的每个字段。你需要的是:当问题出现时,知道该去找哪一层。
遭遇战:写一个不用分层的聊天程序
假设你有一根网线直连两台机器的串口。你想让 A 机往 B 机发一句话。
# 第零层:你们以为的通信
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')以上。发出去的字符串在另一端被原样收到。这不就是通信吗?
但你很快遇到问题:
- 数据损坏了怎么办? —— 电缆被踩了一脚,某个 bit 翻转了。你收到的 "hello" 变成了 "hallo"。你不知道。
- 发太快对方来不及读? —— 串口缓冲满了,丢掉新数据。你不知道。
- 两台机器 IP 不一样怎么路由? —— 串口直连没有寻址,一根线只有一个对端。
- 程序崩溃重启后数据怎么恢复? —— 没有会话状态。
- 中文字符编码各不同怎么办? —— 一个用 GBK,一个用 UTF-8,发出去的 "你好" 变成乱码。
- 一堆程序都等着读串口,谁来管理? —— 你没做多路复用。
如果硬把这些问题全塞到一个程序里,你的代码会在两周内变成一团乱麻。
当前状态检查:
| 问题 | 你的程序有处理吗? |
|---|---|
| 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 类型:
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_STREAM 到 SOCK_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 里你可以手动模拟这个过程——写一个简单的协议封装函数:
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 包层级暗示协议分层:
// 应用层: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_INETvsAF_INET6vsAF_PACKET,相当于你在选择走 IPv4 还是 IPv6 的路。
通关挑战
实验附录:用 scapy 抓包观察分层
权限说明:以下操作需要 root/管理员权限(因为 raw socket 访问需要特权级)。在 macOS 上,Scapy 可能需要额外配置 BPF。在 Windows 上,建议使用 Wireshark 或 WSL 内运行。如果你当前环境无法满足,跳过本章安全无妨——理解分层模型才是主线。
pip install scapy # 安装后可能需要 sudo 运行写一个脚本抓取本机 curl http://example.com 的包,打印每一层的协议头部摘要:
# 提示
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 抓一个包,找到:
- 以太网帧头部的 MAC 地址
- IP 头的 TTL、总长度、校验和
- 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 里亲手建立一条从端到端的连接通道。