计算机网络实验里,抓包分析是理解协议最直接的手段之一。标题里的“Ethereal”可能很多新同学不认识,它是 Wireshark 的前身,后来改名后功能一脉相承,过滤语法和界面布局基本没变。这套实验我反复带过几轮:先用脚本把 TCP 套接字和 UDP 套接字跑起来,抓包看三次握手、四次挥手和无连接通信;再顺手抓 HTTP、FTP、DNS 三种协议报文,把课本上的协议字段一一落到真实流量上。对于正在学计算机网络、做课程实验或者准备面试的读者来说,这篇文章可以帮你把“协议”从抽象的图变成看得见摸得着的包。
整套实验不需要特殊硬件,一台电脑、一个 Wireshark、几段 Python 脚本就够了。下面我把环境搭建、TCP/UDP 捕包分析、HTTP/FTP/DNS 报文拆解和常见踩坑点全部展开,每一步都给可直接复现的方案。
1. 实验环境准备与工具选型
1.1 为什么选 Wireshark 而不是 tcpdump 或 Charles
Wireshark 的前身就是 Ethereal,两者在抓包核心能力上没有本质区别。它既是一个抓包器,也是一个非常强悍的协议解析器,内置了上千种协议的支持。它会自动把 TCP 握手标志、HTTP 请求响应、FTP 命令、DNS 记录类型识别出来,以分层树状结构展示,这对学习协议特别友好。
相比之下,tcpdump 是纯命令行工具,适合在服务器上快速抓包,但新手看十六进制输出很容易发懵。Charles 更多是面向 HTTP/HTTPS 调试,对于 DNS、FTP、原始 TCP/UDP 报文支持很弱。Wireshark 是同时满足“看得见底层细节”和“能按协议语义解读”的工具。实际操作中,如果在 Linux 服务器上临时抓包,我会先 tcpdump 存成 pcap 文件,再拉回本地用 Wireshark 慢慢看。纯课程实验场景,直接用图形界面最省事。
1.2 最小复现环境:Python 套接字脚本与抓包位置
抓包实验最怕环境嘈杂。为了得到干净、可控的流量,我建议用 Python 自带 socket 模块写最小客户端和服务端,流量只在一台机器的回环接口上跑。这样不用牵涉局域网广播、ARP、网关转发等额外噪声,抓包结果非常“教科书”。
抓包位置要特别注意:如果客户端和服务端都运行在 127.0.0.1,数据包走的接口是 Loopback,不是物理网卡。在 Wireshark 首页选择“Loopback: lo”或 Windows 下的“Npcap Loopback Adapter”,然后再点开始捕获。很多同学第一次实验“怎么也抓不到包”,八成是选了以太网或 WLAN 网卡,目光盯着物理接口,流量却在回环接口里穿行。
为了后续减少干扰,我习惯在抓包前先给 Wireshark 设置捕获过滤器,只留目标端口。比如 TCP 实验固定端口 8888,就写tcp port 8888;UDP 实验固定端口 9999,就写udp port 9999。这样抓包文件很小,分析时一眼就能找到关键报文。
1.3 捕获过滤器与显示过滤器不要搞混
Wireshark 里有两类过滤条件,容易被初学者混淆。捕获过滤器是在抓包之前生效,属于内核级 BPF 过滤,语法偏底层,比如tcp port 80、host 192.168.1.10。显示过滤器是抓包之后对已捕获数据包做筛选,语法更加丰富,比如tcp.port == 80、http.request、dns.flags.response == 0。
实验建议是:抓包阶段用捕获过滤器收敛范围,比如只抓某个端口或某台主机;分析阶段再用显示过滤器按协议、标志位、字段值筛选。显示过滤器可以随时修改,而且不会丢失已经抓到的数据,适合反复试。比如查看 TCP 三次握手,可以用tcp.flags.syn == 1把所有 SYN 包筛出来;查看 HTTP 请求,用http.request;查看 DNS 查询,用dns.qr == 0。
2. TCP 套接字捕包分析:三次握手与四次挥手
2.1 构建一对 TCP Socket 程序
先准备服务端脚本,监听本地 8888 端口。以下代码非常适合演示:
import socket srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(('127.0.0.1', 8888)) srv.listen(1) print('server waiting...') conn, addr = srv.accept() print('client connected:', addr) data = conn.recv(1024) print('received:', data) conn.sendall(b'hello from server') conn.close() srv.close()客户端脚本如下:
import socket cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) cli.connect(('127.0.0.1', 8888)) cli.sendall(b'hello from client') resp = cli.recv(1024) print('response:', resp) cli.close()操作顺序:先运行服务端,让它在 accept 处阻塞等待;启动 Wireshark,选择 Loopback 接口,捕获过滤器填tcp port 8888;然后运行客户端。结束后停止抓包,你会看到一连串以 8888 端口为宿主的 TCP 报文。
2.2 三次握手的关键报文解析
过滤结果中,前面三个包对应 TCP 三次握手。Wireshark 默认使用“相对序列号”,让数值从 0 开始显示,方便学习。实际线上的绝对序列号是随机初始化的,但实验时看相对序号更容易理解。下表是我抓到的典型结果:
| 包号 | 方向 | 标志 | 相对Seq | 相对Ack | 说明 |
|---|---|---|---|---|---|
| 1 | 客户端 -> 服务端 | SYN | 0 | 0 | 请求建立连接 |
| 2 | 服务端 -> 客户端 | SYN + ACK | 0 | 1 | 同意连接,确认客户端的SYN |
| 3 | 客户端 -> 服务端 | ACK | 1 | 1 | 确认服务端的SYN+ACK |
第一个包只设置了 SYN 标志,表示客户端请求同步序列号。第二个包同时设置 SYN 和 ACK,服务端既表示“我这边初始序号也是 0”,又表示“我收到了你的序号 0,所以我期望你下一个包序号是 1”。第三个包是客户端纯 ACK,确认收到服务端序号,握手完成。
很多新手看不懂为什么第三次握手的 Ack 是 1。这里的 Ack 表示“期望接收的下一个字节序号”。服务端在第二个包里带有 SYN,虽然没有数据长度,但在 TCP 语义中 SYN 和 FIN 本身各占一个序号。客户端收到序号 0 的 SYN 后,下一个期望序号自然就是 1。
在 Wireshark 的 TCP 头部展开中,你还能看到 Window Size、MSS、SACK permitted 等协商字段。其中 MSS 是“最大报文段长度”,双方在握手时交换各自能接收的最大单段数据长度;如果 MTU 是 1500 字节,扣除 IP 头、TCP 头后,MSS 常见值就是 1460。这些参数直接影响后续分段行为。
2.3 数据传输中的序列号与确认号
握手完成后,客户端发送了b'hello from client'。这个字符串长度为 17 字节,抓包显示为:
- 客户端 -> 服务端:PSH + ACK,Seq=1,Len=17
- 服务端 -> 客户端:ACK,Seq=1,Ack=18
这里 PSH 标志是 Push,意思是“请把数据立即交给应用程序”,不要等缓冲区填满。Len=17 是数据负载长度。服务端收到后回一个纯 ACK,Ack=18,说明它期望客户端下一个包从序号 18 开始。展开 TCP 头,你可以看到确认号变成了 18,正好是“上一个 Seq + Len”。
服务端随后回发hello from server,客户端同样回一个 ACK,Seq 和 Ack 各自推进。这个“一来一回,确认号跟着数据长度走”的规律是 TCP 可靠传输的核心。课堂上讲的累积确认、滑动窗口,在这里都有了直观载体。你可以故意把客户端 sendall 改成长文本,抓包看到 TCP 分段,再对比 MSS。
2.4 四次挥手与连接关闭
客户端脚本执行cli.close()后,会触发连接关闭。正常情况下抓包能看到四次挥手:
- 客户端 -> 服务端:FIN + ACK,Seq=18
- 服务端 -> 客户端:ACK,Seq=1,Ack=19
- 服务端 -> 客户端:FIN + ACK,Seq=1,Ack=19
- 客户端 -> 服务端:ACK,Seq=19,Ack=2
注意:如果服务端在收到 FIN 后立刻关闭,第二步和第三步可能会合并成“带数据的确认包 + FIN”,所以实际抓包不一定是四个包。课堂上说的四次挥手是典型过程,但要理解本质:主动关闭方说“我没数据了”,被动关闭方先确认,再把自己的数据发完,最后也说“我也没数据了”,主动方再确认。
关闭之后,主动关闭方会进入 TIME_WAIT 状态,抓包界面最后还有一个灰色的四重组记录。TIME_WAIT 的目的是让网络中迟到的报文自然消失,同时保证最后一个 ACK 万一丢失还能重传。理论上等待 2 个 MSL,实验时你可以用命令查看端口状态,但不用纠结拿不到包。
2.5 TCP 实验实操避坑
第一个坑:服务端 accept 之后只收一次数据,如果你的 sendall 和 recv 次数不匹配,另一方可能阻塞在 recv,连接一直不关闭。做实验时先设计好收发次数。第二个坑:Windows 防火墙可能拦截 127.0.0.1 上的监听,但通常少见,如果客户端连接超时,先检查服务端是否打印了 “client connected”,再检查端口是否被占用。第三个坑:Wireshark 默认的相对序列号可能在分析重传时产生困惑,如果想看真实序列号,可以取消勾选“TCP -> Relative sequence numbers”。第四个坑:抓包时不要开迅雷之类的下载工具,否则大量并发连接会淹没你的实验包。
3. UDP 套接字捕包分析:无连接的对话
3.1 构建一对 UDP Socket 程序
UDP 套接字代码比 TCP 更短。服务端绑定 9999 端口,等待接收一个数据报,原样回一条消息:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(('127.0.0.1', 9999)) print('udp server waiting...') data, addr = s.recvfrom(1024) print('received:', data, 'from', addr) s.sendto(b'hello from udp server', addr)客户端直接向 9999 端口发数据,不需要 connect,也可以 recvfrom 等待回应:
import socket c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) c.sendto(b'hello udp', ('127.0.0.1', 9999)) data, addr = c.recvfrom(1024) print('response:', data, 'from', addr) c.close()抓包时捕获过滤器写udp port 9999。运行后,正常情况下只会看到两个 UDP 包:一个从客户端随机端口发往 9999,另一个从服务端 9999 发往客户端的临时端口。全程没有握手包、没有 ACK,干净利落。
3.2 UDP 数据报头部逐个字段看
点开第一个 UDP 包,协议树会展开四个字段:Source Port、Destination Port、Length、Checksum,各占 16 位。客户端源端口是操作系统分配的临时端口,目的端口是 9999。Length 是 UDP 头部加数据的总长度,比如hello udp是 9 字节数据,头部 8 字节,Length 就是 17。
Checksum 字段在 IPv4 下可以填 0,表示发送方不计算校验和,但大多数系统都会计算。校验时会将 IP 伪头部、UDP 头部和数据一起算,覆盖源 IP、目的 IP、协议号以及 UDP 总长度,防止数据报在传输中损坏。Wireshark 会直接显示校验结果是否通过,你可以看到 “Checksum: 0x... [correct]”。
UDP 的最大特点是无状态。服务端 recvfrom 拿到数据报后,只能看到“从哪个地址来的”,没有连接上下文,也不需要维护发送缓冲区、接收窗口和重传状态。这也是为什么 UDP 头部这么轻,但应用层要自己处理丢包和乱序。
3.3 UDP 与 TCP 抓包特征对比
| 对比维度 | TCP | UDP |
|---|---|---|
| 建立连接 | 需要三次握手 | 无需握手 |
| 连接状态 | 双方维护复杂状态机 | 无连接状态 |
| 可靠性 | 序号、确认、重传、窗口 | 无确认无重传 |
| 头部开销 | 至少 20 字节 | 固定 8 字节 |
| 抓包特征 | SYN、ACK、FIN、PSH 一系列交互 | 一包请求一包响应 |
| 典型协议 | HTTP、FTP、SMTP | DNS、DHCP、视频流 |
实际抓包时,TCP 流可以用 Wireshark 的“Follow TCP Stream”重组出一个完整字节流,UDP 则没有“流”的概念,只能按五元组硬拼。实验时不要用分析 TCP 的思路套 UDP,看到两个报文一来一回,就是完整的 UDP 通信过程。
4. HTTP 协议抓包分析:一次 Web 请求的完整旅程
4.1 用本地 HTTP 服务构造明文请求
分析 HTTP 最忌讳直接抓公网流量,因为现代网站普遍启用 HTTPS,抓到的内容是加密的,哪怕是 Wireshark 也看不到明文报文。课程实验要的是看清协议文本,所以我建议在本地起一个 HTTP 服务。
先创建一个测试目录,里面放一个简单的index.html:
<html><head><title>test</title></head><body><h1>hello http</h1></body></html>然后在该目录执行:
python -m http.server 8080 --bind 127.0.0.1接着用 curl 发起一次请求:
curl -v http://127.0.0.1:8080/index.htmlWireshark 显示过滤器填http or tcp.port == 8080,开始抓包后重新执行 curl。你会得到一连串包:先是一个完整的 TCP 三次握手,然后有请求报文,紧接着响应报文。HTTP 是纯文本协议,直接看包列表的 Info 列就能看到 “GET /index.html HTTP/1.1” 和 “HTTP/1.1 200 OK”。
4.2 请求报文与响应报文逐行拆解
HTTP 请求报文由请求行、首部行、空行和可选消息体组成。抓到的请求展开后,关键行是:
- 请求行:
GET /index.html HTTP/1.1 - Host:
127.0.0.1:8080 - User-Agent:
curl/... - Accept:
*/* - 空行:表示头部结束
Host 字段特别重要。HTTP/1.1 允许一台服务器托管多个域名,虚拟主机靠 Host 区分。即使你访问的是 IP 加端口,Host 也必须带上端口号。User-Agent 标识客户端类型,服务端可以据此做内容协商。空行虽然不是头部字段,但在报文中是真实存在的两个字节\r\n\r\n,很多协议解析问题就出在这里。
响应报文首行是状态行:HTTP/1.1 200 OK。然后是响应头:
- Server:
SimpleHTTP/0.6 Python/3.x - Content-Type:
text/html - Content-Length:报文体字节数
最后是空行和 HTML 正文。Content-Length 告诉接收方消息体有多长,这样 HTTP 接收方不需要靠空行判断结束。如果响应是Connection: keep-alive,同一个 TCP 连接还可以继续发第二个请求,你可以在实验里连续 curl 两个文件,观察第二次请求时是否复用了之前的 TCP 连接。
4.3 用 Follow TCP Stream 快速还原完整会话
Wireshark 有一个极其好用的功能:选中任意一个 HTTP 包,右键选择“Follow TCP Stream”,弹窗会把同一个 TCP 连接上所有数据按时间顺序重组成完整文本。左边是客户端发送的内容,右边是服务端返回的内容,请求头和响应头一目了然。
这个功能尤其适合教学演示。哪怕你只看懂零散报文,Follow TCP Stream 一开,协议交互过程就变成了一段可读的对话。注意:只有 HTTP/1.x 明文流量才能这样直接读,HTTP/2 是二进制分帧,流重组里可能是一堆乱码。实验里我们刻意用 Python 自带服务,就是为了绕开这套复杂度。
5. FTP 协议抓包分析:控制连接和数据连接的双通道
5.1 快速搭建本地 FTP 并登录
FTP 是个老协议,但设计非常经典,值得拆开看。为了快速复现,我推荐使用 pyftpdlib 搭一个本地匿名 FTP。
安装依赖:
pip install pyftpdlib启动服务:
cd /tmp mkdir ftpdata python -m pyftpdlib -p 2121 -d /tmp/ftpdata -w-w表示允许匿名登录,-d指定共享目录。记住这里端口故意用 2121,避免和系统默认 FTP 冲突。然后打开另一个终端,用 curl 模拟客户端:
curl -v ftp://127.0.0.1:2121/或者用系统自带ftp命令:
ftp 127.0.0.1 2121抓包时过滤器写ftp or ftp-data or tcp.port == 2121。开始抓包后再执行登录命令。你会看到控制连接上依次出现:
- 220 FTP Server Ready
- USER anonymous
- 331 Anonymous login ok, send your complete email address as your password
- PASS anonymous@
- 230 Login successful
- SYST、PWD、TYPE I、PASV、LIST 等后续命令
这些命令和响应都是明文文本,Wireshark 的 Info 列直接显示命令内容。
5.2 控制连接与数据连接:PASV 模式下的二级连接
FTP 最独特的地方在于它使用两条 TCP 连接:控制连接和数据连接。控制连接固定走 21 端口(这里改成 2121),用来传命令和状态码;数据连接是临时的,用于传输目录列表或文件内容。
FTP 有两种建立数据连接的模式。主动模式下,客户端通过 PORT 命令告诉服务端“我临时开放了某个端口”,服务端主动去连接这个端口。被动模式下,客户端发 PASV,服务端回复227 Entering Passive Mode (127,0,0,1,p1,p2),告诉客户端“我开放了某个端口,你来连接”。现代网络环境下被动模式更常见,因为客户端不必开放入站端口。
抓包时如果看到PASV和227,接着就会出现一条新的 TCP 连接,目标端口就是服务端在 227 响应里用两个字节拼出的端口。Wireshark 会把这些数据包标记为 FTP-DATA。传输完文件后,数据连接通常立即关闭。所以一次文件传输会经历“控制连接命令 + 数据连接握手 + 数据传输 + 数据连接挥手”的完整过程。
5.3 FTP 明文凭据的安全启示
观察抓包结果你会发现,FTP 的PASS命令后面跟着明文密码。在真实网络环境中,任何中间设备都能直接读取这些内容。这就是为什么现代场景下常用 SFTP 或 FTPS 来加密传输。做这个实验不是为了鼓励使用 FTP,而是为了直观理解“明文协议”的风险。抓包课程里看到明文验证码,往往比课本上几百字的安全论述更有冲击力。
这个安全点也可以迁移到 HTTP:HTTP 同样是明文,Cookie、登录密码都可能被抓到。HTTPS 在 TCP 之上加入了 TLS 加密,Wireshark 里只能看到 Application Data 字段,看不到明文内部结构。
6. DNS 协议抓包分析:域名解析的幕后
6.1 使用 nslookup 构造一条 DNS 查询
DNS 是典型的“看起来简单,拆开很丰富”的协议。实验时用nslookup example.com或者 Linux 下的dig example.com发起解析请求,然后抓包过滤器写dns或者udp port 53。
运行:
nslookup example.comWireshark 里会出现两个包:一个是客户端发出的 DNS 查询,另一个是本地 DNS 服务器返回的响应。查询包的目的端口是 53,源端口是随机临时端口。响应包的源端口是 53,目的端口和查询包一致。DNS 默认走 UDP,但如果响应数据超过 UDP 载荷能力,会转向 TCP 53。课程实验通常只需关心 UDP 场景。
6.2 DNS 报文的结构化字段
展开 DNS 查询包,报文结构清晰可见。最上面是 DNS Header,固定 12 字节:
- Transaction ID:0x1234 之类的随机值,用于匹配请求和响应
- Flags:查询标志,比如 0x0100 表示标准查询且 RD(递归期望)置位
- QDCOUNT:问题区记录数,通常是 1
- ANCOUNT:应答区记录数,查询时是 0
- NSCOUNT、ARCOUNT:权威区和附加区记录数
展开下面的 Queries 区域,可以看到 QNAME 是按“长度+标签”编码的。比如example.com编码成07 example 03 com 00,每段前面一个字节表示标签长度,最后以 0x00 表示域名结束。QTYPE 是 1,表示 A 记录;QCLASS 是 1,表示 Internet 类。
响应包除了 Header 中的 QR 变为 1、RA 置 1,Answers 区域会包含解析结果。Answer 里的 NAME 经常显示为0xc00c这样的压缩指针,前两位二进制 11 表示这是一个指针,后面的 14 位指向报文中的某个偏移位置,从而复用之前出现的域名,避免重复存储整个域名。
6.3 TTL 与递归解析的观察
响应中的 TTL 字段表示这条记录可以在本地缓存多久,单位是秒。比如 TTL=3600,意味着这一小时内,客户端或本地解析器再次查询该域名时,可以直接用缓存回答,不需要再次发起递归查询。再次执行nslookup example.com,抓包可能看到查询包,但如果本地缓存命中,响应包可能直接从负缓存或内存解析器返回,不一定经过网络。
用普通nslookup只能看到客户端与本地 DNS 服务器之间的一问一答。想看递归过程,可以在 Linux 下执行dig +trace example.com,它会从根服务器开始,依次查询顶级域服务器、权威服务器,每一级迭代都会产生独立的 DNS 包。此时再过滤dns,能看到多个 Transaction ID、多个响应,能直观理解递归和迭代的差别。
6.4 DNS 实验的一些实操心得
DNS 报文看起来字段多,但核心只需要看四块:Transaction ID、Flags、Queries、Answers。先确定请求和响应的 ID 一样,再确认响应里 Answer 的地址和预期一致,最后看一眼 TTL,整个实验就完成了。抓 DNS 时不要开着系统里其他后台应用,否则邮件、即时通讯的域名查询会混进来,干扰判断。
我个人的习惯是:命令行里先清缓存再抓包。Windows 下ipconfig /flushdns,Linux 下根据发行版不同重启解析器服务,或者干脆等 TTL 过期。这样每次抓包都是“冷查询”,报文里的 TTL 和服务器信息更完整。
最后分享一个小技巧:做实验报告时不要只贴 Wireshark 截图,可以用tshark -r 文件.pcapng -Y "http" -T fields -e frame.number -e ip.src -e ip.dst -e http.request.method -e http.request.uri把关键字段导成 CSV,整理成表格后放进报告里,清晰又省空间。抓包分析这件事,本质上就是“用什么工具不重要,能不能把协议状态机对应到真实报文”才重要。多抓几次,多对照 RFC 和课本,等你能不看工具说明就准确读出 SYN、ACK、PASV、递归标志时,计算机网络这门课最硬的部分就真的过去了。