写这篇东西的起因很直接:不管是准备计算机网络考试、复习 408,还是被面试官追问“打开一个网页背后发生了什么”,你迟早都得和 OSI 七层模型、TCP/IP 协议栈、TCP 三次握手、UDP 这些词正面相遇。我在这个方向待了很多年,也给不少同学踩过坑,最深的感受是:很多人不是记不住概念,而是不知道这些概念到底在解决什么问题,更不知道怎么做实验验证它。所以这篇不再按教科书顺序念一遍,而是把 OSI/TCPIP 模型和 TCP/UDP 协议的内部逻辑拆开,讲清楚“为什么分层”“TCP 到底拿什么保证可靠”“UDP 在哪些场景反而是优解”,最后再带一段 iperf3 打流和抓包的实操。
内容会比较长,适合三类人:正在期末复习或准备考研的,刚开始做网络开发的,以及想系统补一遍网络基础的运维和测试。有基础的可以直接跳到第 3 章看 TCP 细节,新手建议从头顺着读。
1. 为什么我们都绕不开“分层模型”:OSI 与 TCP/IP 的设计初衷
其实网络通信最原始的需求非常简单:让两个进程之间能互相传数据。但你要是真去实现,会发现难点全藏在“细节”里——两台机器可以是不同厂商的硬件,链路可以是铜线、光纤甚至无线,中间要经过很多台转发设备,还要保证不同程序各取各的数据不乱套。如果所有功能都塞进一个大协议,任何环节改动都会牵一发动全身。
我经常拿寄快递打比方。你寄东西时只需要写地址、交给驿站,完全不用关心包裹上了什么车、走哪条航线、中间经停哪些分拣中心。驿站负责选路,运输部门负责运输,你只关心“能不能送到”。网络分层就是这套逻辑:每一层只对上层提供稳定的服务接口,下层怎么实现是下层的事。这种“封装 + 分工”的思想,和软件工程里的接口隔离、单一职责完全同源。
分层至少解决了三个现实问题。第一是异构设备互通,不同厂商、不同介质都能按统一的标准对接;第二是复杂度拆解,把端到端的通信拆成“每一跳”“每一段”的子问题;第三是独立演进,物理层从铜缆换到光纤,HTTP 层根本无感知。这就是为什么教学上永远绕不开 OSI 和 TCP/IP 这两套模型。
1.1 分层的核心收益:把“端到端”变成“逐段处理”
数据从主机 A 到主机 B,表面上是一件事,实际上要同时处理物理信号、链路封装、网络寻址、路径选择、进程间通信、应用语义。如果不分层,协议设计会复杂到无法维护。分层之后,每一层只需要关心自己的头部字段和职责:链路层管帧和 MAC,网络层管 IP 和路由,传输层管端口和可靠性,应用层管业务数据。各层只依赖下一层的服务,不越级,不跨层。
这也是 TCP/IP 模型和 OSI 模型存在差异的根源。OSI 是“先定义规范,再找实现”,把网络分成七层,理论上非常工整;TCP/IP 是“先有现实协议,再总结分层”,它更强调能不能跑起来。所以你去看真实网络报文,会发现会话层、表示层这些并不存在独立的协议头,它们的功能要么被应用层协议覆盖,要么被操作系统和库函数吃掉。考试里背七层没问题,工作中排查问题还是按 TCP/IP 的视角更快。
1.2 OSI 七层与 TCP/IP 四层/五层到底怎么对应
很多人一上来就背“物理层、数据链路层、网络层、传输层、会话层、表示层、应用层”,但背完仍然画不出数据封装图。我建议你这样记:OSI 的第一到第三层解决“怎么把数据送到目标设备”,第四层解决“送到设备的哪个进程”,第五到第七层解决“进程之间怎么理解数据”。一句话就能把功能域划清楚。
实际教学常用的是五层模型:应用层、传输层、网络层、数据链路层、物理层。四层模型把数据链路层和物理层并成“网络接口层”。对应关系用一张表就能说完:
| OSI 七层 | TCP/IP 四层/五层习惯 | 数据单位 | 典型协议/设备 |
|---|---|---|---|
| 物理层 | 网络接口层(链路+物理) | 比特 bit | 中继器、集线器,Ethernet 线路标准 |
| 数据链路层 | 网络接口层 | 帧 Frame | 交换机、以太网协议、VLAN、ARP 的下层承载 |
| 网络层 | 网际层 | 包 Packet | 路由器、IP、ICMP、IGMP |
| 传输层 | 传输层 | TCP 段/UDP 数据报 | TCP、UDP |
| 会话层/表示层/应用层 | 应用层 | 报文 Message | HTTP、FTP、SMTP、DNS |
理解这张表有个关键:数据在每一层会加一个头部,所以真实线缆上传的比特里,其实是“用户数据+各层头部”。后面做抓包实验时,你看到的就是这个层层包裹的结果。
2. OSI 各层到底负责什么:从比特到报文的完整旅程
2.1 物理层与数据链路层:比特怎么变成帧
物理层最底层,它管的是电压、光信号、无线频率、接口形状、传输速率和双工模式。物理层不关心电平里的 0 和 1 是什么意思,只负责把比特流从一个节点送到相邻节点。这一层典型设备是集线器,它收到信号就向所有端口转发,效率低但结构简单。
数据链路层就要“懂一点内容”了。它把比特组装成帧,帧里面有目的 MAC 地址、源 MAC 地址、类型字段、数据和帧校验序列 FCS。交换机是典型的链路层设备,它通过学习源 MAC 地址建立 MAC 地址表,再按目的 MAC 决定从哪个端口转发,比集线器聪明得多。顺便说一个高频误解:IP 地址是“最终目标”,但真正让数据在一条链路上前进的是 MAC 地址。IP 到 MAC 的映射需要 ARP 协议,第一次 ping 陌生主机时抓包,你能清楚看到 ARP 请求广播的过程。
2.2 网络层:IP 寻址和路由选择的“交通中枢”
网络层解决的核心问题是“数据应该走哪条路到目标网段”。它会把传输层传下来的数据封装成 IP 包,加上源 IP、目的 IP、TTL、协议号等字段。路由器每转发一跳,TTL 减 1,减到 0 就丢弃并回 ICMP 超时报文,这样做是为了防止环路的死循环。
IP 本身是“尽力而为”的,它不保证不丢、不保证有序,这正好给上层 TCP 留下了存在意义。很多人问 IGMP 和 ICMP 有什么区别:ICMP 是网络层的辅助协议,用来报告差错和探测(比如 ping);IGMP 是用来管理组播组的协议,它在网络层附近工作,配合路由器处理多播成员的加入和离开。考到“多播”时,这一条要分清。
2.3 传输层:从“主机到主机”升级到“进程到进程”
网络层虽然能把包送到主机,但主机上同时跑着浏览器、邮件客户端、游戏若干个程序,内核必须知道这个数据应该交给哪个进程。传输层引入端口号,用“IP 地址 + TCP/UDP 端口”组成的 Socket 唯一定位一个通信端点,把数据从一台主机上的进程传送到另一台主机上的进程。
TCP 和 UDP 在这里分道扬镳。TCP 是面向连接、可靠、基于字节流的协议,首部最少 20 字节;UDP 是无连接、尽力而为、基于数据报的协议,首部只有 8 字节。所谓“封装”就是在应用层数据前面加上源端口、目的端口、序号、校验和等字段;“分用”则是接收端根据目的端口号把数据交给对应进程。理解了这个过程,你就知道为什么 TCP 和 UDP 不能靠端口号直接区别谁更快——端口只是门牌,可靠性来自协议自身机制。
2.4 会话层、表示层和应用层:为什么现实中经常“三合一”
说句实话,工作中几乎没人会单独说“我在会话层调协议”。会话层的职责是建立、管理和终止会话,表示层负责数据格式转换、编码、加密和压缩。现代协议栈里,这些职责被分散了:HTTP 头里的 Content-Encoding 管压缩,TLS 管加密,操作系统底层管字符编码。所以你看到的大部分资料把 OSI 上三层合并成 TCP/IP 的“应用层”,完全够用。
但考试时如果考 OSI,你要会区分:会话层对应的是“会话”而不是“连接”,表示层对应“语法和语义的转换”,应用层才是文件传输、邮件、远程登录这些具体服务。面试时被问到“HTTPS 加密属于哪一层”,最稳妥的答法是:OSI 角度可归入表示层,但现代 TCP/IP 实现里一般算应用层,因为 TLS 握手在应用层完成,只是加密过程依赖底层可靠传输。
2.5 一张图式表格记住各层功能、数据单位、设备
很多资料会单独列设备,其实关键在于“设备工作在哪个层”。交换机看 MAC,路由器看 IP,防火墙经常被调侃“工作在应用层也能工作在传输层”,看具体配置。整理一个高频速查表:
| 层级 | 核心职责 | 数据单位 | 典型设备 | 常见协议/技术 |
|---|---|---|---|---|
| 物理层 | 传输原始比特流 | bit | 中继器、集线器 | 以太网物理层标准、Wi-Fi 物理层 |
| 数据链路层 | 帧封装、MAC 寻址、差错检测 | frame | 交换机、网卡 | 以太网、VLAN、PPPoE、ARP(辅助) |
| 网络层 | 逻辑寻址、路由选择 | packet | 路由器 | IP、ICMP、IGMP、OSPF |
| 传输层 | 端到端通信、可靠/不可靠传输 | segment/datagram | 四层负载均衡设备 | TCP、UDP |
| 应用层 | 业务语义和数据交换 | message | 应用网关、代理 | HTTP、FTP、SMTP、DNS、Modbus/TCP |
背不住也没关系,你只要在脑子里默念数据“从应用层一路向下,每层加一个头;接收端从物理层一路向上,每层剥一个头”,基本不会错。
3. 传输层重点拆解:TCP 和 UDP 的完整机制
3.1 端口号、Socket 与封装分用:门牌号怎么发挥作用
假设你要给某台服务器发 HTTP 请求,浏览器会在操作系统里创建一个 Socket,绑定一个随机端口,然后与服务器的 80 端口通信。Socket 的本质就是“IP 地址 + 端口号”的组合,它同时标记了通信双方。TCP 报文段里必须有源端口和目的端口;UDP 数据报也一样。区别在于,TCP 头部比 UDP 多了序号、确认号、窗口、标志位等字段,因此开销更大。
封装分用是理解协议栈的入口。发送时:应用层数据传给传输层,TCP/UDP 加头部;再传给网络层加 IP 头;再传给链路层加帧头帧尾。接收时反向剥头。我在实训课上总让学生画这条链路,画明白之后,很多“端口不通”的排查思路就自然出来了。值得提醒的是,UDP 接收程序处理大数据时经常要自己分包和组包,因为 UDP 本身不切割应用层消息,一个 send 对应一个数据报,超过 MTU 可能触发 IP 分片或直接丢弃,所以 C 系、Java 等写 UDP 时通常要自己规定消息头和分片逻辑。
3.2 TCP 三次握手与四次挥手:不是形式,是状态机
TCP 是面向连接的,传输数据前必须先建一个“虚拟连接”。三次握手的报文序列是:先由客户端发送 SYN 报文,把 SYN 标志位置 1,并携带初始序号;服务端收到后回复 SYN+ACK,表示“我收到你的同步请求,同时我也要同步”;最后客户端再回一个 ACK,完成建立。
常有人问“为什么不能两次握手”。核心原因是防止“历史失效连接请求”被服务端误以为是新连接。比如客户端第一次发的 SYN 因为网络拥堵老在半路,超时后客户端重新发了一个新的 SYN,结果旧 SYN 先到达服务端。如果只有两次握手,服务端会立刻分配资源并建立连接,客户端收到服务端的响应后才发现这不是我当前想要的连接,但服务端的资源已经被浪费了;三次握手时,客户端可以根据初始序号判断这是不是旧 SYN 的响应,一旦发现是历史连接就发 RST 断开。所以三次握手不是多此一举,它在用一次额外往返解决“旧包迟到”的经典问题。
四次挥手的过程则对应连接的拆除:主动关闭方发 FIN,对端回 ACK,表示“你的数据我收完了”;对端再发 FIN,主动方回 ACK,双方各自释放资源。这里最关键的是 TIME_WAIT 状态,它只出现在主动关闭方,要等 2 个最大报文段生存时间(2MSL)。目的有两个:一是让最后一个 ACK 如果丢失还能重传;二是让旧连接上的迟到报文在网络里彻底消失,避免污染新连接。实际开发里,短连接服务端经常能看到大量 TIME_WAIT 端口堆积,原因就在这。
3.3 TCP 可靠传输的核心:序号、确认号、滑动窗口与重传
TCP 的可靠不是靠网络保证,而是靠“确认 + 重传”自证。它把要发送的字节流编号,每个报文段的序号表示这段数据的起始字节号;确认号则表示“我已正确收到这个序号之前的所有字节,下一个请发这个序号”。这种累计确认机制非常高效:哪怕中途丢了多个小包,只要最末尾的确认到达,发送方就知道前面的都收到了。
滑动窗口则让发送方不必傻等一个一个确认。发送窗口大小同时受两个因素约束:接收方在 TCP 头部“窗口”字段通告的接收能力(rwnd),以及发送方根据网络状况自己维护的拥塞窗口(cwnd)。实际可用窗口取两者的较小值。如果收到连续三个重复 ACK,TCP 会认为有报文丢失,触发快速重传,也就是常说的“TCP dup ack 机制”——不用等超时,直接把没被确认的数据重发出来。这个机制在实际网络里经常决定弱网下的体验,排查重传时一定把它和乱序区分开。
3.4 流量控制与拥塞控制:一个是收方说了算,一个是网络说了算
很多初学者把流量控制和拥塞控制混成一锅粥。其实区分很简单:流量控制关注“接收方内存能不能装下”,由接收方把剩余缓冲区大小通过窗口字段告诉发送方,防止发送方把接收方冲垮。拥塞控制关注“网络链路能不能扛住”,由发送方主动探测网络容量,发现丢包就降低发送速率,防止把网络堵死。
拥塞控制有四条曲线要熟:慢启动、拥塞避免、快重传、快恢复。慢启动阶段 cwnd 指数增长,到达慢启动阈值 ssthresh 后转入线性增长;如果超时,ssthresh 减半,cwnd 回到 1,重新慢启动;如果只是收到三个重复 ACK 触发快重传,则进入快恢复,把 cwnd 降一半而不是归零。这也是为什么 TCP 实际吞吐量会有“锯齿状”波动。考试和面试都爱在这一块出计算题,建议你亲手推一遍。
3.5 UDP 的无连接与“尽力而为”:它真的一无是处吗
UDP 报文首部极小,只有源端口、目的端口、长度、校验和 8 个字节。它不需要握手,不需要 ACK,发完就结束,丢了也不管。表面看它一无是处,但在实时音视频、DNS 查询、DHCP 获取地址、网络游戏同步这些场景,UDP 反而更合适,因为这类业务更怕延迟和抖动,而不是怕丢一两个采样包。就算丢包,听感上也只是偶尔的杂音,如果换成 TCP,重传造成的卡顿反而更明显。
UDP 还天生支持广播和多播,这是 TCP 做不到的。很多设备协议也偏爱 UDP,比如轻量控制命令。至于“QUIC 不是可靠的吗,为什么底层用 UDP”,因为 QUIC 把可靠性搬到了用户态,用 UDP 做底避免 TCP 的队头阻塞,这恰恰说明选择协议的关键不是“可靠与否”,而是“在哪儿实现可靠性”。
3.6 TCP 与 UDP 选型对照表
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接,需三次握手 | 无连接,直接发 |
| 可靠性 | 确认、重传、保序 | 不保证送达,不重传 |
| 有序性 | 字节流有序交付 | 数据报可能乱序 |
| 传输方式 | 主要一对一 | 一对一、一对多、多对多 |
| 头部开销 | 20 字节以上 | 8 字节 |
| 传输效率 | 可能受重传和拥塞控制影响 | 延迟低,但可能丢包 |
| 典型场景 | HTTP/HTTPS、FTP、SSH、Modbus TCP | DNS、DHCP、RTP 视频、QUIC |
实际选型时,我会先问三个问题:应用能不能容忍丢包后等重传?需不需要严格有序?需不需要广播?只要答案偏向“实时优先”“可容忍小丢包”,UDP 大概率是合理选择。
4. 动手验证:用 iperf3 打流与抓包看真实报文
4.1 为什么要“打流”
只看书本很难体会“可靠传输”到底在做什么。打流就是在两台机器之间制造大流量,通过结果参数观察链路带宽、丢包率、抖动和重传次数。这种方式特别适合验证网络配置是否生效、防火墙有没有拦人、物理链路是否达标,以及对比 TCP 和 UDP 的行为差异。
工具首选 iperf3,跨平台、命令简单、输出可读性强。实验拓扑最简单的是两台机器接同一台交换机,一台当服务端,一台当客户端。如果只有一台机器,也可以回环测,就是意义打折。下面所有命令我都按 Linux 环境写,Mac 和 Windows 分支略改就行。
4.2 iperf3 的常用命令:TCP 与 UDP 两种模式
先是服务端启动监听,默认端口 5201:
iperf3 -s客户端测 TCP:
iperf3 -c 192.168.1.100 -t 10输出末尾会给出带宽,注意 TCP 测试还会显示 Retr 重传次数。如果 Retr 一直在涨,基本可以判断链路上存在拥塞或丢包。
测 UDP 要显式指定带宽,因为 iperf3 的 UDP 模式默认只按 1 Mbps 发,很多人第一次测出来数据特别低,就是因为忘了加 -b。一条常用命令:
iperf3 -c 192.168.1.100 -u -b 100M -t 10 -l 1400这段命令表示:以 UDP 模式向服务端 192.168.1.100 发送 100 Mbps 流量,持续 10 秒,每个数据报 1400 字节。输出会包含 Jitter(抖动)、Lost/Total Datagrams(丢包统计)和丢包百分比。如果丢包率很高,说明链路已经承受不住这个速率,或者中间设备有瓶颈。这个结果和 TCP 的“自动降速”一对比,能让你直观看到拥塞控制的必要性。
4.3 通过实验观察 TCP 与 UDP 的真实差异
如果你有权限,可以用 tc 命令给网卡人为加丢包,做一个对照实验。模拟丢包 5%:
sudo tc qdisc add dev eth0 root netem loss 5%然后分别跑一遍 TCP 和 UDP 打流。UDP 模式的丢包率会明显接近 5%,因为发送速率固定,丢了就丢了;TCP 模式由于“确认 + 重传 + 拥塞控制”的共同作用,应用层看到的吞吐量下降,但数据完整性更高——因为 TCP 内部一直在重传丢失的包。这个实验非常直观,做完你就明白为什么“TCP 可靠但可能慢,UDP 快速但不保证到达”。
做完实验记得恢复网卡:
sudo tc qdisc del dev eth0 root netem提醒一句,tc 命令在云主机或容器里未必有权限,可以改用 iperf3 加 -u 打满带宽,观察丢包率来近似模拟。
4.4 抓包验证三次握手和四次挥手
打流只能看到统计,抓包才能“眼见为实”。先用一个最简单的 TCP 服务端,比如 nc:
nc -l 8080另一台机器连接它,同时用 tcpdump 抓包:
sudo tcpdump -i any tcp port 8080 -w handshake.pcap把 pcap 文件拖进 Wireshark 后,过滤tcp.flags.syn==1,你就能看清楚三步报文:客户端 SYN,服务端 SYN+ACK,客户端 ACK。再关闭连接就能看到 FIN 和 ACK 的交错过程。我建议新手把“这三个报文对应的相对序号变化”也算一遍,比单纯背状态转移图管用。
抓 UDP 也一样,比如模拟一次 DNS 查询:
sudo tcpdump -i any udp port 53 -w dns.pcap你会发现整个交互非常“轻”:请求一个包,响应一个包,没有握手,没有确认。把 TCP 的握手包和 UDP 的两包交互放一起对比,无连接和面向连接的区别会刻进脑子里。
5. 常见问题与排查技巧实录
5.1 高频故障:端口不通、地址已在使用、TCP 重传与 UDP 丢包
端口不通是最常见的网络开发问题。我的排查顺序永远是:先看服务是否在监听,确认内核有没有收包,再看防火墙。监听用ss -lntup | grep :8080,如果连本地 localhost 都不通,说明服务绑的地址不对;如果本机通、外部不通,再查 iptables/firewalld 和安全组。很多人一上来就怀疑防火墙,结果最后发现是服务监听在 127.0.0.1,外部当然访问不到。
Java 类客户端重连时经常报“Address already in use”,这不是端口被占死,而是客户端本地端口处在 TIME_WAIT 状态。解决办法包括开启 SO_REUSEADDR,或者在业务层复用连接而不是频繁新建短连接。这类问题在 push 服务和网关类系统里特别常见,属于“协议状态机”在实际工程里的经典显影。
TCP 重传多了,先别急着骂网络。用ip -s link show eth0看网卡统计里的 errors/dropped,再用 tcpdump 抓包看是乱序导致的重复 ACK,还是真正的丢包。如果是多链路捆绑场景,经常出现“同一流的前后半段走了不同路径”,从而触发 dup ack,这种问题是拓扑问题,不是丢包问题。UDP 丢包则有另一个排查点:接收进程处理太慢,导致内核接收缓冲区溢出。netstat -su里的 RcvbufErrors 能直接暴露这一点,你可以用 setsockopt 调大 SO_RCVBUF,但最终还是得看应用处理速度。
5.2 常见理解误区和考试易错点
几个高频误区给新手提前拆掉。第一,“TCP 可靠 = 不会丢数据”,不对,TCP 照样会丢,只是会用重传把数据补回来,最终向上层保证有序且完整。第二,“UDP 一定比 TCP 快”,也不对,在干净局域网上两者带宽差距可能很小,UDP 的收益主要在低延迟和头部开销小,但如果链路丢包严重,UDP 靠丢包换延迟,TCP 靠重传保数据,两者没有绝对优劣。第三,“OSI 七层是真实存在的”,严格讲它更像一个参考坐标系,实际跑的大多是 TCP/IP 五层模型,尤其是会话层和表示层基本没有独立协议实现。
考试里还经常纠结三次握手的两处细节:第一,客户端最后一次 ACK 丢失会怎样?服务端会超时重发 SYN+ACK,直到收到 ACK 或放弃,所以被动打开方有重传计时器。第二,SYN 泛洪会产生大量半连接状态,让服务端半连接队列被打满,这也是三层以下攻击的一个原理。408 或期末考如果出现这些题,记住“状态转换 + 报文序号”比死背文字更稳。
5.3 给复习和训练营同学的操作建议
我不太建议上来就背“OSI 七层功能”然后去刷实训答案,因为那样忘得很快。更有效的方法是画数据封装图:从应用层报文开始,逐层加头部,最后到物理层比特,再反向剥头。你只要亲手画两遍,OSI 和 TCP/IP 的区别自然就记牢了。如果看教材吃力,可以先找湖科大教书匠这类带动图讲解的视频建立感觉,再回到王道或第八版教材做知识点闭环。不少人问“这些视频适不适合考 408”,我的观点是:它们适合搭框架,但 408 的滑窗计算、拥塞控制曲线、报文格式题还是得自己动手推导和刷题,光看不能形成条件反射。
经典教材像《计算机网络:自顶向下方法》的思路是“先应用层后底层”,更符合人类认知习惯,适合入门;王道这类资料适合后期刷题冲刺。但最关键的还是补实验:至少做一遍 4.3 的 tc+iperf3 对照实验,再抓一次 HTTP 请求过程中 TCP 三次握手的包。做完这两件事,你的理解深度会明显超过只背概念的学生。
6. 写在最后:一点个人经验和补充技巧
我最早真正理解 TCP,不是在课堂,而是在实验室两台机器上 ping 一个陌生网段的 IP,抓包看到 ARP 请求真正发出、IP 包随后登场的那一刻。从那以后,所有模型都不再是抽象方块。所以我特别推荐对网络有兴趣的读者做三个小实验:第一,抓一次 ping 陌生 IP 的 ARP 过程;第二,用 iperf3 以不同 -b 值跑 UDP 打流,记录丢包率变化;第三,用 tc 加 5% 丢包后对比 TCP 和 UDP 的表现。这三个实验成本很低,但收获非常大。
最后再分享一个我自己的习惯:遇到底层协议说不清的故障,先抓包,不猜。很多看起来诡异的问题,最后都是“应用层重传间隔太短”“防火墙把 UDP 会话踢掉了”“接收缓冲区太小”这类简单原因。把 TCP 和 UDP 的头部结构、握手流程、窗口机制真正吃透,再配合 iperf3、tcpdump、Wireshark 这三件套,绝大多数网络疑难都能变成可验证的判断题。