刚入行那会儿,我被“七层模型”和“四层模型”搞得晕头转向。面试官一问“TCP 和 UDP 区别”,我就把课本上的“可靠/不可靠、面向连接/无连接”背一遍。直到后来线上出过一次“服务器 TIME_WAIT 爆炸”的事故,我才真正去翻了 TCP 状态机——那之后我对 TCP/IP 的理解才算入了门。
这篇博客不打算画那种到处都能找到的分层彩色框图,而是从一个实际问题出发,把 TCP/IP 各层在真实场景中怎么工作、怎么排查、会踩哪些坑,梳理一遍。
一、模型先一句话说明白
OSI 七层:物理、数据链路、网络、传输、会话、表示、应用(谁背谁忘)
TCP/IP 四层:网络接口层、网络层、传输层、应用层(把 OSI 上三层合并了)
记不住没关系,记住两种协议就够了:
IP 是网络层,负责“找到对方”(路由)
TCP/UDP 是传输层,负责“怎么传数据”(可靠、端口)
下面我按从底往上的方式讲,因为排错时通常是从最底层的通断开始。
二、网络接口层(你基本不用操心,但得知道)
这一层就是以太网、WiFi、光纤等硬件和驱动。作为应用开发者或者运维,你遇到这层的问题通常就是:
网线没插:ip link show 看到 NO-CARRIER
网卡掉线:dmesg | grep eth0
混杂模式不对(抓包抓不到)
一个我踩过的坑:虚拟机桥接模式,宿主机换了 WiFi,虚拟机 IP 还在,结果 ping 不通外网。因为链路层的 MAC 地址表没更新,重启网络服务 systemctl restart networking 才解决。
工具:ethtool eth0、ip link、arp -a
三、网络层(IP、路由、分片)
3.1 IP 地址和子网——上篇博客详细说了,这里不重复
直接给一个排查命令:
ip addr show # 看 IP 和掩码
ip route show # 看路由表
ping -c 4 8.8.8.8
traceroute 8.8.8.8 # Linux 可能叫 tracepath
3.2 IP 分片——一个很多人忽视的坑
以太网 MTU 默认 1500 字节。如果应用层发了一个 2000 字节的 UDP 包,IP 层会分片。那问题来了:大多数防火墙会丢弃分片包,导致 UDP 通 TCP 不通。
我之前做 VoIP 时调整了语音包大小到 1400 以内,才解决“偶尔断断续续”。
检查 MTU 问题:
ping -M do -s 1472 8.8.8.8 # 1472 + 28(ICMP+IP头) = 1500
# 如果能通,MTU 就是 1500;如果提示需要分片,说明沿途 MTU 更小
永久改 MTU(别乱改,先确认路径 MTU):
ip link set dev eth0 mtu 1400
3.3 路由问题实例
有一次新加了一个子网,A 机器能 ping 通 B,但 B 不能 ping 通 A。看路由表发现 A 有回程路由,但 B 的默认网关指向错了。用 ip route get
ip route get 192.168.2.100
# 输出显示走哪个口、哪个网关
四、传输层(TCP 和 UDP 的相爱相杀)
这一层是面试重灾区,也是线上事故高发区。
4.1 UDP:什么都好,就是会丢
UDP 无连接,简单高效,适合 DNS、NTP、流媒体。但你得自己处理丢包、乱序。
我遇到的 UDP 坑:内网两台服务器之间用 UDP 做日志转发,千兆网络,压测时丢包 30%。后来发现是接收端处理太慢,socket 接收缓冲区满了。
调优:
# 查看和增大接收缓冲区(sysctl)
net.core.rmem_max = 134217728
# 程序里 setsockopt SO_RCVBUF
测试 UDP 丢包用 iperf3 -u:
iperf3 -c 10.0.0.2 -u -b 100M
4.2 TCP:号称可靠,但让我们痛苦的地方最多
三次握手和四次挥手(真不用背图)
三次握手:SYN → SYN-ACK → ACK
四次挥手:FIN → ACK → FIN → ACK
但实际排错中出现的问题是:
SYN 洪水攻击:服务器收到大量 SYN,占满连接队列
TIME_WAIT 太多:主动关闭连接的一方会进入 TIME_WAIT 状态,持续 2MSL(Linux 默认 60 秒)。短连接业务(比如老旧的 PHP+MySQL)会堆积几万个 TIME_WAIT,耗尽端口。
真实案例:我司一个 API 网关,压测时发现客户端端口不够用,报错 Cannot assign requested address。ss -ant | grep TIME-WAIT | wc -l 显示 2 万多。解决方法:
调整 net.ipv4.ip_local_port_range 扩大端口范围
开启 net.ipv4.tcp_tw_reuse(允许复用 TIME_WAIT 的端口)
更根本的:改成 HTTP 长连接(Keep-Alive)
查看 TCP 状态分布:
ss -tan | awk '{print $1}' | sort | uniq -c
粘包问题(TCP 流式传输的必然)
很多人刚用 socket 时发两次数据,收到时混在一起了。这不是 bug,TCP 是流式协议,没有消息边界。需要自己在应用层加分隔符或长度前缀。
我早期写一个游戏服务器,客户端发两个包,服务端 recv 一次全收进来了,解包错乱。后来每个包前加 4 字节长度头才解决。
TCP 的 Nagle 算法和延迟确认
两个经常打架的算法:Nagle(积攒小包再发)和延迟确认(等一会再回 ACK)。实时性要求高的应用(游戏、远程桌面)需要禁用 Nagle:
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
排查 TCP 性能问题:用 ss -ti 看 RTT、重传、拥塞窗口:
ss -ti
# 输出中可以看到 rtt、cwnd、ssthresh、retrans 等
4.3 常用端口查看命令
netstat -tulnp # t: TCP, u: UDP, l: listening, n: 数字地址, p: 进程
ss -tulnp # 比 netstat 更快
lsof -i :8080 # 谁占了 8080 端口
五、应用层(HTTP、DNS、HTTPS)
这一层你最熟,但 TCP/IP 相关的常见问题还是有几个。
5.1 DNS 解析问题
现象:能 ping 通 IP,但 curl 域名失败。
排查:
nslookup example.com
dig +trace example.com
# 检查 /etc/resolv.conf
坑:glibc 的 DNS 解析默认超时 5 秒,重试 2 次,总计可能 15 秒。如果你的应用有依赖 DNS 的地方(比如数据库连接用域名),一旦 DNS 慢,整个线程卡住。解决:改用异步 DNS(c-ares)或增大超时/减少重试。
5.2 HTTP 长连接和 TIME_WAIT
前面提到了。服务端如果主动关闭 Keep-Alive 连接,会产生大量 TIME_WAIT。建议让客户端主动关闭,或者调整 tcp_tw_reuse。
5.3 HTTPS 的额外握手延迟
HTTPS 除了 TCP 三次握手,还有 TLS 握手(至少 2 RTT)。对移动网络或者跨洋请求,延迟显著。解决方案:TLS 1.3 的 0-RTT、或者会话复用。
六、实战:用 tcpdump 和 Wireshark 排一个真实故障
有一次线上 API 偶尔超时,概率 5%。用 ab 压测不重现,业务自己测也不稳定。最后我抓包:
tcpdump -i eth0 -s 0 -w capture.pcap port 8080
# 压测几分钟,然后传到本机用 Wireshark 打开
Wireshark 里用统计 → 流量图,发现几个请求的响应时间长达 2 秒。展开 TCP 流,看到大量 TCP Dup ACK 和 TCP Retransmission。说明有丢包。再对照时间点查交换机日志,发现某个端口 CRC 错误过高——最后换了一根网线解决。
所以,TCP/IP 的很多问题,底层的丢包会导致上层 TCP 重传,表现为“慢”而不是“不通”。重传默认超时 200ms,指数退避,几次下来就几秒了。
tcpdump 常用过滤:
tcpdump host 192.168.1.100
tcpdump port 80
tcpdump 'tcp[tcpflags] & (tcp-syn) != 0' # 抓 SYN 包
七、常见问题 Q&A(真实被问过的)
Q1: 两台服务器 ping 通,但某个端口 telnet 不通?
A: 先看服务是否监听:netstat -tulnp | grep 端口。再看防火墙:iptables -L -n 或 firewall-cmd --list-all。还有可能是云平台的安全组没放开。
Q2: 为什么 traceroute 显示很多星号但仍然能通?
A: 中间路由器禁止了 ICMP 回包(比如做了控制平面保护),不影响数据转发。
Q3: UDP 比 TCP 快,那直播为什么不用 TCP?
A: 其实现在很多直播用 QUIC(基于 UDP 的可靠传输)。TCP 的队头阻塞问题在丢包率高时很要命,一个包丢了,后续数据都得等重传,导致画面卡顿。UDP + 前向纠错(FEC)可以容忍部分丢包。
Q4: 断开连接时,为什么是四次挥手而不是三次?
A: 因为 TCP 是双工的,每一端都要单独关闭。FIN 表示“我没有数据要发了”,但还能收。所以主动关闭方发 FIN,被动方回 ACK,然后被动方发 FIN,主动方回 ACK。
Q5: 服务端 keepalive 参数应该怎么设置?
A: 用 tcp_keepalive_time(空闲多久开始探测)、tcp_keepalive_intvl(探测间隔)、tcp_keepalive_probes(探测次数)。默认好像是 7200 秒,太长了。可以改到 600 秒。但更好的方式是在应用层加心跳。
八、日常排错的思维顺序
遇到网络问题,我一般按这个顺序查(从底向上):
1、本地链路:ip link、ethtool、网线灯闪不闪
2、IP 层:ping 网关、ping 远端、traceroute、ip route
3、传输层:ss -tlnp 看端口监听、telnet IP port 测连通、ss -ti 看 TCP 状态
4、应用层:curl -v、日志、业务超时配置
按这个顺序走下来,90% 的问题都能定位到层。
九、学习建议
把 《TCP/IP 详解 卷1》 的目录翻一遍,至少知道什么时候该看哪一章
装一个 Wireshark,抓自己电脑的上网包,看看三次握手、HTTP 请求、TLS 握手长什么样
用 nc -l 和 nc 自己建一个 TCP 连接,并用 ss 观察状态变化
别追求一次把所有细节都记住。你只需要知道:TCP/IP 各层分好工,遇到问题用对应工具去查那一层的指标。时间长了,那些状态名、参数名自然就刻在脑子里了。
最后吐槽一句:TCP 的拥塞控制算法都快变成“玄学”了,BBR、Cubic、Reno……如果你不是搞内核的,先学会看丢包率和 RTT 就够了。下次线上慢的时候,记得先 ss -ti,别上来就重启服务器。