资讯动态

基于WinPcap的UDP发包程序:从报文构造到批量发送

发布时间:2026/9/28 2:29:40 来源:尧图企业网站定制
简介基于WinPcap实现的UDP发包示例程序面向网络编程初学者与需要自定义报文测试的开发者解决如何在用户态借助WinPcap直接构造并发送UDP数据包的问题可用于网络监控、协议验证与性能测试等场景。压缩包共349个文件约5.94MB除主程序源码.cpp/.h、Visual Studio工程文件.sln/.vcxproj与WinPcap静态库.lib/.a外还包含说明文档及一批js/css/html等前端资源可作为界面展示或演示辅助。资源已有187人学习下载。通过阅读WinPcap_UDP_Test源码可掌握WinPcap环境配置、设备列表枚举、网卡打开、数据包封装与发送调用等关键步骤在此基础上可自行修改目标地址、端口与载荷内容快速搭建UDP发包测试工具。整体目录结构清晰适合作为网络协议学习与二次开发的起点。1. 基于WinPcap的UDP发包程序解决的是socket做不到的那类场景做网络调试时最难受的一种情况是UDP 收发都正常但报文内容就是不对想从链路层看一眼数据甚至想把源 IP、TTL、分片偏移都改成内核不让改的值。基于 WinPcap 实现的 UDP 发包程序就是为这类场景准备的——它不走 socket不经过系统协议栈直接在网卡驱动层手工组装以太网帧、IP 头和 UDP 头然后交给 NPF 驱动发出去。这套源码在课程设计、协议栈实验和 udp 网络调试里非常常见核心思路也简单管好一张网卡句柄拼好一段原始帧调用一个发送接口。适合想搞清 UDP 报文到底长什么样、想复现异常报文、或者做点可控发包测试的人看。2. WinPcap发包原理与API选型pcap_open_live到pcap_sendpacket的完整链路2.1 绕过协议栈WinPcap在发包链路里的真实位置WinPcap 的核心是 NPFNetgroup Packet Filter驱动它挂在网卡驱动和 TCP/IP 协议栈之间。普通 socket 发送 UDP 数据时数据要经过应用层、传输层、网络层、链路层由内核帮你填充端口、IP 头、校验和最后才交给网卡。而基于 WinPcap 的 UDP 发包程序在链路层就接管了这一切pcap_open_live()打开一张网卡的句柄pcap_sendpacket()把一个已经拼好的完整以太网帧直接交给 NPF 驱动驱动不做任何协议处理原样把字节放到线缆上。这意味着三件事第一源 IP、源 MAC、TTL、IP 标识符、分片偏移这些字段全部由程序自己填内核不干预第二系统不会自动查路由表也不会自动做 ARP 解析目标 MAC 必须自己搞定第三UDP 校验和、IP 校验和、甚至超过 MTU 时的 IP 分片全部要自己算。理解了这个位置就不会拿着这个方案去和 socket 比「谁更简单」它解决的是「谁能改报文」的问题。2.2 为什么不用socket而用WinPcap控制面和数据面的差别常见的技术问答里总在对比 TCP 和 UDP 的区别但在这个项目里真正的问题不是 TCP 还是 UDP而是「操作系统允不允许你修改报文结构」。socket 方式下你想修改 IP 头的 TTL 得用setsockopt(IP_TTL)想改源 IP 得用IP_PKTINFO想控制分片偏移基本没门。WinPcap 的做法不同整个报文在用户态拼好IP 头里哪个字节是什么都由你说了算。这也正是配套源码常见的用途模拟一个源 IP 伪造的 UDP 报文、复现「TTL 为 1 时路由器回超时」的实验、在 udp 网络调试中给对端制造畸形报文看看对方协议栈会不会翻车。代价也很直接——所有内核原本免费提供的服务都变成你的义务。网上很多基于 WinPcap 的 UDP 发包源码看起来就几十行但初学者把报文拼完发出去收不到响应绝大多数都是栽在 MAC 地址和校验和这两个义务上。2.3 API选型pcap_sendpacket、pcap_inject还是pcap_sendqueueWinPcap 给出发送能力的主要有三个接口pcap_sendpacket()、pcap_inject()和pcap_sendqueue_transmit()。pcap_inject()和pcap_sendpacket()功能基本重叠都是立刻发一个原始帧区别只是返回值语义不同一般做单包实验用pcap_sendpacket()就够了pcap_sendqueue则是把大批帧先排队再一次性刷出去适合压测和连续发包场景后面专门讲。先看一个能跑的最小骨架#include pcap.h #include winsock2.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, ws2_32.lib) int main(void) { pcap_t *handle; char errBuf[PCAP_ERRBUF_SIZE]; unsigned char pkt[64] {0}; // 先放一帧空的第三章会填充真实报文 // 设备名建议先通过 pcap_findalldevs() 枚举得到不要像我这样写死 handle pcap_open_live(\\Device\\NPF_{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}, 65535, 1, 1000, errBuf); if (handle NULL) { fprintf(stderr, open failed: %s\n, errBuf); return -1; } if (pcap_sendpacket(handle, pkt, sizeof(pkt)) ! 0) { fprintf(stderr, send failed: %s\n, pcap_geterr(handle)); pcap_close(handle); return -1; } pcap_close(handle); return 0; }代码逻辑很简单打开设备 → 发一帧 → 关闭。关键在参数。pcap_open_live()的四个参数分别是设备名、抓包长度snaplen、混杂模式开关、超时毫秒数。snaplen填 65535 是为了保证完整接收整帧混杂模式这里填 1实际对发包不是必须但如果你同时开着 Wireshark 验证开着混杂模式会方便to_ms填 1000 表示读超时对pcap_sendpacket的影响不大但对抓包线程有影响。返回值是pcap_t*一旦是 NULL错误信息就在errBuf里最典型的错误就是 NPF 驱动没起来也就是后面避坑章节要解决的问题。3. UDP报文手工组装从结构体定义到校验和计算的源码实现3.1 以太网头、IP头、UDP头的结构体怎么定义手工拼报文最容易出错的就是结构体对齐。网卡是按字节流发数据的C 语言结构体默认会按 4 字节对齐填充所以必须先#pragma pack(push, 1)按 1 字节对齐否则你算的偏移量全对不上。一段常见的帧结构定义长这样#pragma pack(push, 1) typedef struct _EthHdr { unsigned char destMac[6]; // 目标 MAC注意是数组没有字节序问题 unsigned char srcMac[6]; // 源 MAC unsigned short etherType; // 0x0800 表示 IPv4写入时用 htons() } EthHdr; typedef struct _IpHdr { unsigned char verIhl; // 高 4 位版本4低 4 位首部长度5 unsigned char tos; // 服务类型一般填 0 unsigned short totalLen; // IP 头 UDP 头 数据网络字节序 unsigned short id; // 标识字段可随便填分片时会用到 unsigned short fragOffset; // 标志位 片偏移不分片填 0 unsigned char ttl; // 64 或 128 都可 unsigned char protocol; // 17 表示 UDP unsigned short checkSum; // IP 头校验和 unsigned int srcIp; // 源 IP用 inet_addr() 转 unsigned int destIp; // 目的 IP } IpHdr; typedef struct _UdpHdr { unsigned short srcPort; // 源端口 unsigned short destPort; // 目的端口 unsigned short udpLen; // UDP 头 8 字节 数据长度 unsigned short checkSum; // UDP 校验和伪头部参与计算 } UdpHdr; #pragma pack(pop)三个结构体拼起来刚好 14 20 8 42 字节再加上 payload 就是完整的以太网帧。几个特别容易错的地方etherType必须htons(0x0800)写反了网卡不会识别成 IPv4verIhl用0x45表示 IPv4 且无选项IP 头里的totalLen是整个 IP 包长度而不是 UDP 长度。MAC 地址用 6 字节数组最省心如果你用unsigned long long去存再截断调试起来会非常痛苦。3.2 校验和伪头部是UDP校验和不灵光的根源IP 校验和算法很简单把要校验的数据按 16 位一组累加累加过程中溢出的进位回卷加到低位最后取反。UDP 校验和唯一的坑在于它不只校验 UDP 头和数据还要加上一个 12 字节的伪头部里面包含源 IP、目的 IP、协议号和 UDP 长度。伪头部不真正发到线上只参与校验和计算很多源码发出去的包校验和不对就是漏了这一步。unsigned short calCheckSum(unsigned short *buf, int len) { unsigned int sum 0; while (len 1) { sum *buf; len - 2; } // 奇数长度时把最后一个字节补到低位 if (len 1) sum *(unsigned char *)buf; // 把 16 位以上的高半部分回卷到低 16 位 while (sum 16) sum (sum 0xFFFF) (sum 16); return (unsigned short)(~sum); }注意这段代码假定运行环境是大端解释 16 位字而网络字节序本身就是大端所以计算出的校验和直接赋值给结构体字段即可不要再做一次htons()这是很多初学项目里最容易出现的「双重转换翻车点」。UDP 校验和的完整计算必须把伪头部、UDP 头、数据连续放在一段缓冲区里一次算完常见做法是#define IPPROTO_UDP 17 // udpLen 8 payloadLen unsigned char udpBuf[12 8 1500]; memset(udpBuf, 0, sizeof(udpBuf)); // 12 字节伪头部 memcpy(udpBuf, srcIp, 4); memcpy(udpBuf 4, destIp, 4); udpBuf[8] 0; // 保留字节 udpBuf[9] IPPROTO_UDP; // 协议号 udpBuf[10] (unsigned char)((8 payloadLen) 8); udpBuf[11] (unsigned char)(8 payloadLen 0xFF); // UDP 头 8 字节 memcpy(udpBuf 12, udpHdr, 8); // 载荷 memcpy(udpBuf 20, payload, payloadLen); udpHdr.checkSum calCheckSum((unsigned short *)udpBuf, 12 8 payloadLen);上面这段里srcIp和destIp必须是inet_addr()返回的字节序也就是已经是网络字节序才能直接memcpy。伪头部第 8 字节必须清零协议号填 17UDP 长度字段在第 10、11 字节按网络字节序放。UDP 校验和有一个特殊约定如果计算结果为 0发送方可以把它填成 0xFFFF因为 0 在接收端会被当作「不校验」处理。实测里很多实现偷懒把校验和置 0 也能发出去但抓包工具会给你标一行「checksum 未验证」接收端行为也取决于协议栈所以源码里还是算对比较稳妥。3.3 主流程查网卡、填MAC、算校验和、发包的完整骨架把上面的结构体和校验和函数拼起来核心发包流程长这样#include pcap.h #include winsock2.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, ws2_32.lib) int sendUdpPacket(pcap_t *handle, const char *srcIpStr, const char *dstIpStr, const unsigned char *destMac, unsigned short srcPort, unsigned short dstPort, const unsigned char *payload, int payloadLen) { unsigned char sendBuf[1514] { 0 }; EthHdr eth; IpHdr ip; UdpHdr udp; unsigned char *p sendBuf; // 目标 MAC 要自己填WinPcap 不查 ARP memcpy(eth.destMac, destMac, 6); // 源 MAC 这里用 00:11:22:33:44:55 占位真实项目用 GetAdaptersInfo 读本机 unsigned char localMac[6] { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 }; memcpy(eth.srcMac, localMac, 6); eth.etherType htons(0x0800); ip.verIhl 0x45; ip.tos 0; ip.totalLen htons(20 8 payloadLen); ip.id htons(0x1234); ip.fragOffset 0; ip.ttl 64; ip.protocol IPPROTO_UDP; ip.srcIp inet_addr(srcIpStr); ip.destIp inet_addr(dstIpStr); ip.checkSum 0; ip.checkSum calCheckSum((unsigned short *)ip, 20); udp.srcPort htons(srcPort); udp.destPort htons(dstPort); udp.udpLen htons(8 payloadLen); udp.checkSum 0; // 伪头部 UDP 头 数据一起算校验和 unsigned char udpBuf[12 8 1500] { 0 }; memcpy(udpBuf, ip.srcIp, 4); memcpy(udpBuf 4, ip.destIp, 4); udpBuf[8] 0; udpBuf[9] IPPROTO_UDP; udpBuf[10] (unsigned char)((8 payloadLen) 8); udpBuf[11] (unsigned char)(8 payloadLen 0xFF); memcpy(udpBuf 12, udp, 8); memcpy(udpBuf 20, payload, payloadLen); udp.checkSum calCheckSum((unsigned short *)udpBuf, 12 8 payloadLen); // 拼接最终帧 memcpy(p, eth, 14); p 14; memcpy(p, ip, 20); p 20; memcpy(p, udp, 8); p 8; memcpy(p, payload, payloadLen); return pcap_sendpacket(handle, sendBuf, 14 20 8 payloadLen); }这个函数返回pcap_sendpacket()的结果0 表示驱动层接受非 0 表示发送失败具体错误可以再打pcap_geterr()。参数里最需要留意的就是destMac——任何基于 WinPcap 的 UDP 发包程序都不会替你解析目标 IP 对应的 MAC常见做法是先ping通目标主机再用arp -a看缓存或者在代码里调用SendARP()动态解析。MAC 填错的结果很直观本机网卡发出去了但交换机会把帧送到错误端口对端永远收不到这个现象放到后面避坑章节详细说。4. 编译WinPcap发包项目的VS配置依赖库、运行库和NPF驱动的四个必调项4.1 include、lib和wpcap.dll的路径怎么配拿到一份基于 WinPcap 的 UDP 发包程序源码第一件事不是看代码而是确认开发包 WpdPack 是否就位。这个开发包里最常见的两个目录是Include和Lib分别是头文件和导入库的位置。在 Visual Studio 工程属性里把Include目录加到「VC 目录 - 包含目录」把Lib目录加到「库目录」链接器输入里加上wpcap.lib和ws2_32.lib。代码里用#pragma comment(lib, wpcap.lib)也是一种等价做法适合不想改工程文件的场景。运行环境同样要管wpcap.dll和packet.dll需要出现在可执行文件旁边或者把 WpdPack 的bin目录加进系统 PATH。一个常见的困惑是编译通过但运行时报「找不到 wpcap.dll」这就是运行时依赖没配置好。另外要注意 32 位和 64 位版本的匹配工程是 x64 就用 64 位版本的wpcap.lib混用会直接在链接阶段报一堆 LNK2019而不是等到运行时才暴露。4.2 预处理、代码生成与字符集三个编译坑老项目的pcap.h里有一部分远程抓包接口是被宏控制起来的比如pcap_findalldevs_ex()需要先定义HAVE_REMOTE否则头文件里根本不声明。如果你拿到的源码用了远程接口预处理定义里必须加HAVE_REMOTE。这个坑藏得很深报错通常是C2065: pcap_findalldevs_ex: 未声明的标识符。代码生成设置也要改。默认的/MD是动态链接 C 运行库如果你的目标机器没有装对应 VC 运行库程序会在别的机器上当场翻车。我一般会把「代码生成 - 运行库」改成多线程/MT这样 C 运行库静态打进去部署时少一层依赖。字符集这一项更隐蔽很多老源码用char*处理设备名和 IP 地址工程默认 Unicode 字符集会把窄字符转成宽字符调用出现inet_addr参数类型不匹配。直接把字符集切成「多字节字符集」最快改动最小。4.3 驱动装不上时程序如何定位是编译问题还是环境问题编译配置全做完不代表能发包因为真正的引擎 NPF 驱动需要单独安装。有一个很实用的定位原则如果程序在pcap_open_live()就返回 NULL十有八九是环境问题而不是你的报文构造问题。先用命令行确认服务状态sc query npf net start npfsc query npf能看到 NPF 服务是 RUNNING 还是 STOPPED。如果服务处于已停止状态net start npf以管理员身份执行可以尝试拉起。这个命令是区分「源码问题」和「环境问题」的分界线——服务起不来后面所有发包逻辑都白搭。winpcap 安装失败大多卡在这一环现象是安装向导到一半报错、或者服务状态一直停在 STOPPED具体排查放到下一章。5. WinPcap发包程序避坑安装npf错误、抓不到包与包发不出去的排查记录5.1 安装到最后提示NPF驱动无法启动现象WinPcap 安装向导快结束时弹出错误提示安装失败或驱动无法启动程序里pcap_open_live()永远返回 NULLerrBuf里写着类似Error opening adapter: The system cannot find the device specified的信息。原因最常见的是系统里残留了旧版 WinPcap 或卸载不干净NPF 服务被标记成禁用其次是安装时没有管理员权限驱动服务根本没有注册成功在新版 Windows 上老版 NPF 驱动还可能撞上驱动签名强制策略。解决先以管理员身份卸载旧版重启再以管理员安装新版。安装后立刻用sc query npf看服务状态如果还是 STOPPED 且net start npf报错检查设备管理器「查看 - 显示隐藏的设备 - 非即插即用驱动程序」里 NPF 是否带感叹号。对于驱动签名导致的安装失败实验机器上可以临时进入高级启动选项关闭驱动签名强制或者直接换用 Npcap 并开启 WinPcap 兼容模式——API 基本不用改这是老驱动在 Win10 以上系统里最常用的替代路径。5.2 pcap_findalldevs返回空列表或只有localhost现象程序枚举网卡时返回值是 0 但设备链表里只有一个空条目或者一个物理网卡都看不到。原因设备列表为空几乎都是 NPF 服务没有运行。另外还有一种情况pcap_findalldevs()在旧版 WinPcap 里如果没有预先定义HAVE_REMOTE函数声明会缺失导致编译失败而编译通过了却返回空则要怀疑是不是用错了接口——某些老代码用了pcap_findalldevs_ex(PCAP_SRC_IF_WIRELESS, ...)远程抓包扩展没启用时会出现异常行为。解决先net start npf启动服务再跑枚举。如果服务是启动的检查网卡是否被禁用——特别是笔记本的无线网卡被硬件开关关掉时设备列表里自然看不到。最后检查代码里是否混用了pcap_findalldevs和pcap_findalldevs_ex统一使用同一种接口。5.3 程序返回0但Wireshark抓不到帧现象pcap_sendpacket()返回 0驱动说发出去了但 Wireshark 在同网卡上过滤udp或直接看全部流量就是找不到这一帧。原因我遇到这个现象时第一反应是 Wireshark 抓的接口选错了——笔记本上往往有物理网卡、虚拟网卡、WLAN 多个接口抓到 eth0 而发送在 WLAN 上自然看不到。另一个高频原因是无线网卡很多无线网卡驱动不允许应用层直接注入 802.11 帧pcap_sendpacket返回 0 但帧在驱动层就被丢弃或改写。还有一种是 Wireshark 本身没开混杂模式导致它只收发给本机的帧。解决在 Wireshark 里确认选中的是发送句柄对应的那个接口并把抓包选项里的混杂模式打开。如果确认接口无误还是抓不到优先怀疑无线网卡换有线网卡或 USB 网卡重测。这个现象一度被群里传成「玄学」其实就是链路层注入和无线电管理机制之间的冲突。5.4 UDP包发出去了接收端就是收不到现象发送端pcap_sendpacket()返回 0发送端 Wireshark 也能看到帧对端也开着抓包工具但对端抓不到或者抓到了但 UDP 应用层不处理。原因这里最大的坑是 ARP。WinPcap 不查 ARP 缓存你填的destMac如果是从某个旧缓存里抄来的、或者在虚拟机快照恢复后已经过期交换机会把帧转发给错误的物理端口。还有一种情况destMac填成了网关的 MAC但目的 IP 是同网段主机的 IP帧到网关后直接被丢弃。解决先ping一下目标主机让 ARP 缓存刷新再用arp -a查到正确的 MAC 填进代码。这是处理这类问题最有用的「后悔药」——一发 ping 下去整个链路问题瞬间分成两半MAC 错了是链路层问题MAC 对了还收不到就是 IP 或 UDP 层问题。同时建议收发两端都开抓包一步步确认帧到底停在哪个环节。5.5 payload超过MTU后包发不出去或对方组包失败现象payload 在 1200 字节左右时一切正常把 payload 加到 1800 字节后发送端返回 0但接收端一片空白或者抓包工具里看到的是多个奇怪的 IP 分片包。原因标准以太网 MTU 是 1500 字节去掉 20 字节 IP 头和 8 字节 UDP 头UDP payload 最大是 1472 字节。基于 WinPcap 的 UDP 发包程序不做 IP 分片——这是内核协议栈的职责驱动只负责把帧原样发出去。payload 超过 MTU 后网卡可能拒发也可能发出很多重叠的坏分片。解决把 payload 控制在 1472 字节以内先跑通全链路这是最可靠的边界。如果一定要发大数据报就得自己实现 IP 分片把 payload 切成 1480 字节的片每片构造独立 IP 头fragOffset按 8 字节为单位递增最后一个分片清掉 MF 标志位其余分片置 1然后逐片调用pcap_sendpacket()。这个实现里最容易写错的是fragOffset的单位——它表示「8 字节块」而不是字节数。6. 用pcap_sendqueue做批量UDP发包吞吐验证与压测技巧6.1 从pcap_sendpacket到pcap_sendqueue_transmit单包发送的场景用pcap_sendpacket()足够但如果你想看这条链路到底能打多少 UDP 包一次一次调用用户态 API 会卡在调用开销上。WinPcap 的pcap_sendqueue就是为此设计的先在用户态把成百上千个帧排队再一次性交给内核减少用户态和内核态切换次数。用起来是四步pcap_send_queue *q; struct timeval ts; int pktCount 1000; int pktLen 60; // 60字节小包适合做纯吞吐测试 // 第一步按总字节数申请队列 q pcap_sendqueue_alloc(pktCount * pktLen); if (q NULL) { fprintf(stderr, queue alloc failed\n); return -1; } // 第二步按包入队包内容可以复用同一个缓冲区 ts.tv_sec 0; ts.tv_usec 0; for (int i 0; i pktCount; i) { pcap_sendqueue_queue(q, ts, sendBuf, pktLen); } // 第三步一次性发送sync传0表示异步 int sent pcap_sendqueue_transmit(handle, q, 0); if (sent -1) { fprintf(stderr, transmit failed: %s\n, pcap_geterr(handle)); } // 第四步销毁队列 pcap_sendqueue_destroy(q);pcap_sendqueue_alloc()的参数是队列能容纳的总字节数而不是包数这点最容易算错1000 个 60 字节包就是 60000 字节。pcap_sendqueue_queue()内部会拷贝数据所以sendBuf可以循环复用。pcap_sendqueue_transmit()返回的是成功发送的字节数出错了才是 -1判断返回值时别当成包数去读。这种发法在测试工具里还有个明显特征接收端会看到一串几乎连续到达的 UDP 包时间戳间隔很小不像单包循环那样一卡一卡。6.2 用Wireshark和udp测试工具做双向验证批量发包后必须验证两件事包确实发出去了以及对端真的收到了。我常用的组合是 Wireshark 加一个能监听 UDP 的小工具。Wireshark 里过滤udp.port 9000能看到每个包的源端口、目的端口、length 字段还能直接看校验和是否被标记成无效。另一端用一个支持 ASCII 命令输入的 udp 测试工具起监听收到包后回显就能确认从链路层绕道上来的包最终能到达应用层。如果想定量验证吞吐可以在同一条链路上用 iperf3 的 UDP 模式打一次流作为参照。在相同包大小和相同持续时长下WinPcap 的 sendqueue 批量发包和 iperf3 的实测带宽应该在同一量级如果差距太大优先检查是不是加载了混杂模式、或者网卡的中断调节把批处理拆散了。最后给自己留一个习惯第一次跑通永远用 64 字节小包链路没有任何丢包再加到 1472 字节再上 sendqueue 批量压测——我早期跳过第一步直接跑大包校验和、MAC、MTU 三个问题叠在一起排查效率低到不想回忆。先小后大、先单后批这个顺序能帮你省掉大量冤枉时间希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑