第24章 网络编程调试与抓包:gdb调试网络程序、strace跟踪系统调用、tcpdump抓包、Wireshark分析
说实话,写网络程序最头疼的不是写代码,而是出了问题不知道怎么查。我见过太多同事,代码跑起来就万事大吉,一遇到连接超时、数据乱码、内存泄漏,立马抓瞎。今天我就把这几年积累的调试经验,一股脑倒给你。
调试网络程序,说白了就四个工具:gdb 看程序内部状态,strace 看系统调用,tcpdump 抓原始包,Wireshark 做深度分析。这四个工具配合使用,基本能解决99%的网络问题。
核心思路:从应用层到内核层,再到网络层,逐层排查。别一上来就抓包,先看看程序本身有没有问题。
24.1 gdb 调试网络程序
gdb 调试网络程序,跟调试普通程序没什么两样。但有几个点特别要注意:多线程、信号处理、断点设置。我个人习惯在调试网络程序时,先把 SIGPIPE 信号屏蔽掉,不然客户端一断开,程序就挂了,你连断点都来不及看。
调试技巧:用 catch syscall 捕获系统调用,比如 catch syscall connect 可以在 connect 调用时自动断住。
// 示例:一个简单的 TCP 服务器,故意留个 bug
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int opt = 1;
char buffer[1024] = {0};
// 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 SO_REUSEADDR
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 监听
listen(server_fd, 3);
printf("Server listening on port 8080...\n");
// 接受连接
int addrlen = sizeof(address);
client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
// 这里故意写错:读取前没有检查返回值
read(client_fd, buffer, 1024);
printf("Received: %s\n", buffer);
// 忘记关闭 client_fd
close(server_fd);
return 0;
}
用 gdb 调试这个程序时,我会这样操作:
# 编译时加 -g 选项
gcc -g -o server server.c
# 启动 gdb
gdb ./server
# 设置断点
(gdb) b main
(gdb) b accept
(gdb) b read
# 运行
(gdb) r
# 查看变量
(gdb) p server_fd
(gdb) p client_fd
# 查看堆栈
(gdb) bt
# 单步执行
(gdb) n
(gdb) s
注意:调试网络程序时,如果程序 fork 了子进程,记得用 set follow-fork-mode child 让 gdb 跟踪子进程。我曾经在这上面浪费了半天时间,断点打上了就是不触发,后来才发现是跟踪了父进程。
24.2 strace 跟踪系统调用
strace 是我排查网络问题的首选工具。它能看到程序到底调用了哪些系统调用,参数是什么,返回值是什么。说白了,就是给程序装了个监控摄像头。
我记得有一次,一个同事说他的客户端连不上服务器,代码看了好几遍都没问题。我用 strace 一跑,发现 connect 返回了 -1,errno 是 ECONNREFUSED。再一看,服务器根本没启动。这种问题,看代码是看不出来的。
# 基本用法
strace -f -o trace.log ./server
# 只跟踪网络相关的系统调用
strace -e trace=network -f ./server
# 显示时间戳
strace -t -f -o trace.log ./server
# 显示相对时间
strace -r -f -o trace.log ./server
strace 输出示例:
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 3) = 0
accept(3, {sa_family=AF_INET, sin_port=htons(54321), sin_addr=inet_addr("192.168.1.100")}, [16]) = 4
read(4, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", 1024) = 37
write(1, "Received: GET / HTTP/1.1\r\nHost: ", 31) = 31
关键点:strace 的输出中,= 3 表示返回值是 3,= -1 表示出错。如果看到 EINPROGRESS,说明是非阻塞 connect 正在建立连接,这是正常的。
24.3 tcpdump 抓包
tcpdump 是命令行下的抓包工具,功能强大但参数复杂。我平时用的最多的就几个组合:
# 抓取所有经过 eth0 的包
tcpdump -i eth0
# 抓取特定端口的包
tcpdump -i eth0 port 8080
# 抓取特定主机的包
tcpdump -i eth0 host 192.168.1.100
# 抓取 TCP 三次握手
tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'
# 保存到文件,方便用 Wireshark 分析
tcpdump -i eth0 -w capture.pcap port 8080
# 从文件读取
tcpdump -r capture.pcap
tcpdump 输出示例:
13:45:23.123456 IP 192.168.1.100.54321 > 192.168.1.1.8080: Flags [S], seq 1234567890
13:45:23.123457 IP 192.168.1.1.8080 > 192.168.1.100.54321: Flags [S.], seq 987654321, ack 1234567891
13:45:23.123458 IP 192.168.1.100.54321 > 192.168.1.1.8080: Flags [.], ack 987654322
实用技巧:用 -X 参数可以同时显示 ASCII 和十六进制内容,方便查看应用层数据。比如 tcpdump -X -i eth0 port 8080。
24.4 Wireshark 深度分析
Wireshark 是图形化的抓包分析工具,比 tcpdump 直观得多。但说实话,很多人只会用 Wireshark 看个大概,真正会用的人不多。
我常用的几个 Wireshark 功能:
- 过滤表达式:
tcp.port == 8080、ip.addr == 192.168.1.100、tcp.flags.syn == 1 - 跟踪 TCP 流:右键 -> Follow -> TCP Stream,可以看完整的应用层数据
- 统计功能:Statistics -> Flow Graph,看连接建立和关闭的过程
- 专家分析:Analyze -> Expert Info,自动标记异常包
举个例子,我曾经遇到一个性能问题:客户端发送数据后,服务器要等很久才响应。用 Wireshark 一看,发现 TCP 窗口大小一直在变小,最后变成了 0。这说明接收端处理不过来,导致发送端被阻塞。后来优化了接收端的处理逻辑,问题就解决了。
注意:Wireshark 抓包时,如果网卡流量很大,建议用 tcpdump 先抓取过滤后的数据,再用 Wireshark 分析。不然 Wireshark 可能会因为内存不足而崩溃。我吃过这个亏,抓了 2GB 的包,Wireshark 直接卡死了。
24.5 实战:排查连接超时问题
假设你写了一个客户端,连接服务器时总是超时。你会怎么排查?
- 先看程序:用 gdb 调试,确认 connect 调用是否正确,参数是否传对了
- 再看系统调用:用 strace 跟踪,看 connect 返回了什么错误码
- 然后抓包:用 tcpdump 抓取网络包,看 SYN 包有没有发出去,有没有收到 SYN-ACK
- 最后分析:用 Wireshark 看 TCP 三次握手的过程,确认是哪一步出了问题
我遇到过最奇葩的一个问题:客户端能 ping 通服务器,但 connect 就是超时。用 tcpdump 一看,SYN 包发出去了,服务器也回了 SYN-ACK,但客户端没有回 ACK。最后发现是客户端的防火墙把 ACK 包给丢了。这种问题,不看包根本想不到。
总结:调试网络程序,要养成「从内到外」的排查习惯。先看程序本身,再看系统调用,最后看网络包。别一上来就抓包,那样反而容易迷失在大量的数据中。
公众号:蓝海资料掘金营,微信deep3321