☰
双机直连网络实验:用tcpdump抓包还原TCP三次握手与自定义协议
2026/10/13 7:40:08 网站建设 项目流程

1. 实验目标与整体设计

提示:文章正文开始前,先把项目的来源和核心目标说清楚。下面这个标题里的“03”,在我这里指的不是某个固定课程编号,而是我独立实验笔记里的第三篇——一次围绕“双机直连 + 抓包还原”的课堂实验记录。如果你正在准备类似的网络实验,或者刚接触抓包分析,这篇文章可以作为一份可以直接照抄的操作手册。

1.1 这个实验到底在解决什么问题

课堂实验最容易出现的尴尬情况,是实验数据“看着对”但不知道底层发生了什么。比如用浏览器打开网页能通,但中间经历了什么,TCP连接怎么建立,数据帧怎么封装,全是黑盒。这次实验的核心目标,是把这种黑盒打开:我用两台Linux主机直接通过网线相连,在一端用自定义的通信程序发一段业务数据,另一端负责接收,同时在连接链路的两侧都开启抓包,最后把抓到的报文逐层拆开,和协议设计文档逐字段比对。

这个设计的思路,和“对着成品电路板学焊接”类似——直接看成品当然能学到一点,但远不如自己动手把元器件一个个插上去、再量一遍每个节点电压来得扎实。这次实验的本质,是主动构造一个可控的数据流,然后借助抓包工具把所有细节暴露出来。

实验工做下来,我确认了三点收获:第一,TCP三次握手和四次挥手不再只是书上的四行字,而是能在抓包里找到每一个状态的对应报文;第二,应用层自定义协议的设计缺陷会在实际抓包里现出原形,比如字段长度不够、校验算法覆盖范围错了;第三,只要链路一开抓包,性能损耗和offload机制对报文内容的影响会非常直观,这些东西是任何模拟器都演示不出来的。

1.2 为什么选“双机直连”而不是模拟器

很多入门教程会建议用网络模拟软件(比如常见的图形化模拟工具)来搭实验环境,点几下鼠标就能把拓扑画出来,看着确实方便。但我个人强烈建议,如果是想真正理解协议,一定要找两台真实物理机做直连实验。

原因有这么几个:

  • 模拟器里的“抓包”是软件模拟出来的,报文的封装过程被简化了,很多细节(比如网卡自动计算校验和、TCP分段行为)根本不会出现。
  • 双机直连的拓扑极简,只有两台设备、一根网线,数据流路径清晰,排查问题范围小。
  • 物理链路会带来真实的硬件行为——网卡灯、线序、速率协商、防火墙干扰,这些恰恰是新手最容易忽略又最常踩坑的地方。

当然,模拟器也有它的用武之地:做大规模拓扑演练、验证路由协议收敛、或者在没有硬件环境的时候做流程推演。但从“学习协议细节”这个目标出发,真实链路的价值远高于模拟器。

1.3 承接这次实验的软硬件清单

角色硬件/软件说明
发送端Linux笔记本A(内置有线网卡)运行自定义Python发送程序
接收端Linux笔记本B(内置有线网卡)运行接收服务,同时开抓包
连接介质六类非屏蔽双绞线(直通线序)长度2米,链路速率协商为1Gbps
抓包工具tcpdump/tshark命令行抓包,保存为pcap文件后用wireshark可视化分析
数据生成Python 3 + 自写脚本构造符合自定义协议格式的业务数据

这两台设备都是普通家用笔记本,没有额外采购专用仪器。实验全程不依赖外部网络,所有通信都限制在这两台机器组成的小型局域网里,既好控制变量,也不会被办公网络的准入策略干扰。

第1步先把环境搭好。别小看这一步,很多实验做不下去,问题不是出在代码,而是出在物理层。

2. 实验环境搭建与连通性验证

2.1 硬件连接和线序到底怎么选

两台电脑用网线直连,最稳妥的方案是用“直通线”还是“交叉线”?现代网卡基本都支持自动翻转(Auto MDI-X),所以市面上买到的普通直通线插上就能用。但如果你手里只有一条比较老的交叉线,大部分网卡也能识别,只是协商速度可能会低一些。

我在实际操作中遇到过这种情况:网线插好后,两端的网卡指示灯都不亮。那时候第一反应是线坏了,换了一条线还是一样,最后排查出来是网线水晶头松了,重新压紧之后灯才亮。所以碰到link灯不亮,不要急着怀疑网卡,先看物理接触。

接线完成后的验证节点:

  1. 两端网卡指示灯均亮起(绿色/橙色)。
  2. ip link命令能看到对应网卡状态为UP且LOWER_UP。
  3. ethtool 网卡名查看协商速率,正常应该是1000Mb/s或100Mb/s。

这里补充一个概念:双机直连时,两台设备处于同一个二层广播域,没有交换机转发,但IP通信完全没问题。只要网卡UP,物理层就通了。

2.2 静态IP配置和防火墙放行

物理层通了之后,需要给两台机器配上同网段的静态IP。我用的是:

  • 发送端 A:192.168.10.1/24
  • 接收端 B:192.168.10.2/24

配置方法有很多,这里用最直接的ip命令:

# 发送端 A sudo ip addr add 192.168.10.1/24 dev enp0s25 sudo ip link set enp0s25 up # 接收端 B sudo ip addr add 192.168.10.2/24 dev enp0s25 sudo ip link set enp0s25 up

配完地址先做一次连通性测试:从A ping B,确认能通。很多发行版默认开着防火墙,所以需要关闭或放行ICMP和实验用的自定义端口(我这里用的UDP端口8888和TCP端口8899)。关闭防火墙的命令在systemd系统上是:

sudo systemctl stop firewalld # 如果装了firewalld sudo systemctl stop ufw # 如果是Ubuntu等使用ufw

如果是生产环境机器,不建议直接关防火墙,放行指定端口就够了。但这是课堂实验,关闭防火墙可以少踩几个“疑难杂症”的坑。

提示:关防火墙不是目的,目的是排除干扰项。真正排查问题的时候,应该用一个已知可控的环境作为起点。

2.3 验证连通时容易忽略的细节

连续ping 100个包全部成功,不代表链路就是完美的。我在这次实验里还做了以下几项检查,这些都是在正式开抓之前必须确认的:

  • MTU检查:ip link show里默认MTU是1500。如果等会儿实验里打算发大包(超过1500字节),就会出现分片,抓包里数据会被拆成多个段,干扰Payload格式的分析。
  • ARP缓存确认:ip neigh show能看到对方的MAC地址。如果看不到,说明链路层通信还没建立,后面一切免谈。
  • 速率协商一致性:两端ethtool 网卡名显示的速率应当一致。如果不一致,要检查网线是否劣质、网口是否有灰尘。

这些细节全部确认无误后,实验环境才算真正“就绪”。我把这张清单每一条都写进了实验记录,方便等会儿复现问题时有据可查。

3. 应用层协议设计与抓包方案

3.1 为什么自己定义一套协议格式

课堂实验如果想做“抓包还原”,最简单的方式是直接抓HTTP或DNS流量,网上现成教程一大把。但我特意绕开这条路,自己设计了一个极简应用层协议。这么做的原因很现实:直接抓现成协议虽然省事,但报文里的每个字段为什么存在、长度怎么定、校验怎么算,你很难在抓包文件里体会到——因为这些协议太成熟了,所有细节都被打磨得看不出设计意图。

自己定义协议之后,抓到的每一个字节都能跟设计文档对应上,协议设计得好不好、有哪些隐藏漏洞,会在真实抓包结果里一目了然。这有点像“自己出题自己做”,练的是内功。

3.2 报文格式定义和Python发送端实现

这次实验自定义了一个名为“GCL”的轻量协议,格式如下:

偏移量字段名称长度(字节)说明
0魔数4固定为0x47434C31(ASCII为GCL1),用于识别协议
4版本1当前为0x01
5消息类型10x01表示数据请求,0x02表示数据应答
6流水号4自增计数,方便追踪报文对
10时间戳8Unix时间戳(小端序)
18数据长度4数据体的字节数,最大限制为512
22数据体可变业务数据,本次全部填充0xA5
22+N校验值4从魔数开始到数据体结尾的累加和(取低32位)

发送端代码很短,核心逻辑只有几十行:

import socket import struct import time def build_gcl_packet(seq: int, payload: bytes) -> bytes: magic = 0x47434C31 version = 0x01 msg_type = 0x01 timestamp = int(time.time()) data_len = len(payload) header = struct.pack("<IBBIQ", magic, version, msg_type, seq, timestamp) # 注意:这里显式写入4字节的数据长度 header += struct.pack("<I", data_len) body = header + payload # 从头部开始到payload结束,累加并取低32位 checksum = 0 for byte in body: checksum = (checksum + byte) & 0xFFFFFFFF body += struct.pack("<I", checksum) return body def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("192.168.10.2", 8899)) payload = bytes([0xA5] * 80) for seq in range(1, 11): packet = build_gcl_packet(seq, payload) sock.send(packet) print(f"已发送 seq={seq}, 长度={len(packet)}") time.sleep(0.1) sock.close() if __name__ == "__main__": main()

这里故意选择TCP而不是UDP,目的是让抓包里同时出现“三次握手”“数据段确认”“四次挥手”三个阶段,学习价值更高。如果用UDP,抓包里就只有单向的数据报文,就没有握手和挥手的分析了。

3.3 接收端和双向抓包的布置

接收端代码我这里简化一下,只保留核心的监听逻辑:

import socket def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(("192.168.10.2", 8899)) server.listen(1) print("等待连接...") conn, addr = server.accept() data = conn.recv(4096) print("收到数据,长度:", len(data)) conn.close() if __name__ == "__main__": main()

抓包的关键点是双向同时抓。发送端和接收端各自开一个tcpdump,命令如下:

# 发送端 A,抓 eth0 上与接收端B的通信 sudo tcpdump -i enp0s25 -s 0 -w send_side.pcap host 192.168.10.2 & # 接收端 B,抓 eth0 上与发送端A的通信 sudo tcpdump -i enp0s25 -s 0 -w recv_side.pcap host 192.168.10.1 &

-s 0表示不截断报文,保存完整包;host过滤条件限定只抓两台机器之间的流量,避免无关广播包干扰分析。等发送端程序跑完,停掉tcpdump,两个pcap文件就是后续分析的全部素材。

注意:接收端的recv只调用了1次,意味着如果发送端发了10个包,接收端可能只读到前几个包就被close了。我在实际实验里把接收逻辑改成了循环读取,确保数据全部收完再关闭。这个问题也属于典型的“基于课堂实验的常见合理改动”。

4. 抓包结果分析:从连接到数据还原

4.1 TCP三次握手在抓包里的真实模样

打开Wireshark加载recv_side.pcap,过滤tcp.port == 8899,能看到非常清晰的完整流程。前三个包就是SYN、SYN-ACK、ACK,如果你对TCP头部的熟悉程度一般,我强烈建议在Wireshark里点开每个包,对照右下角的十六进制逐字节看一遍,不要只看Wireshark帮你解析好的“缩写”字段。

我这边记录的几个关键点:

  • 帧1是发送端发来的SYN,Seq=0,Flags=0x002,窗口大小自动协商。
  • 帧2是接收端回应的SYN-ACK,Seq=0,Ack=1。注意这个“Ack=1”指的是“期望收到的下一个字节序号是1”,因为在SYN阶段序号会被消耗1。
  • 帧3是最后一个ACK,Seq=1,Ack=1,握手完成。

这三个包往返顺序不要搞反,一旦顺序不对,就是链路中存在中间设备影响(比如两个方向的路径不对称,或者有安全设备抢答),这在真实网络故障排查里也经常出现。

4.2 自定义协议的逐字节比对

握手完成后开始传数据。我在抓包里定位到第一个数据段,找到应用层Payload起始位置。TCP头之后就是GCL协议数据。用Wireshark的“显示为字节流”功能,把前24字节贴出来,逐字段对照协议格式:

47 43 4C 31 01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 50 00 00 00

逐一拆解:

  • 47 43 4C 31:魔数0x47434C31,ASCII字符“GCL1”,定位成功。
  • 01:版本号,没问题。
  • 01:消息类型,数据请求,没问题。
  • 01 00 00 00:流水号,小端序为1,这是第1个包。
  • 后面8字节是时间戳。
  • 50 00 00 00:数据长度,十六进制0x50即80,与发送端80字节的Payload完全对应。

全部字段都跟设计文档对上了。继续往后看,数据体部分全是0xA5,最后4字节是校验值。我在接收端脚本里也加了校验逻辑,收到的每个包计算一次累加和,和报文携带的校验值比对,全部一致。

这一步做完,实验的核心目标就达成了一半:自定义协议在真实链路上完整地跑通了,且抓包的还原结果和设计一致。

4.3 TCP确认、重传和四次挥手

再看TCP层面的表现。发送端一次性发了80字节数据,因为数据量小,TCP通常不会单独发确认包,而是等到第2个数据段时用“累积确认”的方式一并确认。抓包里能看到接收端回复的ACK包的Ack序号在不断增长。这个行为恰好解释了“往返时延”和“吞吐量”之间的权衡——如果每个包都单独确认,网络开销会翻倍。

实验结束后,发送端主动close连接。抓包末尾能看到FIN、ACK、FIN、ACK的标准化四次挥手。这里有个细节值得注意:主动关闭方的状态会进入TIME_WAIT,抓包里虽然看不到状态,但后续其他实验如果立刻重用同一个端口绑定,可能会因为地址占用而失败,解决办法是设置SO_REUSEADDR。

4.4 双向抓包对比带来的额外发现

把发送端和接收端的pcap文件都加载进Wireshark,用“电话”视图看整个会话,能发现一个有意思的现象:发送端的相对序号和接收端的相对序号在绘图里是两条错开的线,一边的Seq增长对应另一边的Ack增长。如果只在单边抓包,很难理解序号和确认号的相对关系。所以我强烈建议做双向抓包,并且把两个文件合并到一个会话视图里分析。

实验到这里,核心闭环已经完成。但作为实操记录,我更想把实验中踩过的那几个坑原原本本写出来,因为这些坑要么不被人提起,要么非常反直觉。

5. 实践过程踩坑记录与问题排查

5.1 发送端抓包看不到应用层数据

第一次跑实验的时候,我在发送端抓到的TCP数据段里没看到GCL协议的魔数。排查了很久,最后锁定是网卡的TCP分段卸载(TSO/GSO)机制在搞鬼。

简单解释一下:现代网卡硬件能力很强,操作系统会把大块数据直接交给网卡,由网卡自己分成多个TCP段。这样减少了CPU的中断次数,提升了吞吐量,但副作用是,在发送端网卡驱动层面抓包时,看到的是“未分段前的大包”或“被硬件改造过的包”,而不是线路上真正传输的报文。

解决方式有两个:

  1. 关闭TSO/GSO再抓包:
sudo ethtool -K enp0s25 tx off sg off tso off gso off
  1. 在接收端抓包,因为接收端看到的是线路上的真实分段。

我在实验里两个方法都试了,关闭offload之后发送端能正常看到Payload,但接收端抓到的包始终是最标准的结果。所以建议优先在接收端抓包做协议分析。

5.2 防火墙导致“ping通但连不上TCP端口”

A ping B能通,A用Python脚本连B的8899端口却一直超时。查看B的监听状态发现端口在LISTEN,但连接就是进不来。排查到最后是ufw还在运行,虽然ICMP被放行了,TCP 8899没有放行规则。

这个案例非常典型,也正好印证了前面我说的“课堂实验先把防火墙关掉”。因为第一次实验,最容易出现的现象就是“看起来链路通,但应用连不上”,新手会花大量时间怀疑代码、怀疑协议格式,最后发现是系统策略拦截。

正确的排查思路应该是:先确认ICMP通,再确认端口通(比如用nc -vz 192.168.10.2 8899做端口探测),最后再看应用层。一条一条剥离干扰,而不是一上来就翻代码。

5.3 Wireshark显示校验和错误

接收端抓包里,每个TCP段的Checksum状态栏都显示红色“incorrect”。第一次看到这个我差点以为是链路有问题。实际原因是网卡的TCP校验和卸载(checksum offload)功能在起作用:操作系统在发送时将校验段留空,由网卡硬件在真正发送数据时计算并填充正确值。而tcpdump抓的是网卡驱动已经处理过的包,所以显示的校验和是“尚未计算完成”的原始值。

这种“抓包看到校验错但链路实际正常”的现象,只能说太常见了。判断标准很简单:如果两端抓到的包都有TCP Checksum错误,且双方通信正常,一般就是offload现象;如果只有单向出错且伴随重传、丢包,才需要认真追查介质和干扰。

5.4 序列号绝对值和相对值看花眼

Wireshark默认展示的是相对序列号(把握手包的Seq显示为0),方便阅读。但我在手动比对字节时,发现十六进制窗口里的绝对序列号和上层显示的相对序号对不上,一度以为抓包数据有问题。后来在Wireshark的“协议属性界面”里把“相对序号”选项切换掉,才能看到网络上的真实绝对值。

这个坑很小,但涉及一个重要的排查纪律:看协议分析工具的高级解释之前,一定要回到原始字节;手里的工具越智能,越要警惕它替你做过什么“美化”。

我将上面所有踩坑场景整理成一张速查表,方便后续复用:

现象直接原因处理办法
网卡灯不亮水晶头松动/线序不匹配重新压线,确认直通线
两端link UP但ping不通静态IP不在同网段/防火墙拦ICMPip addr检查,放行或关闭防火墙
ping通但TCP连不上防火墙未放行目标端口nc -vz探测端口,放行规则
发送端抓包无PayloadTSO/GSO offload关闭网卡offload,或在接收端抓包
Checksum报错但通信正常checksum offload不用处理,换接收端观察即可

5.5 一次由网线劣质导致的协商降速

实验过程中有段时间发送速率从1Gbps降到100Mbps,导致同一批报文出现了更多TCP分段。替换了一条品牌网线后速率恢复。这提醒我,实验的物理介质虽然不是“算法”,但劣质线缆会造成额外的重传和延时长尾,直接影响实验数据的可读性。如果实验里重传包特别多,先检查物理链路,而不是急着改代码。

6. 实验成果与经验复盘

6.1 完成了哪些目标

这次“课堂实验”做完,我手里拿着两个pcap文件、一份协议设计文档、一版可复用的Python收发代码。从结果上看,目标全部落地:

  • 双机物理直连,网络连通性验证通过。
  • 自定义应用层协议在TCP上完成10个报文的发送与接收。
  • 抓包还原出完整的TCP三次握手、数据传输、四次挥手流程。
  • 协议各字段与设计文档逐字节一致。
  • 校验值全部匹配。
  • 覆盖了TSO、防火墙、协商速率等真实环境问题。

如果只看表面,实验“成功”了。但我更看重的是过程中无形获得的东西:对TCP状态转移的敏感度、对卸载机制的意识、对工具显示信息背后的原始数据的警觉。这些经验不是看书看来的,是拿真实链路一遍遍试出来的。

6.2 实验后续可以怎么扩展

如果时间富余,或者想做进阶练习,我建议往这几个方向深挖:

  • 把TCP换成UDP,对比抓包里缺少了哪些保护机制,加深对“可靠传输到底贵在哪”的理解。
  • 在两台机器中间加一台交换机或软路由,观察TTL变化和跨网段的MAC寻址差异。
  • 给自定义协议增加“应答-重传”逻辑,模拟一个小小的可靠传输协议,看看状态机在真实验证下会不会出BUG。
  • 用tc工具在接收端加入延迟和丢包,测试重传算法在真实链路下的表现。

这些扩展里,我最推荐第一个——UDP对比实验。因为TCP的复杂机制只有当你看到UDP的“裸奔”之后,才会真正觉得“哦,原来TCP帮我挡住了这么多问题”。跑一次,比看十篇文章都管用。

6.3 给后来者的一句话建议

课堂实验最大的价值不在于“把实验指导书上的步骤走完”,而在于过程中主动给自己找麻烦:改参数、关防御、破坏链路、看它出什么错、再修复。把实验做“坏”几次,学到的东西远比顺利跑通一次要多。

我自己的体会是,抓包分析这件事,工具门槛并不高,难的是建立“字节视角”——看任何网络现象,都习惯性往最底层去问一句:“这个字段到底是谁写的,值是怎么算出来的?”只要有了这个意识,TCP、HTTP、TLS这些协议在你眼里就会慢慢变得透明起来。

最后再分享一个小技巧:凡是做抓包实验,抓完立刻给pcap文件加一行注释(存成pcapng格式时),写清楚实验编号、拓扑、时间、当时改过的参数。否则过两个星期你再翻文件,看着几十个类似命名的pcap文件,根本分不清哪个是哪个,那种感觉可比踩坑难受多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询