【计算基础|网络12】TCP 抓包与经典边界问题
面试问你"三次握手",你能背;线上一个接口偶发超时、CLOSE_WAIT 堆积、连接池打满,你抓过包吗?这篇是工程实战篇------不讲教科书上的箭头图,直接上 tcpdump、Python socket、netstat,把五次真实事故复现给你看:怎么抓握手挥手、怎么复现重传、怎么把 TIME_WAIT 堆出来、拔网线时连接到底怎么死、以及 Keepalive 和 Keep-Alive 为什么是两回事。
环境:macOS 27、Python 3.14、
tcpdump 4.99.1(macOS 自带)、netstat/lsof。⚠️ macOS 上抓包必须用sudo(BPF 设备权限),下面命令请在你自己的终端里用 sudo 跑;本机非 root 环境用lsof/netstat/socket 自省替代观察状态。
一、tcpdump 入门:十个参数记住就够
结论先给:tcpdump 不是用来"看所有包"的,是用来"按条件筛包"的。过滤表达式写错,要么什么都抓不到,要么包多到刷屏。
1.1 高频参数表
| 参数 | 作用 | 例子 |
|---|---|---|
-i any |
抓所有网卡 | tcpdump -i any |
-i lo0 |
只抓回环(本地调试最常用) | tcpdump -i lo0 |
-nn |
不解析域名和端口名,直接显示 IP:端口 | 必加,慢死的元凶就是反查 |
-X |
同时打印十六进制和 ASCII | 看报文内容用 |
-vv |
详细输出(更多字段) | 排查时加 |
-c N |
抓够 N 个包就退出 | 写脚本必加 |
-w file.pcap |
原始包存文件,事后用 Wireshark 打开 | 留档取证 |
-r file.pcap |
读 pcap 文件 | 离线分析 |
-s 0 |
抓完整包(默认 snaplen 96 字节,包会被截断) | 抓完整头部必加 |
1.2 过滤表达式语法
过滤表达式是 tcpdump 的灵魂,记住下面这几个原词就够:
| 原词 | 含义 | 例子 |
|---|---|---|
host X |
主机 X | host 127.0.0.1 |
port N |
端口 N | port 8080 |
net 10.0.0.0/8 |
网段 | net 10.0.0.0 |
src X |
源地址是 X | src 10.0.0.1 |
dst X |
目的地址是 X | dst 10.0.0.1 |
and / or / not |
逻辑组合 | port 8080 and not src 10.0.0.1 |
tcp[tcpflags] & tcp-syn != 0 |
SYN 包 | 抓握手用 |
1.3 三条能直接抄的命令
bash
# ① 抓本机 8080 端口的所有包,不解析,抓 20 个就停
sudo tcpdump -i lo0 -nn -c 20 port 8080
# ② 只抓 SYN 和 FIN(看握手挥手)
sudo tcpdump -i lo0 -nn -c 20 'tcp port 8000 and (tcp[tcpflags] & tcp-syn != 0 or tcp[tcpflags] & tcp-fin != 0)'
# ③ 抓完整包存文件,事后拖到 Wireshark 看
sudo tcpdump -i lo0 -nn -s 0 -w demo.pcap port 9100
⚠️ 过滤表达式要用引号包起来 ,否则 shell 会把 (、)、! 当成 shell 语法解释掉,报错。
二、Wireshark 速查(操作向)
Wireshark 是图形版的 tcpdump,分析 pcap 文件时比命令行舒服十倍。记住四个用法就够:
| 功能 | 过滤语法 | 什么时候用 |
|---|---|---|
| 按端口过滤 | tcp.port == 8080 |
只看某个服务的包 |
| 只看 HTTP | http |
看应用层请求 |
| 只看 SYN 包 | tcp.flags.syn == 1 |
抓握手 |
| 只看重传 | tcp.analysis.retransmission |
排查丢包 |
| 追一条连接 | 右键某包 → Follow → TCP Stream | 把一次交互的所有包按顺序拼出来 |
| IO 图 | 统计 → IO Graph | 看带宽随时间的变化 |
着色规则(不同状态的包用不同颜色标出来)和专家信息(Expert Information,自动标红重传/零窗口等异常)是 Wireshark 最值钱的两个功能,别的工具替代不了。
三、实战 1:抓三次握手和四次挥手
3.1 实验设计
自己写一个 echo 服务端,再写一个客户端 connect → send → close。连接建立和关闭的过程就是标准的三次握手 + 四次挥手。
服务端 echo_server.py:
python
import socket
import threading
HOST, PORT = "127.0.0.1", 9100
def handle(conn, addr):
try:
data = conn.recv(1024)
conn.sendall(b"pong:" + data)
finally:
conn.close()
def main():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOST, PORT))
srv.listen(128)
print(f"echo 服务端 {HOST}:{PORT}")
while True:
conn, addr = srv.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
if __name__ == "__main__":
main()
3.2 抓包命令
另开一个终端,先起抓包,再起客户端:
bash
# 终端 A:先抓包(只看 9100 端口的 SYN/FIN/RST,抓 10 个就停)
sudo tcpdump -i lo0 -nn -c 10 'tcp port 9100 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)'
# 终端 B:起服务端
python3 echo_server.py
# 终端 C:发起一次连接
python3 -c "
import socket
s = socket.socket()
s.connect(('127.0.0.1', 9100))
s.sendall(b'hi')
print(s.recv(100))
s.close()
"
3.3 抓到的握手挥手长什么样(典型输出)
tcpdump 抓到的 SYN/SYN+ACK/ACK 三个包,典型长这样(字段顺序:时间 源 → 标志 flags 选项):
text
12:34:56.000000 IP 127.0.0.1.58118 > 127.0.0.1.9100: Flags [S], seq 1000, win 65535, options [mss 16344,...], length 0
12:34:56.000100 IP 127.0.0.1.9100 > 127.0.0.1.58118: Flags [S.], seq 2000, ack 1001, win 65535, options [mss 16344,...], length 0
12:34:56.000200 IP 127.0.0.1.58118 > 127.0.0.1.9100: Flags [.], ack 2001, win 65535, length 0
逐字段拆:
| 行 | 标志 | 含义 | SEQ/ACK 变化 |
|---|---|---|---|
| 1 | [S] |
客户端发 SYN,我要连接,我的初始序号是 1000 | seq=1000 |
| 2 | [S.] |
服务器回 SYN+ACK,同意连接,我的初始序号是 2000,确认你的 1001 | seq=2000, ack=1001 |
| 3 | [.] |
客户端 ACK 服务器,确认你的 2001 | ack=2001 |
🔴 重点看 SEQ/ACK 的增量:SYN 和 FIN 各占一个序号。所以客户端第一次发的 SEQ 是 1000,服务器回的 ACK 就是 1001(=1000+1);服务器自己的 SYN 是 2000,客户端回的 ACK 就是 2001。这个"+1"是面试和排错高频考点。
挥手时的四个包([F] 是 FIN,[.] 是 ACK):
text
客户端 > 服务器: Flags [F.], seq 3000, ack 2001 # 客户端说"我没数据要发了"
服务器 > 客户端: Flags [.], ack 3001 # 服务器说"知道了"
服务器 > 客户端: Flags [F.], seq 2001, ack 3001 # 服务器说"我也没数据了"
客户端 > 服务器: Flags [.], ack 2002 # 客户端说"知道了,拜拜"
为什么是四次?因为服务器收到客户端 FIN 时,可能还有数据没发完,所以先回 ACK 表示"你的我收到了",等自己数据发完再发 FIN。ACK 和 FIN 不能合并。
3.4 用 netstat 观察状态变化(不需要 sudo)
本机非 root 环境用 netstat 也能看到连接状态。连接建立瞬间跑:
bash
netstat -an -p tcp | grep 9100
实测一次 close 后看到的状态(客户端主动关):
text
tcp4 0 0 127.0.0.1.9100 127.0.0.1.58118 FIN_WAIT_2
tcp4 7 0 127.0.0.1.58118 127.0.0.1.9100 CLOSE_WAIT
tcp4 0 0 127.0.0.1.9100 *.* LISTEN
🔴 这两行是排错金矿:
- CLOSE_WAIT 出现在被动关闭方 。出现一堆 CLOSE_WAIT 说明你的应用没调 close()------recv 都返回了,代码忘了关 socket,操作系统就一直帮你停在 CLOSE_WAIT。
- FIN_WAIT_2 出现在主动关闭方,在等对方的 FIN。正常很快就过去;堆积说明对方没关连接。
四、实战 2:抓丢包和重传
4.1 怎么模拟丢包
Linux 用 tc netem 一行命令丢包;macOS 用 dnctl(需 sudo):
bash
# Linux:lo0 上丢 30% 的包
sudo tc qdisc add dev lo0 root netem loss 30%
# 抓包时重点看 tcp.analysis.retransmission(Wireshark 过滤)
4.2 重传的两种触发方式
TCP 重传不是只有一种,抓包能看到两种完全不同的现象:
| 类型 | 触发条件 | 抓包特征 | 速度 |
|---|---|---|---|
| 快速重传 | 收到 3 个重复 ACK(说明中间某个包丢了,但后面的包到了) | 立刻重传丢失的那个包,不用等超时 | 快(毫秒级) |
| 超时重传 | 超过 RTO(RTO 一般 200ms~几秒,会指数退避)还没收到 ACK | 重传最后一个未确认的包,RTO 翻倍 | 慢(指数退避) |
🔴 快速重传为什么要等 3 个重复 ACK 而不是 1 个? 因为 1 个重复 ACK 可能只是乱序到了,3 个重复 ACK 基本可以断定中间丢包了,不用傻等超时。
五、实战 3:复现大量 TIME_WAIT 并调参
这是线上最经典的事故之一:客户端(或调用方)大量短连接,TIME_WAIT 堆积到几万,端口耗尽,新连接报 Cannot assign requested address。
5.1 复现脚本
客户端循环 300 次:connect → send → recv → close。每次都是客户端主动关闭,所以 TIME_WAIT 出现在客户端这一端。
python
# short_conn_client.py
import socket
TARGET = ("127.0.0.1", 9100)
for i in range(300):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(TARGET)
s.sendall(b"ping")
s.recv(1024)
s.close()
print("完成 300 次短连接")
5.2 实测结果
先跑一次基线:
bash
netstat -an -p tcp | grep -c TIME_WAIT
# 19(系统本来就有的)
再跑 300 次短连接,立刻统计:
bash
python3 short_conn_client.py 300
netstat -an -p tcp | grep 9100 | grep TIME_WAIT | wc -l
✅ 实测输出:
text
完成 300 次短连接
323
300 次短连接,留下 323 个 TIME_WAIT(比 300 多一点是因为上一轮实验还没消散完)。抽几条看:
text
tcp4 0 0 127.0.0.1.9100 127.0.0.1.57757 TIME_WAIT
tcp4 0 0 127.0.0.1.9100 127.0.0.1.57758 TIME_WAIT
tcp4 0 0 127.0.0.1.9100 127.0.0.1.57759 TIME_WAIT
tcp4 0 0 127.0.0.1.9100 127.0.0.1.57760 TIME_WAIT
tcp4 0 0 127.0.0.1.9100 127.0.0.1.57761 TIME_WAIT
看右边那个端口:57757、57758、57759......每次 connect 都消耗一个临时端口。
5.3 TIME_WAIT 为什么有害
TIME_WAIT 要持续 2MSL 才消失(msl = 最长分节生命期)。实测 macOS 的 MSL:
bash
sysctl net.inet.tcp.msl
# net.inet.tcp.msl: 15000 (单位 ms,即 15 秒,2MSL = 30 秒)
⚠️ 危害:
- 端口耗尽:本机临时端口范围是有限的。macOS 默认:
bash
sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
# net.inet.ip.portrange.first: 49152
# net.inet.ip.portrange.last: 65535
总共才 65535 - 49152 = 16383 个临时端口。如果每秒建 1000 个短连接,30 秒的 TIME_WAIT 窗口里最多能扛 16383 / 30 ≈ 546 次/秒,超了就报 Cannot assign requested address。
- 内存占用:每个 TIME_WAIT 在内核里占一个 socket 结构,几万个就是几 MB,量大了也伤。
5.4 怎么调
| 手段 | 做什么 | 注意 |
|---|---|---|
| 用长连接 | 连接池复用,别每次都 connect/close | 治本,优先做 |
| 扩大端口范围 | 把 ip_local_port_range 往前调 |
治标,Linux: sysctl -w net.ipv4.ip_local_port_range="10240 65535" |
| tcp_tw_reuse(Linux) | 允许新连接复用 TIME_WAIT 的端口 | 需要 timestamps 打开;macOS 没有这个参数 |
| 服务端主动关 vs 客户端主动关 | 让 TIME_WAIT 出现在客户端而不是服务器 | 服务器端尽量被动关 |
🔴 核心结论 :TIME_WAIT 是设计来保护连接的(防止旧连接的迟到包被新连接当成自己的),不是 bug。正确做法是别让它产生(用长连接),而不是想办法把它干掉。
六、实战 4:拔网线/断电/崩溃对连接的影响
6.1 场景
TCP 连接建立后,一端直接拔网线(不发 FIN,断电)。对端完全不知道连接已经死了。
服务器 客户端 服务器 客户端 #mermaid-svg-FqPQoF6c0UqjvHsw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FqPQoF6c0UqjvHsw .error-icon{fill:#552222;}#mermaid-svg-FqPQoF6c0UqjvHsw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FqPQoF6c0UqjvHsw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FqPQoF6c0UqjvHsw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FqPQoF6c0UqjvHsw .marker.cross{stroke:#333333;}#mermaid-svg-FqPQoF6c0UqjvHsw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FqPQoF6c0UqjvHsw p{margin:0;}#mermaid-svg-FqPQoF6c0UqjvHsw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FqPQoF6c0UqjvHsw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FqPQoF6c0UqjvHsw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FqPQoF6c0UqjvHsw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FqPQoF6c0UqjvHsw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FqPQoF6c0UqjvHsw .sequenceNumber{fill:white;}#mermaid-svg-FqPQoF6c0UqjvHsw #sequencenumber{fill:#333;}#mermaid-svg-FqPQoF6c0UqjvHsw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FqPQoF6c0UqjvHsw .messageText{fill:#333;stroke:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FqPQoF6c0UqjvHsw .labelText,#mermaid-svg-FqPQoF6c0UqjvHsw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .loopText,#mermaid-svg-FqPQoF6c0UqjvHsw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FqPQoF6c0UqjvHsw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FqPQoF6c0UqjvHsw .noteText,#mermaid-svg-FqPQoF6c0UqjvHsw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FqPQoF6c0UqjvHsw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FqPQoF6c0UqjvHsw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FqPQoF6c0UqjvHsw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FqPQoF6c0UqjvHsw .actorPopupMenu{position:absolute;}#mermaid-svg-FqPQoF6c0UqjvHsw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-FqPQoF6c0UqjvHsw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FqPQoF6c0UqjvHsw .actor-man circle,#mermaid-svg-FqPQoF6c0UqjvHsw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FqPQoF6c0UqjvHsw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 连接正常建立 服务器突然断电/拔网线没发 FIN A 不知道 B 已死,继续发 重试 N 次还没 ACK 数据继续发数据...重传第 1 次(超时后)重传第 2 次应用层超时,报错关闭
6.2 应用层怎么感知连接死了
TCP 自带的 Keepalive 默认 2 小时才开始探测,根本等不起。真正线上做法:
| 机制 | 谁实现 | 探测间隔 | 灵敏度 |
|---|---|---|---|
| TCP Keepalive | 内核 | 默认 2 小时才开始发探测包 | 慢到没用 |
| 应用层心跳 | 自己写 | 30~60 秒 | 快,可定制 |
🔴 结论:别指望 TCP Keepalive 帮你发现死连接。要自己在应用层做心跳:每隔 N 秒发一个心跳包,连续 M 次没响应就判定死了,主动关闭并重连。
七、实战 5:Keepalive 与 Keep-Alive 的区别
这俩名字像,其实完全是两个东西,面试和工作中都有人搞混。
| 维度 | TCP Keepalive | HTTP Keep-Alive |
|---|---|---|
| 层级 | 传输层(TCP) | 应用层(HTTP) |
| 目的 | 探测连接是否还活着 | 复用 TCP 连接发多个 HTTP 请求 |
| 怎么做 | 连接空闲 N 小时后,发空 ACK 探测 | 一个 TCP 连接上顺序发多个 HTTP 请求 |
| 触发 | 内核自动 | 浏览器/客户端 Connection: keep-alive |
| 默认值 | 2 小时空闲才开始(实测 macOS:net.inet.tcp.keepidle: 7200000 ms = 2 小时) |
HTTP/1.1 默认开启 |
| 失败后果 | 连接被判定死了,关闭 | 换个连接重新发 |
实测 macOS 默认值:
bash
sysctl net.inet.tcp.keepidle net.inet.tcp.keepintvl net.inet.tcp.keepcnt
# net.inet.tcp.keepidle: 7200000 (空闲多久才开始探测,毫秒)
# net.inet.tcp.keepintvl: 75000 (探测包间隔,毫秒)
# net.inet.tcp.keepcnt: 8 (最多探测几次)
🔴 一句话区分 :Keepalive(TCP)是"看对方还活着没",Keep-Alive(HTTP)是"这条 TCP 连接别着急关,我还要发下一个请求"。一个管死活,一个管复用。
八、经典问题:TCP 到底丢不丢?
面试最爱问:"TCP 是不是保证不丢包?"标准答案:不是绝对不丢,是"连接正常的情况下保证按序、不丢、不重复"。
| 情况 | TCP 能保证吗 |
|---|---|
| 网络正常,连接还在 | ✅ 按序、不丢、不重复到达应用层 |
| 中间路由器短暂丢包 | ✅ 重传补上,应用层无感知 |
| 一端拔网线/断电/崩溃 | ❌ 已发但未 ACK 的数据可能真的丢了 |
| 中间 NAT 设备把表项清了 | ❌ 连接静默死了,双方都不知道 |
TCP 的"可靠"是尽力而为 + 通知:它会重传、会校验、会排序,但如果底层连接本身没了,它也变不出数据来。
🔴 应用层必须自己兜底:
- 业务确认机制:重要操作要有应用层 ACK,不能光靠 TCP。
- 重试 + 幂等:网络抖动时客户端要能安全重试(接口设计成幂等)。
- 事务/状态机:钱的事,别赌 TCP。
九、抓包分析速查表
线上抓包看到这些现象,按这张表查:
| 抓包看到 | 可能原因 | 解决方向 |
|---|---|---|
大量 [S] 但没有 [S.] |
目标没起 / 防火墙挡了 / backlog 满了 | 查服务是否监听、查防火墙、调 somaxconn |
大量 [S.] 但没有第三个 [.] |
客户端到服务器的回程路不通 / 防火墙丢包 | 查回程路由、查 conntrack 表满 |
大量 [.] 但数据卡住 |
对端窗口为 0(零窗口) | 接收方应用读太慢,查消费端 |
| 重复 ACK 三连 | 中间丢包 | 快速重传会救,持续丢查链路 |
| RTO 重传多 | 链路质量差 / 跨机房延迟高 | 查丢包率、调 RTO |
| 大量 TIME_WAIT | 短连接太多 | 上连接池、调端口范围 |
| 大量 CLOSE_WAIT | 应用忘了 close() | 查代码,谁没关 socket |
| 大量 FIN_WAIT_2 | 对方不关闭连接 | 查对方是不是崩溃/阻塞 |
| RST 突然出现 | 连接被强制重置(进程崩、防火墙杀) | 查对端日志、查防火墙 |
总结:你真正需要记住的 8 件事
- tcpdump 三剑客 :
-i lo0指定接口、-nn不解析、port X过滤端口,再加-c限量。 - 抓握手看 SYN/SYN+ACK/ACK,SEQ 各 +1(SYN 和 FIN 各占一个序号)。
- 四次挥手不能合并:因为服务器收到 FIN 时可能还有数据没发完。
- TIME_WAIT 出现在主动关闭方 ,持续 2MSL,短连接太多会把端口耗尽------用长连接治本。
- CLOSE_WAIT 堆积 = 你代码忘了 close(),不是网络问题。
- 快速重传靠 3 个重复 ACK,超时重传靠 RTO 指数退避,抓包特征完全不同。
- TCP Keepalive(2 小时)和 HTTP Keep-Alive(连接复用)是两码事,别搞混。
- TCP 不是绝对不丢:连接断了就是断了,应用层要自己做心跳、重试、幂等。
验证清单
- 能不查资料写出"抓 8080 端口、不解析、抓 10 个包"的 tcpdump 命令
- 能说清 SYN/SYN+ACK/ACK 三个包各自的 SEQ 和 ACK 是多少
- 能复现 300 次短连接产生大量 TIME_WAIT
- 能说出本机临时端口范围是多少、耗尽会报什么错
- 看到一堆 CLOSE_WAIT 能第一反应"应用没 close"
- 能区分快速重传和超时重传的触发条件
- 能一句话说清 TCP Keepalive 和 HTTP Keep-Alive 的区别
- 能说清为什么 TCP 不是"绝对不丢"
参考资源
- tcpdump 官方手册 https://www.tcpdump.org/manpages/tcpdump.1.html
- Wireshark 官方用户指南 https://www.wireshark.org/docs/wsug_html_chunked/
- TCP 状态图(RFC 793 附录)https://www.rfc-editor.org/rfc/rfc793
- Linux man 7 tcp(tcp(7) 手册页,含所有 TCP_* socket 选项)https://man7.org/linux/man-pages/man7/tcp.7.html
- Wiresarking 官方:TCP 重传分析 https://wiki.wireshark.org/TCP Analysis
- 腾讯云:TIME_WAIT 过多问题排查 https://cloud.tencent.com/developer/article/1497122
下一篇《【计算基础|网络13】UDP 与可靠传输》讲:UDP 首部为什么只有 8 字节、UDP 为什么不可靠但还满世界在用、怎么在 UDP 上自己实现可靠传输(QUIC 的思路)、以及"同一个端口能不能同时被 TCP 和 UDP 监听"这个经典面试题。