1. 为什么我坚持用TCP/IP四层模型来理解网络
TCP/IP四层模型这五个字,我在带新人、写故障复盘、跟运维对线的时候几乎每周都要提一次。它能干的事情很具体:把一次网络通信拆成网络接口层、网络层、传输层、应用层四段,让你在出问题的时候知道该往哪个方向看,而不是抱着一句“网不通”干瞪眼。它解决的问题是定位效率——同一个“页面打不开”的现象,可能是网线没插好,也可能是路由黑洞,可能是防火墙拦了端口,还可能是应用层返回了502,四层模型给的就是一套排查顺序。
这篇内容适合三类人看:刚入行的开发同学,写代码时会把connect timeout和read timeout混为一谈的;做后端、运维、测试,需要独立定位线上问题的;以及准备面试、被问到“三次握手为什么不是两次”时只能背答案的。就算你只会敲几行 ping,看完也能把四层这套框架用起来。
我不打算把它写成教科书。市面上的分层图已经够多了,缺的是“这一层到底管什么、不管什么”“参数为什么是这个数”“抓包看到的东西对应哪一层”这类落地的东西。下面我按自己的理解顺序来讲:先说分层的本质和取舍,再逐层拆解,然后走一遍完整的数据旅程,最后上手抓包验证,附一份我自己常用的排查速查表。
1.1 从一次“网不通”的排查说起
先说个真实场景。某个内部服务突然报连接超时,值班的同学第一反应是“服务挂了”,去看日志发现日志里干干净净,一行请求都没进来。第二反应是“服务器宕机”,ssh 上去一看机器活得好好的,CPU 空闲。第三反应开始玄学:重启服务、重启机器、换客户端,全都没有用。
后来用分层的方式五分钟定位:先ping对端 IP,通的——网络层没问题,路由可达;再telnet目标端口,卡住不返回——传输层的端口不通;最后看本机防火墙规则,发现一条新下发的策略把该端口拦了。整个过程没有任何“猜”,每次只验证一层,验证完就排掉一层,剩下的范围越来越小。
这就是分层最朴素的价值:把一个大问题切成一串小问题,每层只回答一个是非题。反过来,如果没有这套框架,排查就会变成“把能想到的办法都试一遍”,运气好碰上了,运气不好浪费两小时还说不清原因在哪。
1.2 四层模型到底切开了什么
TCP/IP四层模型的分法,本质是按“职责边界”切,不是按“代码模块”切。每一层只关心自己那一小段,对上层提供接口,对下层提出要求。
| 层级 | 核心职责 | 数据单位 | 典型协议 | 出问题时的表现 |
|---|---|---|---|---|
| 应用层 | 业务语义、报文格式 | 消息 / 报文 | HTTP、DNS、SSH、SMTP | 返回码异常、字段错、超时但连接正常 |
| 传输层 | 端到端可靠性与端口寻址 | 段(TCP)/ 数据报(UDP) | TCP、UDP | 端口不通、连接被重置、重传多 |
| 网络层 | 主机到主机的寻址与路由 | 包(Packet) | IP、ICMP、ARP(部分实现归此层) | ping 不通、跨网段不通、TTL 超时 |
| 网络接口层 | 同一链路上的帧传输 | 帧(Frame) | 以太网、Wi-Fi、PPP | 网线/网卡、MAC 冲突、链路丢包 |
跟 OSI 七层对照,TCP/IP 是把 OSI 的会话层、表示层合并进应用层,把数据链路层和物理层合并成网络接口层。很多教材会强调“OSI 是理论模型、TCP/IP 是工程模型”,我的看法更直接:OSI 切得细,适合教学;TCP/IP 切得粗,适合干活。真实项目里没人会问“这是会话层问题还是表示层问题”,但所有人都会问“这是应用的问题还是网络的问题”。
注意:四层模型里的“网络接口层”不是一个具体协议,而是一个集合名词。以太网、Wi-Fi、PPPoE、4G/5G 的链路层实现都在这里,但它们的行为差异极大。所以排查到这一层时,问“是不是链路层问题”意义不大,得具体问“是不是这个网卡、这个 VLAN、这条专线的问题”。
1.3 分层带来的边界与代价
分层不是白拿的,它有两个代价必须清楚。
第一个代价是头部开销。每下一层加一个头,一个 TCP 段出去,以太网头 14 字节 + IP 头至少 20 字节 + TCP 头至少 20 字节,光头部就 54 字节。MTU 1500 的情况下,真正能装的业务数据只有 1460 字节,这就是后面要展开的 MSS 的来源。如果你做的是高频小包的业务(比如实时对战的帧同步),这个比例就非常难受,这也是很多游戏协议宁可在 UDP 上自己实现可靠性,也不要 TCP 的原因之一。
第二个代价是跨层信息丢失。上层看不到下层的细节,下层也不知道上层在干什么。比如 TCP 只知道“对端确认了这些字节”,但它不知道这些字节是登录请求还是图片数据;应用层也不知道自己这条连接中间穿过了一个 NAT、一个限速策略和一个老旧的中间设备。很多“明明服务没问题但用户就是连不上”的疑难杂症,根因都在这种跨层信息不对称上。
我自己的习惯是:设计阶段按四层去思考,排查阶段一定要跨层去怀疑。分层是为了降低认知负担,不是为了画地为牢。
2. 逐层拆解:每一层究竟管什么、不管什么
教科书的问题是只告诉你“这一层做什么”,不告诉你“这一层不做什么”。而实际工作里,知道一层的边界在哪,比知道它的功能更重要,因为越界的问题你在这层是永远找不到的。
2.1 网络接口层:最容易被忽视的起点
这一层干的事很单纯:把一串字节变成能在物理介质上传输的信号或帧,并且在同一链路内把帧送到正确的邻居手里。以太网里靠的就是 MAC 地址,48 位,前 24 位是厂商标识,后 24 位由厂商分配。同一广播域内,设备之间靠 ARP 把 IP 地址问出 MAC 地址,然后按 MAC 直接投递。
这一层最关键的参数是MTU(最大传输单元),以太网默认 1500 字节。它决定了上层 IP 包的最大尺寸,超过这个尺寸就必须分片,而分片是性能杀手:任一分片丢失,整个原始包都要重传。实际网络里 MTU 不一致是极常见的坑——本地 1500,隧道封装之后变成 1400 甚至更小,如果路径上没人做 MTU 探测或者 ICMP 被拦了,表现就是“小请求正常、大请求卡死”,非常典型。
我见过太多人把“链路层”和“物理层”混为一谈,觉得网线松了才是链路层问题。实际上交换机端口协商成半双工、网卡 offload 导致抓包看到的分段和实际发出的对不上、VLAN 标签配错、无线信道干扰导致重传率高,这些全在这一层,而且全都会表现成上层“偶发超时”。你要是只在应用层加日志,永远看不见它们。
实操心得:抓包时如果看到大量“TCP Previous segment not captured”或者重传,先别急着骂服务端。在交换机上看看接口的 CRC 错误计数和丢包计数,很多时候是链路质量问题,跟 TCP 一点关系都没有。
2.2 网络层:寻址与路由的全部逻辑
网络层的核心是 IP,它做两件事:给每台主机一个全局可路由的地址,以及决定这个包下一步往哪送。注意 IP 是“尽力而为”的,它不保证送达、不保证顺序、不保证不重复,这些全甩给了上面。很多人对这一点没概念,以为 IP 是可靠的,结果在 UDP 上写业务代码时理所当然地认为包不会丢,线上就出事了。
IP 头里几个字段值得单独说:
- TTL(生存时间):每过一个路由器减 1,减到 0 就丢弃并回一个 ICMP 超时。这个设计原本是为了防止老式网络里的包永久打转,现在它的主要用途变成了
traceroute的原理基础,也成了很多排查手段的抓手。 - 协议号:标明上层是 TCP(6)、UDP(17)还是 ICMP(1)。防火墙做策略时,除了看 IP 和端口,常常也要看这个字段。
- 分片相关字段(标识、标志、片偏移):IPv4 里字段齐全,IPv6 干脆把分片挪到了端到端,中间路由器不再分片,这是 IPv6 一个很大的行为变化,很多老排查经验在 IPv6 下直接失效。
路由这块,主机只关心一条:这个目的地址是不是我直连网段的?是就 ARP 找邻居直接发,不是就丢给默认网关。所以“能不能通”这个问题,在网络层其实可以拆成两个子问题——默认网关可达吗、网关知道怎么去目的地吗。这两个问题的排查手段完全不同,前者靠ping 网关,后者靠traceroute。
2.3 传输层:TCP 和 UDP 的真实分工
传输层解决的是“进程到进程”的通信,靠端口号区分同一台机器上的不同服务。这里最常见的误解是“TCP 可靠、UDP 不可靠”,这话没错但太粗。更准确的表述是:TCP 提供的是有序的、带确认的字节流,它保证你收到的字节和发出的字节一模一样;UDP 提供的是带校验的数据报,它只保证“尽力送到”,其余一概不管。
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 需要握手建立、挥手释放 | 无连接,发送即结束 |
| 可靠性 | 确认 + 重传 + 排序 | 无,丢了就丢了 |
| 边界 | 字节流,无消息边界 | 保留消息边界,一次发一次收 |
| 头部开销 | 20 字节起 | 8 字节 |
| 拥塞控制 | 有(慢启动、拥塞避免、快速重传等) | 无,需应用自己实现 |
| 典型用途 | HTTP/1.1、SSH、数据库连接 | DNS、音视频、实时对战、QUIC 底层 |
关于“字节流没有消息边界”这点,值得多讲两句,因为它导致的 bug 我见过太多次。TCP 是流,你发两次 100 字节,对面可能一次收到 200 字节,也可能分三次收到。所以任何基于 TCP 的自定义协议,都必须自己定义长度字段或者分隔符,否则解析一定出错。这不是“偶尔出问题”,而是“早晚出问题”,在高并发和网络抖动下概率会放大。
三次握手和四次挥手,我建议不要只背流程,要理解它背后的动机。三次握手的核心目的是双方都确认对方的收发能力正常,同时交换初始序列号。两次握手只能确认“我发你收”,无法确认“你发我收”,服务端会为一个可能根本收不到响应的连接分配资源。四次挥手则是因为 TCP 是全双工的,两个方向要各自关闭,被动方还可能还有数据没发完,所以不能合并。
2.4 应用层:协议设计与最容易被甩锅的一层
应用层是业务最熟悉的一层,也是问题最容易堆积的一层。HTTP 的状态码、DNS 的解析顺序、SSH 的密钥交换,这些都在这一层。它不管字节怎么传,只关心“这段字节代表什么含义”。
这里有个特别常见的甩锅模式:接口报错,开发说“网络问题”,运维说“应用问题”,来回踢皮球。我的做法是拿一个判断标准卡住——如果 TCP 连接建立成功、数据能双向收发,那基本就是应用层问题。因为传输层能建连说明路是通的,能收发说明没有被拦,剩下的只能是协议格式、超时配置、证书、鉴权这些应用层的事。
应用层设计里我最看重的一点是超时策略。很多服务默认不设超时,或者设一个 30 秒的超时,结果上游早就断了,下游还在傻等,一个慢接口拖垮整个线程池。合理做法是分层设置:连接超时短(1~3 秒),读超时长(按业务 P99 的 2~3 倍),并且总超时不能超过上游的超时预算。这个数字没有标准答案,但一定要算过,不能拍脑袋。
3. 一次完整的数据旅程:从发起请求到收到响应
光看分层图不够直观,走一遍完整的流程,你就明白每一层实际加了什么、减了什么。假设客户端要访问一个 HTTP 接口,域名已经解析成 IP。
3.1 封装:每一层往包里加什么
封装是自顶向下的过程。应用层生成 HTTP 报文(比如 GET 请求,几百字节),交给传输层;传输层加上 TCP 头(源端口、目的端口、序列号、确认号、窗口、标志位等 20 字节),变成 TCP 段;网络层加上 IP 头(源 IP、目的 IP、TTL、协议号等 20 字节),变成 IP 包;链路层加上以太网头(源 MAC、目的 MAC、类型,14 字节)和尾部校验(4 字节),变成帧,最后变成电信号发出去。
算一下开销:1500 的 MTU 减去 20 字节 IP 头、20 字节 TCP 头,得到MSS = 1460 字节。这就是单个 TCP 段能携带的最大业务数据量。如果你的业务一次要传 10KB 数据,那就得切成至少 8 个段,这 8 个段能不能连续发出去,取决于拥塞窗口和接收窗口。
反过来,接收端是解封装:网卡收到帧,校验通过后剥掉以太网头,交给 IP 层;IP 层看目的 IP 是不是自己(或者需要转发),剥掉 IP 头交给 TCP;TCP 按序列号重排、去重、确认,把有序字节流交给应用;应用按自己的协议格式解析。
注意:抓包工具(如 tcpdump)默认看到的是某一层的视图,
-e才显示链路层头,不加就只到 IP 层。很多人抓包后说“看不到 MAC 地址”,其实是参数没加,不是没抓到。
3.2 连接状态的维护:不只是三次握手
从“发起连接”到“关闭连接”,中间的状态变化比大多数人以为的复杂。客户端侧典型状态流转是 CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED;服务端侧是 CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。
这里面最值得关注的两个状态,一个是TIME_WAIT,一个是CLOSE_WAIT。TIME_WAIT 是主动关闭方在等 2MSL,目的是保证最后一个 ACK 能到达,以及让旧连接的迷路包在网络中消散。高并发短连接场景下 TIME_WAIT 堆积是常态,不是故障,但数量太大确实会耗尽本地端口,需要从连接复用(keep-alive)或内核参数上解决。CLOSE_WAIT 则是被动关闭方收到了 FIN 但应用没调 close,堆积说明代码里有连接没关,这是真 bug,必须查代码。
我踩过最典型的一个坑是:某服务用连接池,但异常分支里漏了归还连接,跑几天之后 CLOSE_WAIT 上千,新连接建不上,报的错却是“连接超时”,看日志完全看不出跟连接泄漏有关。从那以后我养成习惯,排查超时问题一定先ss -tan state close-wait看一眼。
3.3 关键参数:窗口、RTT 与带宽的三角关系
传输层的性能,本质上被三个量卡住:带宽、RTT、窗口大小。三者关系是:
理论吞吐上限 = 窗口大小 / RTT举个例子,跨国链路 RTT 200ms,窗口 64KB,那么单连接吞吐上限约 64KB / 0.2s = 320KB/s,也就是 2.5Mbps 左右。哪怕你买的是 1Gbps 带宽,单条 TCP 连接也只能跑这么快。这就是为什么大文件传输要开多连接,也是为什么长肥管道(高带宽高延迟)场景必须开窗口缩放。
反过来的算法也常用:如果想跑满 100Mbps,RTT 50ms,那么需要窗口 = 12.5MB/s × 0.05s ≈ 625KB。这个数字直接决定了你要不要调内核的接收/发送缓冲区。
还有一个常被忽略的量是重传率。netstat -s里的重传段数和总发送段数之比,正常应该远低于 1%。如果超过 2%,说明链路质量有问题,此时的“慢”不是带宽不够,而是有效带宽被重传吃掉了。我曾经遇到一个机房内网重传率 5%,业务方一直以为是代码慢,加了无数日志,最后是交换机一个端口光模块老化。
4. 实操:用抓包和命令亲手验证四层结构
理论说再多,不如自己抓一次包。这一节给的是能直接复现的操作,环境是任意 Linux 机器,不需要特殊权限之外的东西。
4.1 环境与工具准备
需要的东西很基础:
ping、traceroute(或tracepath)用于网络层验证ss(现代系统自带,替代 netstat)用于传输层状态tcpdump用于抓包dig或nslookup用于应用层 DNS 验证curl用于构造应用层请求
# 确认工具是否齐全,缺哪个装哪个 which ping traceroute ss tcpdump dig curl # 查看本机路由表,确认默认网关 ip route show # 查看网卡与 MTU ip link show第一件事是确认自己的网络底子:默认网关是谁、本机 IP 是多少、网卡 MTU 是不是 1500。这三条信息决定了后面所有排查的起点。很多人跳过这步直接抓包,结果抓了半天发现抓的是错的网卡(多网卡机器很常见),白忙一场。
4.2 抓一次请求,对照四层看
开两个终端。第一个终端抓包:
# -nn 不做域名和端口解析,-i 指定网卡,port 80 过滤 sudo tcpdump -i eth0 -nn -A 'tcp port 80' -c 20第二个终端发请求:
curl -v http://example-target/health抓到的输出里,你能清晰看到几件事:三次握手的 SYN、SYN-ACK、ACK 三个包;HTTP 请求的明文内容(-A参数把载荷打成 ASCII);响应的状态行。如果加上-e还能看到 MAC 地址,加上-v能看到 TTL 和窗口值。
重点看三个数字:源端口(随机高位端口,说明客户端用的是临时端口)、TTL(能反推经过了多少跳,常见初始值 64/128/255)、win(接收窗口,反映对端还能收多少)。这三个值平时没人注意,但排查时极其有用,比如 TTL 异常小说明路由绕远了,win 一直是 0 说明接收方处理不过来。
4.3 用命令观察各层状态
不想抓包的时候,用几条命令也能把四层看个大概:
# 网络层:连通性与路径 ping -c 4 目标IP traceroute -n 目标IP # 传输层:端口监听与连接状态统计 ss -tlnp # 本机监听端口 ss -tan state established | wc -l # 已建立连接数 ss -tan state time-wait | wc -l # TIME_WAIT 数量 ss -s # 汇总统计 # 传输层:重传与异常 netstat -s | grep -iE 'retrans|reset|timeout' # 应用层:DNS 解析链路 dig +trace 目标域名dig +trace这条特别值得跑一次,它会把从根到权威的整条解析链路打出来。很多人排查“域名解析慢”,只看本地 DNS 返回时间,其实慢可能发生在链路的任何一段。跑完你就明白为什么 DNS 缓存这么重要,也明白 TTL 设得太短会带来什么代价。
4.4 手工验证 MTU 与分片
这是我最推荐做一次的实验,因为它能把“MTU 不一致”这个抽象概念变成可感知的现象。
# 1472 + 8(ICMP头) + 20(IP头) = 1500,正好不分片 ping -M do -s 1472 目标IP # 1473 就会触发"Message too long" ping -M do -s 1473 目标IP-M do的意思是禁止分片。1472 能通、1473 不通,就说明路径 MTU 是 1500。如果两个都不通但你ping小包是通的,说明路径 MTU 小于 1500,可能是中间有隧道封装。这时候用-s二分法往下试,很快就能测出真实值。
这个实验的现实意义在于:如果你遇到“大文件传一半卡住、小请求正常”的现象,八成就是路径 MTU 比两端都小,而路径上的 ICMP 又被拦了,导致分片永远拼不回来。解决办法要么调小本端 MTU,要么让路径放行必要的 ICMP。
提示:做这个测试前先确认目标允许 ICMP,很多云环境默认禁 ping,测试结果会误导你。可以先
ping小包确认基础连通性,再逐步加大。
5. 常见问题与排查技巧实录
这一节是我自己的速查表,按“现象 → 先怀疑哪层 → 用什么手段”组织,遇到问题可以直接对号入座。
5.1 典型故障速查表
| 现象 | 优先怀疑层 | 快速验证手段 | 常见根因 |
|---|---|---|---|
| 完全 ping 不通 | 网络层 / 接口层 | ip link、ip route、ping 网关 | 网卡 down、网关错、路由缺失 |
| ping 通但端口连不上 | 传输层 | ss -tlnp、telnet IP 端口 | 服务未监听、防火墙拦截、监听在 127.0.0.1 |
| 连接建立后立刻断开 | 应用层 / 传输层 | 抓包看 RST 时机 | 协议不匹配(明文连 TLS 端口)、鉴权失败 |
| 偶发超时,重试就好 | 接口层 / 网络层 | netstat -s看重传、看接口错误计数 | 链路丢包、MTU 不一致、无线干扰 |
| 大响应卡死,小响应正常 | 接口层 / 网络层 | ping -M do -s二分测 MTU | 路径 MTU 更小且 ICMP 被拦 |
| 连接数上不去 | 传输层 | ss -s、看 TIME_WAIT 与本地端口范围 | 端口耗尽、未复用连接 |
| 服务端 CLOSE_WAIT 堆积 | 应用层 | ss -tan state close-wait | 代码未关闭连接、连接池泄漏 |
| 域名解析慢或失败 | 应用层 | dig +trace、cat /etc/resolv.conf | DNS 配置错、上游超时、TTL 过短 |
5.2 分层定位法:从哪一层开始怀疑
我的固定顺序是自下而上,理由很实际:下面的层不通,上面所有层的表现都是假的。链路断了,你在应用层看日志只会看到一堆超时,毫无信息量。
具体动作是四步:
第一步,ping目标 IP。通,说明网络层没问题,直接跳到第三步;不通,进入第二步。
第二步,ping网关。网关通说明本机链路正常,问题在网关之外,用traceroute看在哪一跳断的;网关不通说明本机链路或 IP 配置有问题,查ip link、ip addr、ARP 表。
第三步,测目标端口。能连上说明传输层通,问题在应用层;连不上说明被拦或服务没起,查监听状态和防火墙规则。
第四步,应用层验证。用curl -v看完整的请求响应,重点看状态码、响应头、耗时分解(DNS、连接、传输各花多久)。
这套顺序的价值在于每一步都能排除一大片,不会来回反复。我见过有人一上来就翻应用日志,翻了两小时,最后发现是安全组没放行,这种时间浪费完全可以避免。
5.3 几个反直觉的坑
第一个坑:ping通不代表端口通。ICMP 和 TCP 走的是完全不同的处理路径,很多环境禁 ping 但端口正常,也有环境允许 ping 但业务端口全拦。只用 ping 判断“网络好不好”是非常不靠谱的。
第二个坑:telnet卡住不等于端口关闭。如果是被防火墙 DROP,表现是一直卡着直到超时;如果是 REJECT,表现是立刻拒绝。这两种情况对应的处理方式完全不同,抓包看有没有回 RST 就能区分。
第三个坑:抓包看到重传,不一定是网络丢了。如果抓包点在本机网卡上,而网卡开了大量 offload,你看到的分段可能和实际发出的不一致,甚至会出现“假重传”。正确做法是在对端或中间设备上抓,或者临时关掉 offload 再验证。
第四个坑:TIME_WAIT 多不一定是问题。它是正常的连接关闭流程产物,只要本地端口没耗尽、不影响新建连接,就不用管。真正要处理的是主动减少短连接、开启连接复用,而不是简单调小 TIME_WAIT 时间,那会破坏协议的可靠性保证。
第五个坑:窗口为 0 不等于对端挂了。接收窗口为 0 说明对端应用层没及时读数据,缓冲区满了,TCP 会进入持续探测。这时候该查的是对端应用的处理能力,而不是网络。
5.4 一份可直接抄的排查脚本骨架
最后给一段我自己常用的最小排查脚本,出问题时跑一遍,基本信息全有了:
#!/bin/bash TARGET=$1 PORT=$2 echo "=== 1. 本机网络配置 ===" ip -brief addr ip route show echo "=== 2. 网络层连通性 ===" ping -c 3 -W 2 "$TARGET" echo "=== 3. 路径追踪 ===" traceroute -n -w 2 -q 1 "$TARGET" | head -15 echo "=== 4. 传输层端口 ===" timeout 3 bash -c "echo > /dev/tcp/$TARGET/$PORT" && echo "端口 $PORT 可连接" || echo "端口 $PORT 不可连接" echo "=== 5. 连接状态概览 ===" ss -s echo "TIME_WAIT: $(ss -tan state time-wait | wc -l)" echo "CLOSE_WAIT: $(ss -tan state close-wait | wc -l)" echo "=== 6. 重传与异常 ===" netstat -s 2>/dev/null | grep -iE 'retransmit|reset|failed'这段脚本没什么高深技巧,就是把上面四步固化成命令,避免慌乱时漏步骤。我建议每个团队都留一份类似的,新人值班时直接用,比口头指导靠谱得多。
我个人在这些年踩坑的经验里最看重的一点是:分层模型最大的价值不在“知道有四层”,而在“知道自己现在看的是哪一层”。同样一条报错,有人看了十分钟就知道该查防火墙,有人查了一整天还在翻业务日志,差别不在技术深度,而在有没有一套稳定的定位顺序。四层这套框架就是那个顺序。你不需要把它背得滚瓜烂熟,但遇到问题时能条件反射地问一句“这是哪一层的事”,基本上就已经赢了大半。至于剩下的细节,抓几次包、改几次 MTU、看几次重传率,自然就熟了。