文章目录
-
- [一、tcpdump 是什么](#一、tcpdump 是什么)
- 二、快速安装
- 三、命令核心结构
- 四、必知必会的选项
- [五、核心:BPF 过滤表达式 (重中之重)](#五、核心:BPF 过滤表达式 (重中之重))
-
- [1. 常用原语](#1. 常用原语)
- [2. 组合示例](#2. 组合示例)
- 六、输出格式精读
- 七、C++后端实战场景
-
- [1. 最简单的调试:本机回环请求](#1. 最简单的调试:本机回环请求)
- [2. 确认完整HTTP请求/响应](#2. 确认完整HTTP请求/响应)
- [3. 排除健康检查或心跳噪声](#3. 排除健康检查或心跳噪声)
- [4. 分析 TCP 连接生命周期](#4. 分析 TCP 连接生命周期)
- [5. 调试自定义二进制协议](#5. 调试自定义二进制协议)
- [6. 确认客户端真实IP是否可达](#6. 确认客户端真实IP是否可达)
- [7. 观察重传和窗口问题](#7. 观察重传和窗口问题)
- 八、进阶技巧
- 九、注意事项(后端程序员要避的坑)
- [十、什么时候会真正打开 tcpdump(感悟与场景)](#十、什么时候会真正打开 tcpdump(感悟与场景))
-
- [什么时候该请出 tcpdump](#什么时候该请出 tcpdump)
- 为什么现在觉得"用不到"
- 一个简单的比喻
- 现在可以做什么,让以后用起来更顺畅?
- [tcpdump 的"盲区"](#tcpdump 的“盲区”)
- 十一、关于业务日志和tcpdump是否冲突?
-
- 一张表看清区别
- 为什么必须两者共存?三个经典场景告诉我们
-
- [场景一:日志说发了,tcpdump 说没发 (互斥时,tcpdump 更权威)](#场景一:日志说发了,tcpdump 说没发 (互斥时,tcpdump 更权威))
- [场景二:日志没记,但 tcpdump 抓住了 (日志有盲区)](#场景二:日志没记,但 tcpdump 抓住了 (日志有盲区))
- 场景三:互相印证,缩小范围 (组合拳)
- [核心思想:tcpdump 是用来否决我们的日志,或者为我们的日志背书的](#核心思想:tcpdump 是用来否决我们的日志,或者为我们的日志背书的)


一、tcpdump 是什么
- 本质 :一个基于
libpcap的命令行抓包工具,能够捕获并解析网络接口上收发的数据包。 - 核心原理 :
- 将网卡设置为混杂模式,接收所有流经该接口的数据包(不限于发给本机的包)。
- 在内核层 使用 BPF (Berkeley Packet Filter) 过滤规则,只把用户关心的包复制到用户态,极大减少性能开销和丢包。
- 与后端开发的关系:当我们怀疑网络问题,但代码和日志查不出时,它就是最终的"证据"------可以看到三次握手是否完成、HTTP请求体是否发送完整、对端是否回了RST等。
二、快速安装
bash
# Debian/Ubuntu
sudo apt-get install tcpdump
# CentOS/RHEL
sudo yum install tcpdump
注意 :运行 tcpdump 通常需要 root 权限,或给用户赋予 CAP_NET_RAW 能力。
macOS 用户注意 :macOS 自带的 tcpdump 与 Linux 下的行为基本相同,但在接口名称、权限模型和默认捕获长度上略有差异。例如,无线网卡通常是
en0,且需要sudo;默认快照长度可能更大,但为了安全仍建议使用-s 0。此外,macOS 中-i any不受支持,必须指定具体接口。
三、命令核心结构
bash
tcpdump [选项] [过滤表达式]
选项控制显示和抓取行为,表达式决定抓什么。
四、必知必会的选项
| 选项 | 作用 | 说明 |
|---|---|---|
-i eth0 |
指定监听的网络接口 | 任何接口用 -i any;回环接口用 -i lo |
-n |
不解析主机名 | 直接显示 IP,避免 DNS 查询干扰和延迟 |
-nn |
不解析主机名和端口名 | 直接显示 127.0.0.1.8080,不显示 localhost.hobart |
-v / -vv / -vvv |
详细程度 | -v 多 TTL/ID;-vv 多窗口/选项;-vvv 极其详细 |
-c 100 |
抓取 100 个包后自动停止 | 避免在生产环境无限抓取 |
-w file.pcap |
把原始包保存到文件 | 不解析内容,直接写二进制,效率高 |
-r file.pcap |
从文件中读取包并解析 | 相当于离线分析,方便反复筛选 |
-s 0 |
抓取完整数据包,不做截断 | 默认可能只抓 68/96 字节,对分析应用层数据必须用 -s 0 |
-X |
同时以十六进制和 ASCII 显示包内容 | 调试自定义二进制协议时最常用 |
-A |
只以 ASCII 文本显示包内容 | 适合抓 HTTP 请求这样的文本协议 |
-e |
显示链路层头部信息 (MAC 地址) | 排查 ARP 或交换机问题时用 |
-t |
不显示时间戳 | 精简输出 |
-tttt |
显示人类可读的日期时间 | 2026-08-11 10:30:00.123456 |
五、核心:BPF 过滤表达式 (重中之重)
过滤表达式可看成组合多个原语 ,用 and、or、not 连接,支持括号分组。
1. 常用原语
- 主机 :
host 10.0.0.5、src host 10.0.0.5、dst host 10.0.0.5 - 端口 :
port 80、src port 8080、dst port 3306 - 端口范围 :
portrange 8000-8100 - 网络 :
net 192.168.1.0/24 - 协议 :
tcp、udp、icmp、arp - 指定 TCP 标志位 :(常用)
- 只抓 SYN 包(连接建立第一个包):
'tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0' - 只抓 SYN+ACK 包:
'tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) != 0' - 只抓 RST 包:
'tcp[tcpflags] & (tcp-rst) != 0'
- 只抓 SYN 包(连接建立第一个包):
- 包长度 :
greater 100(包长大于 100 字节),less 64
2. 组合示例
bash
# 来自10.0.0.5且目标端口80的TCP包
tcpdump -nn 'src host 10.0.0.5 and tcp dst port 80'
# 主机A和主机B之间的所有UDP流量
tcpdump -nn 'host 10.0.0.5 and host 10.0.0.6 and udp'
# 不是SSH(22)端口的流量,括号和引号很重要
tcpdump -nn 'not port 22'
# 抓取发往本机8080端口,且包长度大于500的SYN包
tcpdump -nn 'tcp dst port 8080 and greater 500 and tcp[tcpflags] & tcp-syn != 0'
六、输出格式精读
一条典型的 TCP 输出行:
text
08:10:20.123456 IP 192.168.1.10.54321 > 192.168.1.20.8080: Flags [S], seq 1000, win 65535, options [mss 1460,sackOK,TS val 123 ecr 0,nop,wscale 7], length 0
分解如下:
- 时间戳 :
08:10:20.123456 - 协议 :
IP - 源与目标 :
源IP.源端口 > 目标IP.目标端口 - Flags :这是重点
[S]:SYN (连接请求)[S.]:SYN+ACK (连接应答)[.]:ACK (确认,单个点)[P.]:PSH+ACK (数据推送)[F.]:FIN+ACK (连接结束请求)[R]/[R.]:RST (重置连接)
- seq :当前包序列号,
seq 1000。- 后续包如果带数据,会显示
seq 1001:1500,表示该包携带了序号1001到1499的字节。
- 后续包如果带数据,会显示
- ack:期望收到的下一个序列号,仅当ACK标志置位时显示。
- win:窗口大小,流量控制依据。
- options :TCP选项,如
mss最大报文段长度、wscale窗口缩放因子等,排查窗口问题时很有用。 - length :有效载荷长度,此处的
0代表纯TCP头(如SYN包)。
七、C++后端实战场景
1. 最简单的调试:本机回环请求
调试本机两个微服务(如一个C++服务调用另一个的8080端口):
bash
tcpdump -i lo -nn 'port 8080'
我们会看到请求和响应包流,立即能判断是请求没发出,还是对端没回应。
2. 确认完整HTTP请求/响应
抓取并显示整个HTTP请求内容(文本):
bash
tcpdump -i eth0 -nn -A -s 0 'tcp port 80 and host 10.0.0.5'
-A 会打印出 GET /api/... 和 HTTP头,甚至响应体。如果乱码,记得加上 -X 看十六进制。
3. 排除健康检查或心跳噪声
我们的服务端口是8080,但编排系统每分钟都来健康检查(来自固定IP 10.0.0.1),想忽略它:
bash
tcpdump -nn -i eth0 'port 8080 and not src host 10.0.0.1'
4. 分析 TCP 连接生命周期
抓取一次完整的 TCP 流(只抓握手和挥手包):
bash
tcpdump -i eth0 -nn 'tcp port 8080 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)'
观察有没有大量 RST 返回,或者挥手过程缺了 FIN。
5. 调试自定义二进制协议
用 -X 打印十六进制和ASCII内容,并保存为 pcap 文件,再用 Wireshark 写 C++ 一样的协议解析器(自定义 Lua/C 插件)去分析。
bash
tcpdump -i eth0 -w debug.pcap -s 0 'port 33000'
6. 确认客户端真实IP是否可达
C++ 后端获取到客户端IP后,要反连客户端提供某些服务?先抓包看看有没有来自该 IP 的包到达网卡:
bash
tcpdump -i eth0 -nn 'host 1.2.3.4'
7. 观察重传和窗口问题
bash
tcpdump -i eth0 -nn -vv 'port 8080 and (tcp[tcpflags] & tcp-syn == 0)'
加上 -vv 可以清楚看到窗口大小变化、是否有重传 (seq 1000 重复出现) 等现象。
八、进阶技巧
- 循环保存文件 :
-W 5 -C 100 -w file.pcap会循环使用5个文件,每个最大100MB,避免撑爆磁盘。 - 精细化时间戳 :在
-tttt的基础上,某些版本支持--time-stamp-precision=nano获得纳秒时间。 - 配合 Wireshark 解密 TLS :
tcpdump 本身不解密,但可以设置环境变量SSLKEYLOGFILE=~/sslkeylog.txt并启动我们的 C++ 程序(程序需使用 OpenSSL/BoringSSL 等且不覆盖该钩子),然后用 tcpdump 抓包,在 Wireshark 中导入该密钥文件即可看到明文。这对调试 C++ 写的 HTTPS/gRPC 服务至关重要。 - 管道直接输出给其他工具:
bash
tcpdump -i eth0 -w - not port 22 | nc remote_host 12345
可以把包流式传输到远端分析(小心带宽)。
- 结合
strace:若怀疑 C++ 程序在系统调用层就有问题,可以strace -e trace=network ./your_server的同时,抓包对比 tcpdump 的结果,确认数据是在发送前还是发送后出错。
九、注意事项(后端程序员要避的坑)
- 生产环境抓包务必谨慎 :
- 一定要加严格过滤器,例如限定端口和主机。
- 使用
-c限制数量,或用-W-C限制文件大小。 - 大量抓包会占用 CPU,且磁盘 I/O 可能影响服务响应。
- 注意截断 :
-s 0是好习惯,否则应用层数据不全,我们很可能误判。 - 权限问题 :
- 标准用户必须使用
sudo。 - 在容器 (Docker) 中,默认可能无法使用 tcpdump,需要赋予
NET_ADMIN能力。
- 标准用户必须使用
- 时间同步 :
- 对比多台机器抓的包时,务必确保机器时间已经 NTP 同步,否则顺序错乱。
- 接口选择 :
-i any会抓所有接口,但无法用于向 pcap 文件写入(某些系统),且混杂模式表现可能不同。调试具体服务时,明确指定-i eth0或-i lo更好。
- 区分进出 :
- 过滤器
src和dst是相对于监控的接口而言。如果我们在服务端机器上抓-i eth0,进来的请求是dst host 本机IP,出去的响应是src host 本机IP。
- 过滤器
十、什么时候会真正打开 tcpdump(感悟与场景)
有一次,我在写一个个人项目,一个简单的 TCP 服务在测试时突然开始间歇性拒绝连接。端口监听正常,但总有大约 5% 的新连接直接失败,客户端收到 "Connection reset by peer"。查看应用日志,发现只有少数连接建立成功,大部分请求根本没进入业务逻辑。我检查了文件描述符限制、线程池状态、系统内存,一切正常。那种感觉就像程序在闹鬼。
后来我在服务器上跑了这样一条命令:
bash
tcpdump -i eth0 -nn 'tcp port 8080 and (tcp[tcpflags] & (tcp-syn|tcp-rst) != 0)'
不到 10 秒,谜底揭晓:每个失败的连接都是三次握手完成之后,服务端立刻回了一个 RST 包。进一步对比正常连接发现,这些 RST 均来自一个特定的工作线程,而那个线程刚好在那几分钟内因为一个死锁卡住了 accept 调用。它没有崩溃,端口也还在监听,但新连接分配到它时,它根本无法处理,内核替它回了 RST。日志之所以干净,是因为连接根本没走到应用层。没有 tcpdump,我可能还在傻傻地调内核参数。
这次经历让我们深刻体会到:当所有日志都沉默时,网络包不会撒谎。
什么时候该请出 tcpdump
作为 C++ 后端开发,我们打交道最多的是网络编程(socket、epoll、TCP 连接、自定义协议、HTTP/gRPC 等)。下面这些情况,tcpdump 会直接派上用场:
-
"我明明发了数据,对端说没收到"
- 我们调用
send()成功了,对端却说没收到包。 - 这时打开 tcpdump,一看便知:数据包到底出没出本机网卡?如果抓到了发出的包,说明是网络或对端的问题;如果没抓到,说明我们的程序根本没发出去,或者被本机防火墙拦截了。
- 我们调用
-
TCP 连接异常断开(突然被 RST)
- 线上服务日志频繁打印 "Connection reset by peer"。
- 我们抓包看到对端回了一个
RST标志的包,结合上下文可能发现:是对端进程重启了?还是请求了一个已经关闭的端口?或者防火墙发了 RST?没有 tcpdump 我们只能猜。
-
怀疑粘包/拆包问题
- 我们写了一个自定义二进制协议,接收端有时解析出错。
- 用
tcpdump -X抓下数据包的实际十六进制内容,就能看到 TCP 流里,我们的数据是不是被分成了好几个包发出,或者粘在一起,从而验证我们的封包解包逻辑是否正确。
-
延迟突然变高,但代码里没加计时器
- 服务间调用从 5ms 变成了 200ms,不知道是网络问题还是业务逻辑变慢。
- tcpdump 抓包并保存为 pcap 文件,用 Wireshark 打开,可以直接看到每个请求和响应之间的时间差,精确定位是网络传输慢还是服务器处理慢(三次握手的时间、请求发出到第一个响应字节的时间,都一目了然)。
-
学习 TCP 协议本身时
- 自己写一个简单的 socket 程序,故意不调用
close()就退出,然后用 tcpdump 观察对端是否收到 RST。 - 调整系统
tcp_keepalive参数,抓包验证 keep-alive 探测包是否如预期发出。 - 这些动手实验能让我们对 TCP 状态机、三次握手四次挥手的理解"刻进骨头里"。
- 自己写一个简单的 socket 程序,故意不调用
-
容器/微服务排查
- 服务部署在 Docker 里,
curl localhost:8080超时。可能是端口没映射?进程没监听?还是 iptables 规则拦截了? tcpdump -i any port 8080能直接看到 SYN 包有没有到达网卡,从而准确判断问题层面。
- 服务部署在 Docker 里,
为什么现在觉得"用不到"
- 我们现在多数是在本机写代码,简单的客户端/服务端通信,一切都在掌控中。
- 真正的网络问题往往出现在多机协作、复杂网络环境、高并发或生产环境下,这些问题在个人学习阶段较少遇到。
- tcpdump 是一个"事后侦探",不像 GDB 调试那样需要我们主动介入,它是网络真出"悬案"时才请出来的。
一个简单的比喻
- printf 日志 相当于我们装在家里的监控,平时记录日常。
- tcpdump 相当于我们在房子周围装的高精度传感器,只有怀疑有人偷东西(网络丢包、攻击)或者电路异常(TCP 断开)时,我们才会去看它的详细记录。
现在可以做什么,让以后用起来更顺畅?
即使暂时用不到,也可以提前做一次"排练":
-
用 C++ 写一个简单的 TCP echo 服务器和客户端。
-
在本机运行,然后开一个终端执行:
bashsudo tcpdump -i lo -nn port 你的端口 -
观察连接建立时的
SYN、SYN+ACK、ACK,发送数据时的PSH、ACK,以及关闭时的FIN序列。
这样一次亲手实践,比看十次笔记都管用,而且会让我们对以后排查网络问题充满信心。
等到那天我们真面对一个顽固的线上网络故障,其他人都一筹莫展时,我们默默敲下 tcpdump 命令,看到那个关键的 RST 包,一切豁然开朗------那种感觉,就是后端工程师的浪漫。
tcpdump 的"盲区"
也要知道 tcpdump 看不到什么:
- 本机防火墙 (iptables) 影响:tcpdump 的抓包点位于网络协议栈中较早期的 AF_PACKET 层,直接捕获网卡收发的原始帧。这意味着,即使 iptables 后续在内核中将包丢弃,tcpdump 通常还是能抓到这些包(因为捕获发生在过滤之前)。不过,对于本地发出的某些包,不同 hook 点的处理顺序可能导致盲区,但绝大多数情况下,我们能"看到"被防火墙拦掉的包。这一点在排查"明明对方说发过来了,但我的服务没收到"时格外有用------如果 tcpdump 上都没出现这个包,那问题基本不在本机防火墙,而是更上游的网络或对端。
- 应用层协议细节 :tcpdump 能显示原始字节,但如果我们不熟悉 HTTP 协议格式,我们只能看到乱码,还需要用
-A或 Wireshark 解析。 - 浏览器渲染过程:完全在 tcpdump 视野之外。
十一、关于业务日志和tcpdump是否冲突?
这是一个非常好的问题,说明我们开始思考不同工具在开发流程中的定位了。
答案很明确:不冲突,它们是不同层面的东西,解决的是完全不同的问题。
我们可以把它们理解为:业务日志是我们的"自述",tcpdump 是"监控录像"。 一个是"我说我做了什么",一个是"实际发生的客观事实"。
一张表看清区别
| 维度 | 业务日志 (我们在代码里写的 printf/LOG) |
tcpdump (网络抓包) |
|---|---|---|
| 负责回答的问题 | "程序逻辑是否正确?" 为什么请求被拒绝了?这个变量值是多少?代码走到这行了没? | "网络通信是否发生?" 包发出去了吗?对方回了吗?连接断了吗? |
| 视角 | 程序内部视角,只记录程序自己认为发生过的事情。 | 操作系统/网络视角,记录网络上真实流过的比特。 |
| 谁"写"的 | 我们写的 LOG_INFO("用户搜索: %s", keyword) |
内核的网络协议栈和网卡驱动。 |
| 它"说谎"吗 | 会 。程序在调用 send() 前崩溃了,日志就不会有这条记录;或者我们的 if/else 分支根本没进,导致日志逻辑被跳过。 |
基本不会。只要包经过了网卡,它就能抓到。它代表了物理层真实发生的通信。 |
| 关联性 | 我们有意识地把它和业务逻辑绑在一起,是"业务状态"的体现。 | 它完全不懂业务,只看 IP、端口、TCP 标志位和原始字节。 |
为什么必须两者共存?三个经典场景告诉我们
场景一:日志说发了,tcpdump 说没发 (互斥时,tcpdump 更权威)
- 我们的日志 :
[INFO] 成功发送 256 字节响应给客户端 - 实际情况 :程序在调用
write()之前就宕机了,或者我们的LOG语句放在write()之前,而write()本身失败了但我们没检查返回值。 - tcpdump 的作用 :我们在线上抓包,根本没看到服务器发出的响应包。铁证如山,是我们的程序发送逻辑出了问题。 日志骗了我们。
场景二:日志没记,但 tcpdump 抓住了 (日志有盲区)
- 现象:服务器频繁告警 "Connection reset by peer",但我们的日志里找不到任何对应客户端的请求记录。
- tcpdump 的作用 :抓包一看,某个客户端 IP 发来了一个
RST包,直接重置了连接。也许是我们设置的keepalive_timeout太短,或者是中间防火墙干的。这些底层的网络事件,我们的业务日志根本感知不到,但 tcpdump 一清二楚。
场景三:互相印证,缩小范围 (组合拳)
- 问题:前端说请求超时。
- 我们的排查步骤 :
- 看日志 :
[NORMAL] 用户搜索:boost→ 说明业务层收到了请求,代码逻辑执行到了。 - 看 tcpdump :抓到
[P.]响应包,length和我们生成的响应体大小一致。→ 说明响应也成功发出了。 - 结论 :问题不在后端。 立刻去前端或反向代理层排查。
- 看日志 :
- 这背后是我们的决策逻辑 :
- 如果日志有,抓包无 → 后端发送环节出 Bug。
- 如果日志无,抓包有 → 请求进来了,但后端处理异常 (崩溃/未进分支)。
- 如果日志有,抓包有 → 后端工作完美,问题在别处。
核心思想:tcpdump 是用来否决我们的日志,或者为我们的日志背书的
它不是用来替代日志的。我们永远会先看日志,因为日志有业务上下文。
- tcpdump 是最后的测谎仪 。当故障原因扑朔迷离,各方开始"甩锅"------前端说后端没返回,后端说代码没问题,运维说网络不通时------tcpdump 抓出来的二进制数据包是唯一不会说谎的、端到端的证据。
- 日志是我们业务系统的内脏监控,告诉我们消化好不好。
- tcpdump 是马路上装的监控摄像头,告诉我们人到底进没进门,出没出门。
所以,我们写业务时的日志不仅没有和 tcpdump 冲突,而且我们正在建立一种非常专业的后端思维:用业务日志监控内部状态,用 tcpdump 监控外部通信,两者一合,天下无敌。