9.1 ISO 七层模型
1. ISO 七层模型与其历史渊源
1.1 七层模型的诞生与现实权衡
ISO 七层模型,全称为开放系统互连参考模型(Open Systems Interconnection, OSI),是国际标准化组织(ISO)在 20 世纪七八十年代主持制定的网络通信架构参考体系。彼时计算机网络正处于野蛮生长的早期阶段,不同大型机与小型机厂商(如 IBM 的 SNA、DEC 的 DECnet)各自推行专有的通信协议与物理接口,异构网络之间宛如孤岛,设备之间难以无缝互通。为了打破厂商锁定的藩篱,ISO 试图自顶向下设计出一套普适、严密的通用模型,将网络传输过程中涉及的各阶段处理正交分解为七个相互独立的层次。
然而在工业界的真实演进中,发端于 ARPANET 实验网的 TCP/IP 协议族凭借其实用主义哲学、开放的代码实现以及与早期 Unix 系统的紧密整合,以雷霆之势迅速统治了全球互联网。尽管严苛遵循 OSI 七层定义的协议(如 CLNP、X.400)在市场上大多退居边缘,但 OSI 模型关于层次解耦的核心智慧却被完整保留了下来。时至今日,工程师依然普遍采用七层模型中的术语作为共同的技术语言:例如提及工作在“第二层”的以太网交换机、“第三层”的 IP 路由器,或是工作在“第四层”的四层负载均衡器与“第七层”的应用网关。
1.2 层次解耦与工程优势
分层架构的核心价值在于确立了严格的职责边界与契约隔离。在理想的分层拓扑中,每一层只需专注于处理本层的控制逻辑,并通过明确约定的接口调用直接下层提供的服务。只要层间接口保持稳定,某一层内部的具体实现、硬件形态乃至传输算法都可以自由替换而不会引发全系统的级联震荡。最典型的例证是 TCP/IP 协议栈:同一套上层应用与传输控制机制,既能平稳运行在有线以太网的铜缆与光纤之上,也能直接嫁接到 Wi-Fi 的射频电磁波或 5G 移动蜂窝网络中。
2. 层次划分与核心职责
OSI 模型从底层的物理介质逐步上升到面向用户的业务逻辑,自下而上划分为七个逻辑层级:
物理层(Physical Layer)
物理层负责在物理信道上传输非结构化的原始比特流。它规范了物理传输介质(双绞线、光纤、无线电波)、电气特性(信号电压、电平跳变)、机械连接器尺寸与引脚定义,以及时钟同步机制。物理层的核心关注点在于如何将二进制数字信号可靠地转化为可在物理世界中传播的波形或脉冲,对所传输数据所代表的业务内涵完全不加介入。
数据链路层(Data Link Layer)
数据链路层将不可靠的物理线路提升为无差错的数据链路,负责在直接相邻的节点之间可靠传输以“帧”(Frame)为单位的数据。它引入物理地址(MAC 地址)标识网卡硬件,负责成帧定界、差错检验(如 CRC 循环冗余校验),并在部分协议中提供链路层流量控制。常见的二层设备为以太网交换机。
网络层(Network Layer)
网络层解决数据在异构网络环境下的跨网络路由与逻辑寻址问题。它将传输单元抽象为“数据包”或“分组”(Packet),通过全局统一的逻辑地址(如 IPv4/IPv6 地址)跨越多个物理网络,结合路由算法决定数据包到达目标宿主机的最佳转发路径。路由器是工作在这一层的核心枢纽。
传输层(Transport Layer)
网络层仅负责将数据包递送到目标主机,而传输层则接管端到端(End-to-End,即进程到进程)的通信连接。传输层通过端口号区分宿主机内不同的运行程序,并提供按需的传输可靠性保障。TCP 协议在这一层构建了连接建立、序号追踪、丢包重传与拥塞控制;而 UDP 则保留了轻量的无连接数据报投递。
会话层(Session Layer)
会话层负责在互联系统的应用程序之间建立、管理、同步并终止会话连接。它提供会话检查点与恢复机制,以便在长事务中断后能够从断点继续传输。在当今广泛采用的 TCP/IP 体系中,会话层通常没有独立的协议实体,其逻辑多半由应用自身或上层框架兼并。
表示层(Presentation Layer)
表示层关注所交换数据的语法与语义解释,确保异构系统之间能够正确理解数据内容。其主要职责涵盖字符集转换(如 UTF-8 与 ASCII)、二进制结构的序列化与反序列化,以及数据压缩与加解密处理(例如 TLS 握手的部分语义)。同样,在现代体系中,这些功能大多已沉淀为开发语言的运行时库或应用层协议的固有组件。
应用层(Application Layer)
应用层是网络协议栈与最终用户程序直接交互的最顶层,直接向各类应用程序暴露网络通信能力。HTTP/HTTPS、DNS、SSH、SMTP 等众多耳熟能详的应用协议均运行在这一层。浏览器、邮件客户端和各类分布式客户端程序,正是通过调用这些协议契约来完成特定业务诉求的。
3. 端到端通信中的逐层封装与路由器跨层现实
为了清晰洞察分层模型在真实运行时的流转逻辑,我们以浏览器发起一次普通的 Web 请求为例:
当用户点击超链接时,浏览器首先构造出一个标准的 HTTP 请求报文(应用层)。随后,传输层 TCP 为该报文前置添加 20 字节的 TCP 首部,填入源端口与目标服务端口(如 80),将其封装为一个 TCP 报文段(Segment);接着,网络层在报文段前方附加 20 字节的 IPv4 首部,打上本机的源 IP 与目标服务器的宿主 IP,将其封装为 IP 数据报(Packet);数据链路层随后接管该数据报,在首部附加上本地网关的下一跳物理 MAC 地址,末尾追加校验码,组装成以太网帧(Frame);最终,物理层网卡将帧中的二进制序列转换为高频电信号发射至网线。整个自顶向下的流转,构成了清晰的逐层嵌套封装。
当这个以太网帧沿着线路到达第一跳交换机和汇聚路由器时,中间路由设备在处理边界上呈现出鲜明的层次差异:传统的二层以太网交换机仅解析外层的 MAC 地址并在端口间转发表项;而路由器则是一个标准的三层系统——它接收到物理帧后,首先剥除以太网帧头,提取出内层的 IP 数据报,检查目的 IP 地址并检索本地内核路由表;在确定下一跳网关与出接口之后,路由器会将 IP 数据报的生存时间(TTL)减 1,重新计算并写入 IPv4 首部校验和,随后为该数据报重新打上当前出接口的源 MAC 和下一跳路由器的目标 MAC 地址,重新封装为新的物理帧推送出去。由此可见,物理链路层首部在每一个局域网跳步(Hop)之间都会被剥落重构,而核心的网络层 IP 地址在无 NAT 介入的标准路由拓扑中则始终保持全局不变。
报文在公网历经多跳中转,最终到达目标服务器网卡。服务器网络子系统自底层向上执行逐级解封装:网卡校验并剥离以太网帧头,IP 模块校验 IP 首部确认目标地址属于自身,TCP 模块核对校验和与端口号并将载荷汇入套接字接收队列,最终由 Web 守护进程调用 recv 读取纯粹的 HTTP 报文。随后的 HTTP 响应报文则沿相反方向重演相同的封装与解封装生命周期,直至安全交付至浏览器渲染展示。
4. 习题
第 9 章正文里的 encap.c(code/系统编程/9_网络基础/encap.c)不调用套接字,只在内存里按协议格式拼一个完整的帧。下面四道题从它出发,把「每一层加了什么、哪一层改了什么」变成可以量的数字。
encap.c 拼出的帧里,以太网 14 字节、IPv4 20 字节、UDP 8 字节、负载 2 字节,合计 44 字节。按同样的头部开销,算出下面两次传输的总长与头部占比(头部字节数除以不含帧尾校验的帧长,保留一位小数):
(1) [1.1] 用 TCP 发送 1 字节,TCP 首部 20 字节。总长是多少?头部占多少?
(2) [1.1] 用 UDP 发送 1460 字节。总长是多少?头部占多少?
(3) [1.1] 比较两问的结果,说明为什么按键、游戏状态这类几十字节的小消息,即使不在乎可靠性,也很少为每次输入单独开一个数据报。
正文说帧里的物理地址每一跳都变,IP 地址不变。在 encap.c 里模拟一次转发:把源 MAC 改成 02:00:00:00:00:03、目的 MAC 改成 02:00:00:00:00:04,把 TTL 由 64 减到 63,然后重新编译运行。
要求:
- 打印改完、重算校验和之前的字节转储,指出相对原帧哪几个偏移上的字节变了;
- 重算 IPv4 首部校验和,写出新值,并确认
receiver check仍然是0xffff; - 端口、地址、负载、UDP 校验和都没动:为什么 IPv4 首部校验和必须重算,UDP 校验和却不用?
参考:原帧的 IPv4 首部校验和是 0xf760,UDP 校验和是 0xeb20;TTL 改为 63 后 IPv4 首部校验和应为 0xf860。
UDP 数据报交给 IP 后可能被分片。写一个 frag MTU PAYLOAD:给定链路 MTU 和 UDP 负载长度(不计 8 字节 UDP 首部),按 IP 分片规则算出每一片,输出:
MTU 1500, UDP payload 4000 (datagram 4008 bytes)
frag 1: offset 0 bytes 1480 MF=1 ip total 1500
frag 2: offset 185 bytes 1480 MF=1 ip total 1500
frag 3: offset 370 bytes 1048 MF=0 ip total 10682
3
4
规则:每片的 IP 数据部分除最后一片外必须是 8 的倍数,偏移以 8 字节为单位,除最后一片外 MF 为 1。验收:
./frag 1500 4000输出三片,长度依次 1480、1480、1048,偏移 0、185、370;./frag 576 4000输出 8 片,前 7 片各 552 字节,最后一片 144 字节、偏移 483;./frag 9000 4000只有 1 片,4008 字节,不分片。
最后一问:第二片的偏移为什么是 185 而不是 1480?中间那个分片的 IP 首部里,标志位和片偏移各是什么?
正文的封装例子只有一段链路。写一个 path FRAMEFILE HOPS:读入一个十六进制帧和一个逐跳的转发脚本,模拟它经过两台路由器到达目的主机,每一跳输出一行。
帧文件格式与第 9 章习题数据相同(每行 16 个十六进制字节)。转发脚本每行描述一段链路,写三个字段 DEV SRC_MAC NEXTHOP_MAC,表示这一跳从哪个接口发出、源 MAC 换成什么、目的 MAC 换成什么;单独一行 local 表示这一跳就是目的主机,不再转发。用下面这个帧(192.168.1.10:40000 发往 203.0.113.5:9999,负载 ping,46 字节):
02 00 00 00 00 01 02 00 00 00 00 02 08 00 45 00
00 20 2b 1c 40 00 40 11 11 f9 c0 a8 01 0a cb 00
71 05 9c 40 27 0f 00 0c 5f fd 70 69 6e 672
3
转发脚本:
eth0 02:00:00:00:00:02 02:00:00:00:00:0b
eth1 02:00:00:00:00:0c 02:00:00:00:00:0d
local2
3
程序的要求:
- 每处理一行
DEV SRC_MAC NEXTHOP_MAC,先按当前的 TTL 打印一行hop N: dev DEV src SRC_MAC dst NEXTHOP_MAC ttl T,再把源、目的 MAC 换成给定的值,TTL 减一,重算 IPv4 首部校验和; - TTL 已经是 0 时输出
dropped: TTL exhausted并停止,不再写帧; - 遇到
local时不再改写帧,直接输出local: delivered to UDP port 9999, ttl T,T是此刻帧里的 TTL(上一次转发已经减过),端口从 UDP 首部的目的端口字段读出; - 最后把每一跳改写后的完整帧按十六进制打印出来,便于核对。
验收:上面的输入应输出两行 hop 加一行 local;hop 两行的 TTL 是 64 和 63,local 那行是 62,因为第二行转发时把它减了一次;两个转发出去的帧里 IPv4 首部校验和分别是 0x12f9 与 0x13f9,把各自的首部字节重新求和都是 0xffff;目的端口打印 9999。再把输入帧的 TTL 改成 1 并重算它的 IPv4 首部校验和,重新运行:第一跳照常输出 ttl 1 并把 TTL 写到 0,第二跳应当输出 dropped: TTL exhausted 而不是继续转发。
最后一问:真实的路由器改写了帧里的哪些字段、没改哪些?NAT 设备把源地址换成公网地址时,为什么必须同时改 UDP 或 TCP 的校验和,只改 IPv4 首部校验和不够?