news 2026/10/21 10:20:56

基于QT5与WinPcap的仿Wireshark抓包器:架构、解析与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于QT5与WinPcap的仿Wireshark抓包器:架构、解析与避坑

简介:这是一份仿WireShark的网络抓包程序完整源码,基于QT5和WinPcap实现,面向网络工程师、C++开发者及网络安全学习者,可用于捕获、过滤与分析网络数据包,辅助网络故障排查、协议分析与应用调试。压缩包共392个文件,包含C/C++源文件(.c/.cpp/.h)、QT界面文件(.ui/.qrc)、工程配置(.vcproj/.dsp/.sln/.pro)、编译产物(.o/.lib/.a)以及HTML帮助文档、图片等,整体约8.15MB,目录结构按功能模块划分,查找定位方便。目前已有197人浏览学习。程序界面与操作逻辑贴近WireShark,覆盖网卡选择、抓包启动、数据包解析、列表展示与过滤的完整流程,源码涉及QT5信号槽、多线程界面更新以及WinPcap核心API的调用,还包含UDP抓包、测试发包等可执行示例,便于对照学习。对于希望深入理解Windows网络抓包原理或基于此做二次开发的读者,是一份具参考价值的实战资料。

1. Sniffer:值得复现的 QT5 + WinPcap 仿 WireShark 抓包器

调一个私有协议的时候,我最烦的就是打开 WireShark 等它加载一堆用不上的解析器。真正需要的其实就三样:一个能实时滚动的包列表、一个能看字段的协议树、一个能还原字节的十六进制面板。这个基于 QT5 和 WinPcap 实现的网络抓包程序 Sniffer,就是把 WireShark 最常用的这套三栏界面搬到一个 C++ 工程里,自己掌控每一列显示什么、过滤怎么写、解析做到哪一层。它是抓包器里最标准的骨架:WinPcap 负责收包和 BPF 过滤,QT5 负责界面和线程调度,中间用信号把包数据从底层送到界面。上手之后你会发现,真正卡人的不是解析以太网帧头,而是抓包线程和界面线程的同步,以及 WinPcap 在 Win10/11 上的驱动兼容。这份资源适合想快速搭出私有协议调试工具的人,也适合把 Qt 多线程和 C++ 网络编程串起来练一遍的进阶者。

2. 工程骨架:抓包线程、队列与信号上抛怎么搭

这个工程能跑起来,取决于三件事:WinPcap/Npcap 的驱动装好,QT5 的 pro 文件里链接对了库,抓包线程不碰界面。前两件事是环境问题,第三件事才是代码层面的重点。工程里最常见的错误,是把 pcap_loop 直接丢进主线程跑,界面瞬间冻结,然后回调函数里又在 new QTableWidgetItem,Windows 直接给你闪退。所以骨架的第一原则是:抓包在一个线程,界面在另一个线程,中间用 Qt 信号做交接。

2.1 三个类就能跑起来:MainWindow、CaptureThread 与 PacketMeta

工程里核心类就三个。PacketMeta 是包数据的统一出口,一个 struct 里放源/目的 MAC、IP、端口、协议名、时间戳和原始包数据;CaptureThread 继承 QThread,里面跑 pcap_next_ex 循环,抓到包就 emit 一次信号;MainWindow 负责三栏界面和菜单,收到信号填充表格和十六进制视图。

pro 文件是编译的第一个门槛,我一般这样写:

QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TEMPLATE = app TARGET = Sniffer INCLUDEPATH += C:/npcap-sdk/include LIBS += -L C:/npcap-sdk/lib -lwpcap -lws2_32 SOURCES += main.cpp \ mainwindow.cpp \ capturethread.cpp \ packetparser.cpp HEADERS += packetmeta.h \ capturethread.h \ mainwindow.h

INCLUDEPATH 指向 Npcap SDK 的 include,LIBS 里 -lwpcap 和 -lws2_32 缺一不可。缺了 wpcap 编译直接报 undefined reference;ws2_32 是 Windows Sockets 库,WinPcap 依赖它做底层名字解析。如果你的机器装的是老版本 WinPcap,库路径可能是 C:\Program Files\WinPcap\Lib,但建议直接用 Npcap SDK 重编,原因放到避坑章细说。

2.2 抓包循环:pcap_next_ex 轮询,别在主线程跑 pcap_loop

抓包线程的 run 函数是整个工程的心脏。核心循环长这样:

void CaptureThread::run() { pcap_pkthdr* header = nullptr; const u_char* data = nullptr; while (running_) { int ret = pcap_next_ex(handle_, &header, &data); if (ret == 1) { PacketMeta meta = parse_packet(data, header->caplen); meta.timestamp = header->ts; meta.rawData = QByteArray( reinterpret_cast<const char*>(data), header->caplen); emit packetReady(meta); } else if (ret == -1) { emit errorHappened(QString::fromLocal8Bit(pcap_geterr(handle_))); break; } // ret == 0 表示读取超时,没有新包,继续下一轮 } }

running_ 是 volatile bool 成员,stop() 里先置 false,再调用 pcap_breakloop(handle_) 打断阻塞读。这里我刻意用 pcap_next_ex 而不是 pcap_dispatch:pcap_dispatch 是回调式的,包会成批进入回调栈,在 Qt 信号里把栈打得很深;pcap_next_ex 一次只取一个包,循环结构好控制退出,也可以在循环里随时检查 running_。

打开网卡时的参数同样关键:

char errbuf[PCAP_ERRBUF_SIZE] = { 0 }; pcap_t* handle = pcap_open_live( deviceName.toStdString().c_str(), 65535, // snaplen:抓完整包,不截断 1, // promisc:混杂模式 50, // to_ms:读取超时 50ms errbuf);

snaplen 设 65535 是为了拿到完整的以太网帧,不截断;如果设 1514 或者更小,包尾的数据会在解析时丢失。promisc 设 1 表示混杂模式,但要注意:在交换式网络里混杂模式只能保证本网卡收下经过它的所有帧,交换机会把别的机器的流量直接送到目标口,能不能看到别人的包取决于网络结构,不是软件层面的玄学。to_ms 设 50 是兼顾界面响应和 CPU 占用的值。

注意:to_ms 不要设 0,在部分驱动下会导致读取线程长时间阻塞,pcap_breakloop 也救不回来,界面会像死掉一样。

2.3 信号上抛:跨线程别直接操作界面

数据从抓包线程到界面线程,靠的是信号槽。跨线程信号默认走队列连接,Qt 会把参数拷到 UI 线程的事件队列里,包数据到达后,MainWindow 的槽函数在 UI 线程执行,天然避开了线程竞争。

class CaptureThread : public QThread { Q_OBJECT public: explicit CaptureThread(pcap_t* handle, QObject* parent = nullptr); void stop(); signals: void packetReady(PacketMeta meta); void errorHappened(QString message); protected: void run() override; };

这里有一个隐藏坑:如果 PacketMeta 里放了 QByteArray,需要先 Q_DECLARE_METATYPE(PacketMeta) 注册元类型,否则运行时报错 Cannot queue arguments of type 'PacketMeta'。还有一种做法是维护一个 QMutex 保护的 QQueue,UI 线程用 QTimer 定时取,两种方案都能跑。我一般用信号,代码干净些,但 Q_DECLARE_METATYPE 这一步别漏。

千万别在 run() 里直接调用 table_->insertRow()。QTableWidget 不是线程安全的,跨线程操作轻则刷新撕裂,重则闪退。把包数据塞进信号参数交出去,是这套工程里最值得坚持的一条纪律。

3. 协议解析:从字节流到三栏联动视图的完整路径

抓包器把包摆上界面之前,先要把字节数组翻译成能看的字段。网络抓包最难的不是抓,而是看懂。WireShark 的可视化之所以高效,是因为它把每种协议的字段偏移提前算好,鼠标点一下就能看到对应字节。Sniffer 也要按这条路走:先解析,再显示,最后把表格、协议树、十六进制三块联动起来。

3.1 从 14 字节以太网头开始:字节序和偏移别搞反

我先把解析器要用的字段偏移列成一张表,写代码时照着这张表取数,基本不会错:

字段偏移长度说明
目的 MAC06接收方地址
源 MAC66发送方地址
EtherType1220x0800=IPv4,0x0806=ARP
IPv4 版本+IHL141高 4 位版本,低 4 位头长(单位 4 字节)
TTL221经过的路由器跳数
协议号2316=TCP,17=UDP
源 IP264网络字节序
目的 IP304网络字节序
TCP 源端口342大端序
TCP 目的端口362大端序
TCP 序号384会话重组时会用到

注意 TCP 不是直接跟在以太网头后面,中间隔着 IPv4 头;IP 头长度不固定,所以要先读 IHL 算出偏移,再读端口。x86 是小端机,但网络字节序是大端,直接 memcpy 成 quint16 再强转,拿到的一定是反的,这是我最早翻车的地方。

PacketMeta parse_packet(const u_char* data, int len) { PacketMeta m = {}; if (len < 14) return m; // 以太网头:目的 MAC、源 MAC、EtherType m.dstMac = macToString(data); // 偏移 0 m.srcMac = macToString(data + 6); // 偏移 6 quint16 etherType = (data[12] << 8) | data[13]; int offset = 14; if (etherType == 0x0800 && len >= offset + 20) { m.netProto = "IPv4"; m.srcIp = ipv4ToString(data + offset + 12); m.dstIp = ipv4ToString(data + offset + 16); int ihl = (data[offset] & 0x0F) * 4; // 头长字段单位是 4 字节 m.ttl = data[offset + 8]; quint8 proto = data[offset + 9]; offset += ihl; if (proto == 6 && len >= offset + 20) { // TCP m.transProto = "TCP"; m.srcPort = (data[offset] << 8) | data[offset + 1]; m.dstPort = (data[offset + 2] << 8) | data[offset + 3]; m.tcpSeq = (data[offset + 4] << 24) | (data[offset + 5] << 16) | (data[offset + 6] << 8) | data[offset + 7]; } else if (proto == 17 && len >= offset + 8) { // UDP m.transProto = "UDP"; m.srcPort = (data[offset] << 8) | data[offset + 1]; m.dstPort = (data[offset + 2] << 8) | data[offset + 3]; } } else if (etherType == 0x0806) { m.netProto = "ARP"; } return m; }

macToString 要特别注意用 QString("%1:%2...").arg(b, 2, 16, QLatin1Char('0')) 补零,不然 0A 会被打成 A。每次取字段之前都先判断 len,防止畸形包越界,这是抓包器稳定性的底线。snaplen 被截断时,长度判断会拦住残缺包,避免解析出完全没有意义的端口号。

3.2 IPv4 与 TCP/UDP 解析:协议号、端口和 IHL 的三处细节

协议号字段是单字节,在 IPv4 头里偏移 9,6 是 TCP,17 是 UDP,别和 EtherType 混了。EtherType 0x0800 表示下一层是 IPv4,而 IP 头里那个 6 才表示再下一层是 TCP,两层判断各司其职。

IHL 是 IP 头里最容易读错的字段。它占一个字节的低 4 位,单位是 4 字节,所以代码里要先 & 0x0F 再 * 4。标准 IPv4 头是 20 字节,带选项时能到 60 字节。如果直接按 20 字节固定偏移去读 TCP 端口,带选项的包全部错位,这也是用 WireShark 对比时最容易发现的差异。

DNS 解析总是抓包时最显眼的小包流量,因为真正的大流量还没开始,域名解析已经先忙活起来了。看到 udp 53 的包,基本就可以判断是哪台主机在发起 DNS 查询,这对应 WireShark 里习惯按域名观察流量的场景。BPF 过滤表达式只能按 IP 和端口匹配,想在 Sniffer 里按域名过滤,需要自己在解析层做 DNS 响应缓存,这是显示过滤和抓包过滤的差别,后面会细说。

3.3 三栏联动:表格存 meta,协议树按需构建

解析完成后的 PacketMeta 进入 UI 线程,表格只负责把每层的摘要填到对应列:

void MainWindow::onPacketReady(PacketMeta meta) { metaList_.append(meta); int row = metaList_.size() - 1; table_->insertRow(row); table_->setItem(row, 0, new QTableWidgetItem(QString::number(row + 1))); table_->setItem(row, 1, new QTableWidgetItem(meta.timestampString())); table_->setItem(row, 2, new QTableWidgetItem( meta.srcIp + ":" + QString::number(meta.srcPort))); table_->setItem(row, 3, new QTableWidgetItem( meta.dstIp + ":" + QString::number(meta.dstPort))); table_->setItem(row, 4, new QTableWidgetItem(meta.transProto)); table_->setItem(row, 5, new QTableWidgetItem( QString::number(meta.rawData.size()))); }

这里只存几十字节的 meta,不存协议树。协议树是 QTreeWidgetItem 对象,每个节点都有不小的内存开销,五千包每包十个节点就是五万个对象,内存和刷新都扛不住。所以协议树放到点击行的时候再构建:

void MainWindow::onTableRowSelected(int row) { PacketMeta m = metaList_[row]; tree_->clear(); QTreeWidgetItem* eth = new QTreeWidgetItem(tree_, {"以太网帧头"}); eth->addChild(new QTreeWidgetItem( {QString("目标MAC: %1").arg(m.dstMac)})); eth->addChild(new QTreeWidgetItem( {QString("源MAC: %1").arg(m.srcMac)})); if (!m.netProto.isEmpty()) { QTreeWidgetItem* ip = new QTreeWidgetItem(tree_, {m.netProto + " 层"}); ip->addChild(new QTreeWidgetItem( {QString("源IP: %1").arg(m.srcIp)})); ip->addChild(new QTreeWidgetItem( {QString("目的IP: %1").arg(m.dstIp)})); ip->addChild(new QTreeWidgetItem( {QString("TTL: %1").arg(m.ttl)})); } hexView_->setPlainText(formatHex(m.rawData)); }

formatHex 每行输出 16 字节,左边是十六进制、右边是 ASCII,明文协议的内容能直接读出来。三栏布局里表格、协议树、十六进制面板就是 WireShark 的 Inspector 范式,做可视化抓包器,这套界面结构是最容易让用户上手的。点击行时重新解析既省内存,又保证协议树和当前表格行永远一致,不会出现两边数据对不上的情况。

4. 过滤与回放:BPF 表达式、pcap 落盘和离线验证

抓包器能收包只是第一步,真正干活靠过滤。WinPcap 的过滤在内核里完成,也就是说驱动层就把不匹配的包丢掉了,不会往上抛。这个性质决定了过滤必须写对,因为不匹配的包永远不会出现在你的抓包循环里。WireShark 里按 ip.addr 过滤是界面层的显示过滤,跟这里的内核 BPF 不是一回事,很多人在这两个概念上栽过跟头。

4.1 BPF 过滤:pcap_compile 的写法与 Wi-Fi 网卡陷阱

给已打开的网卡设置 BPF 过滤器,标准的写法是三步:

bool applyBpfFilter(pcap_t* handle, const QString& expression) { bpf_program fp; memset(&fp, 0, sizeof(fp)); int rc = pcap_compile(handle, &fp, expression.toStdString().c_str(), 1, // optimize PCAP_NETMASK_UNKNOWN); // netmask if (rc != 0) { qWarning() << pcap_geterr(handle); return false; } if (pcap_setfilter(handle, &fp) != 0) { qWarning() << pcap_geterr(handle); return false; } pcap_freecode(&fp); return true; }

optimize 传 1 让 BPF 编译器做优化,netmask 传 PCAP_NETMASK_UNKNOWN 表示不关心本机地址段,做网段匹配时够用。pcap_compile 失败时错误信息在 pcap_geterr 里,最常见的报错是 syntax error,说明表达式写错了。pcap_freecode 一定要调用,多次设置过滤器时,不释放会在这个句柄上累积代码段,属于慢性内存泄漏。

常用的表达式就这么几类:host 192.168.1.100 只看某台主机,tcp port 443 只看某个端口,src 10.0.0.0/8 and udp 组合网段和协议,arp 只看 ARP。把这些做成输入框,每次抓包前先想清楚要什么,再启动抓包循环。

Wi-Fi 网卡是过滤器最容易踩的坑。pcap_findalldevs 返回的无线网卡,链路层类型可能是 DLT_IEEE802_11 而不是 DLT_EN10MB,BPF 过滤器按以太网头偏移解析,tcp port 443 在这种网卡上经常匹配不到任何包。表现是表达式没问题、驱动也正常,但就是一包都抓不到,属于看着玄学、实则链路类型不对的典型案例。解决办法是抓无线流量时优先用有线网卡,或者接受无线网卡只能抓到发给本机的帧。

4.2 落盘与回放:pcap_dump 一行保存,离线再用 WireShark 验证

保存抓包结果,最省事的方式是直接调 WinPcap 的 dump 接口:

pcap_dumper_t* dumper = pcap_dump_open(handle, "capture.pcap"); if (dumper) { // 每抓到一个包时调用 pcap_dump(dumper, header, data); pcap_dump_flush(dumper); // 实时落盘,程序崩溃也能保住前面数据 }

pcap_dump_open 会从 handle 里读链路层类型,自动写出 24 字节的 PCAP 全局头,所以保存期间别关闭 handle。pcap_dump_flush 是实时写盘,代价是每包一次系统调用;批量抓包时建议改成每 100 包 flush 一次,平衡可靠性和吞吐。要做实时分片保存的话,加一个包数计数,到阈值就 pcap_dump_close 再重新打开新文件,文件名带时间戳就行,这样长时间抓包不会撑爆单个文件。

如果不想依赖 dumper,也可以自己写 PCAP 文件头,结构很固定:

字段值说明
magic0xa1b2c3d4固定魔数,小端序
version_major2主版本
version_minor4次版本
thiszone0时区修正,通常为 0
sigfigs0时间戳精度,通常为 0
snaplen65535抓包截断长度
linktype1DLT_EN10MB,以太网

写完文件头后,每条记录按 pcap_pkthdr 的格式写入:时间戳秒、时间戳微秒、包实际长度、包抓取长度,然后跟原始字节。这样存出来的文件可以直接用 WireShark 打开,这是验证自己的解析器有没有解析错的最佳方式,把同一个包在两边打开对比字段即可。

离线回放只需要换一个入口:

pcap_t* off = pcap_open_offline(filepath.toStdString().c_str(), errbuf);

打开后走和在线抓包完全相同的解析分发逻辑,只是把网卡读取换成文件读取。离线模式让调试特定请求变得可复现:线上抓一份 pcap 存下来,回到本地慢慢分析,不用再对着屏幕抢包。

4.3 拖拽打开 pcap:setAcceptDrops 与事件过滤器

很多人写 Qt5 程序时拖拽没反应,是因为只调了 setAcceptDrops(true) 但没重写 dragEnterEvent 和 dropEvent。更省事的做法是给 MainWindow 安装事件过滤器,统一拦截拖拽事件:

void MainWindow::setupDrop() { setAcceptDrops(true); installEventFilter(this); } bool MainWindow::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::DragEnter || ev->type() == QEvent::DragMove) { auto* e = static_cast<QDragEnterEvent*>(ev); if (e->mimeData()->hasUrls()) { e->acceptProposedAction(); } return true; } if (ev->type() == QEvent::Drop) { auto* e = static_cast<QDropEvent*>(ev); QString path = e->mimeData()->urls().first().toLocalFile(); if (path.endsWith(".pcap", Qt::CaseInsensitive)) { openOfflineFile(path); } return true; } return QMainWindow::eventFilter(obj, ev); }

QTreeWidget、QTableWidget 这些子控件会消化 drop 事件,在 MainWindow 上统一拦截比在每个子类里重写一遍可靠。DragMove 也要接受,否则拖到区域边缘再回来会出现 DragLeave 的怪现象。文件路径先检查后缀再交给 openOfflineFile,这个函数内部就是 pcap_open_offline 加解析循环,代码和在线模式共用同一套解析器。

5. 避坑:WinPcap 装不上、拖拽失效、界面卡死怎么办

下面这几条是实际跑这个工程时最容易踩的坑,每一条我都见过不止一个人卡住,写出来当后悔药。环境问题永远排在代码问题前面,先把驱动和权限搞清楚,再回头看代码。

5.1 WinPcap 装不上:多数是驱动签名和 64 位路径问题

现象:安装到最后一步报错,设备管理器里看不到 NPF 驱动设备,程序打开网卡时提示 error opening adapter。

原因:WinPcap 4.1.3 的驱动在 Win10/11 上签名校验不通过,新系统直接拒绝加载。很多工具链自带的 WinPcap 安装包又是 32 位路径,系统是 64 位时也容易出问题。搞 FPGA 开发的人会在装 Vivado 时遇到 vivado winpcap 安装失败,本质就是同一个驱动兼容问题。

解决:把系统里的 WinPcap 卸载干净,装 Npcap,安装时勾选 WinPcap API 兼容模式,工程里继续用 wpcap.lib 和 pcap.h 编译,代码不用改。如果 Vivado 这类工具找不到 Npcap,把它安装目录里的 wpcap.dll 和 packet.dll 手动复制到工具自带的 winpcap 目录里,一般能救回来。

5.2 明明有流量却抓不到包:混杂模式不等于全收

现象:网卡列表正常,pcap_open_live 成功,但列表里只有零星的 ARP 或广播包。

原因:两种情况。第一种是权限不足,WinPcap/Npcap 需要管理员权限运行程序,普通权限下驱动层收包受限;第二种是现在基本都是交换网络,混杂模式只能让本网卡收下经过它的所有帧,交换机会把别的机器的包直接送到目标口,不会广播到所有口,所以看不到别人的流量是正常的。

解决:右键管理员身份运行程序;确认选择的网卡描述不是无线网卡,无线网卡抓包链路层类型不对,很多过滤器会失效;想抓别的机器流量,需要在交换机上配置端口镜像,这不是软件层面的玄学,是网络结构决定的。

5.3 抓到几千个包界面直接卡死

现象:包量到几千以后,界面响应越来越慢,CPU 居高不下,滚动列表像拖泥。

原因:每包都 insertRow、每包都 scrollToBottom、协议树提前构建,这三个操作单独看都不重,叠加起来就是 O(n²) 级别的刷新开销。另一个常见原因是 2.2 节提到的 to_ms 设 0,内核缓冲容易满,pcap_next_ex 陷在驱动读取里出不来,抓包线程看起来像死掉。

解决:批量插入时先 setUpdatesEnabled(false),插入完再恢复;滚动条每 200 包滚一次,不要每包都滚到底;协议树只在点击行时构建,绝不在抓包循环里创建树节点。这几个改动做完,同样五千包,界面流畅度是两个世界。

5.4 中文路径打不开 pcap 文件

现象:程序里通过文件对话框选了一个 D:\抓包\test.pcap,pcap_open_offline 返回空指针,离线回放打不开文件。

原因:老 pcap API 的 char* 参数按本地代码页解释,Qt 的 QString 默认转成 UTF-8 之后,中文路径编码不一致,底层文件创建函数找不到路径。这个坑在线抓包时不出现,只有离线回放时冒出来,排查起来很隐蔽。

解决:传参前用 toLocal8Bit() 转成系统本地编码;更省事的办法是先用 QFile::copy 把文件复制到临时目录的 ASCII 路径再打开。从那以后我写离线导入功能,路径转换永远是第一行代码。

5.5 过滤器加上后包量为零

现象:在界面输入 tcp port 443,一包都抓不到,去掉过滤就正常。

原因:先怀疑表达式语法。语法错误时 pcap_compile 返回非零,错误信息在 pcap_geterr 里;再怀疑网卡链路类型。无线网卡的 802.11 头比以太网头长,BPF 过滤器按以太网偏移解析,过滤出来全是无效结果。

解决:先用简单表达式验证,从 ip 开始,再加 arp,最后叠加 tcp port 443,这样能定位是表达式问题还是链路层问题。过滤结果为零时,可以把抓包数据落盘,用 WireShark 打开看链路层类型是不是 Ethernet,比对着代码猜快得多。

6. 进阶:按五元组做会话重组,还原一条 TCP 对话

包列表是一堆散件,日常排错更想看的是这条连接里到底说了什么。WireShark 的 Follow TCP Stream 就是这个功能,Sniffer 也可以自己实现一个简化版。思路不复杂:按五元组把包归到同一组,再按 TCP 序号拼回载荷。

6.1 五元组会话表:把乱序的包归成一条对话

QString sessionKey(const PacketMeta& m) { QStringList parts; parts << m.srcIp << QString::number(m.srcPort) << m.dstIp << QString::number(m.dstPort) << m.transProto; parts.sort(); // 双向方向统一成同一条会话 return parts.join("|"); }

排序是关键一步。客户端到服务器和服务器到客户端的包方向相反,不排序会被拆成两条会话。用一个 QHash<QString, QList > 存会话,界面左侧加一个会话列表,显示“源IP:端口 → 目的IP:端口”的摘要,选中某条会话就触发一次重组。

6.2 简化版 Follow TCP Stream:按序号拼 payload

做重组前,PacketMeta 里要补两个字段:tcpSeq(uint32)和 payload(QByteArray)。在 parse_packet 的 TCP 分支里,TCP 头偏移值在 data[offset+12] 的高 4 位,乘以 4 就是 TCP 头长度,从 TCP 头之后截取的就是应用层载荷。

QByteArray reassembleTcp(const QList<PacketMeta>& pkts) { QList<PacketMeta> sorted = pkts; std::sort(sorted.begin(), sorted.end(), [](const PacketMeta& a, const PacketMeta& b) { return a.tcpSeq < b.tcpSeq; }); QByteArray out; for (const PacketMeta& m : sorted) { out.append(m.payload); } return out; }

这是最简实现,没有处理 TCP 重传、乱序重叠和 SACK,生产级重组逻辑是 WireShark 里最复杂的部分之一。但对私有协议调试,只要确认抓包环境没有丢包,这个简化版已经能把一个 HTTP 请求还原成 GET / HTTP/1.1 这样看得懂的东西。验证方法很简单:抓一个访问本机服务的包,在会话列表里点开这条,看到的就是完整的请求头和响应体。

从那以后我调协议栈的习惯固定成三步:先看包列表掌握全局,再用会话重组看内容,最后用 BPF 过滤定位到具体连接,绝大多数玄学丢包都能变成可解释的日志。这份 Sniffer.zip 下载下来,从 main.cpp 开始读,把你自己的协议解析器挂进 packetparser.cpp,比空谈框架管用得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 10:20:43

Python逆向新番时间表接口,搭建追番数据监控系统

新番时间表这东西&#xff0c;追番党都知道&#xff0c;每周最怕的就是"今天周几、更新了没"这种灵魂拷问。我一开始用的是土办法&#xff0c;手动把每部番的播出时间抄进表格里&#xff0c;定好闹钟去刷。坚持了三个星期就崩了——不是忘了写&#xff0c;是根本刷不…

作者头像 李华
网站建设 2026/10/12 4:22:46

Jump Game V:从动态规划到拓扑排序的优化之路

前阵子在一家在线刷题平台上碰到一道困难题&#xff0c;题号 1340&#xff0c;名字叫 Jump Game V。说真的&#xff0c;第一次读完题目&#xff0c;我完全没搞清楚它和普通跳跃游戏有什么联系。题目标签写着动态规划&#xff0c;可如果直接套经典跳跃问题的思路&#xff0c;会掉…

作者头像 李华
网站建设 2026/10/16 6:50:53

全桥LLC变换器电压电流双环竞争控制:原理、设计与工程排坑

全桥LLC谐振变换器在新能源充电、通信电源、服务器电源里已经成了主流拓扑&#xff0c;但真正把“电压电流双环竞争控制”玩明白的人其实不多。很多刚接触这个方案的工程师第一反应都是&#xff1a;LLC不是调频率就行了吗&#xff1f;电压环加个PI就完事了&#xff0c;为什么还…

作者头像 李华
网站建设 2026/10/16 10:31:29

虚拟电厂多时间尺度调度复现:储能容量衰减建模与Matlab实现

最近项目里要评估一个虚拟电厂调度方案的效果&#xff0c;文献读下来之后发现&#xff0c;真正难的不在调度的数学框架&#xff0c;而在储能系统容量衰减——大多数公开的调度模型默认电池容量恒定&#xff0c;但实际运行时每一轮充放电都在悄悄损耗寿命。我们这次要复现的这篇…

作者头像 李华
网站建设 2026/10/16 14:28:30

订单取送序列计数:LeetCode 1359的递推与组合数学解法

1. 题目到底在问什么1.1 核心需求解析先把这个题翻译成人话。给定 n 笔订单&#xff0c;每笔订单有且只有两个动作&#xff1a;取件&#xff08;Pickup&#xff09;和送件&#xff08;Delivery&#xff09;。你要把这 2n 个动作排成一个执行序列&#xff0c;规则只有一个——同…

作者头像 李华
网站建设 2026/10/16 5:09:31

rundll32原理与安全运维实践:DLL函数调用机制详解

1. 项目概述&#xff1a;为什么 rundll32 是 Windows 系统里“最安静却最锋利”的那把刀你有没有遇到过这种情况&#xff1a;一个功能明明系统原生支持&#xff0c;却找不到对应可执行文件&#xff1f;比如想清空回收站、弹出光驱、打开网络连接属性窗口——这些操作在图形界面…

作者头像 李华