简介:这是一份基于WinPcap实现的UDP发包示例工程,适合学习网络编程、理解UDP协议以及上手WinPcap库的开发者参考。压缩包共349个文件,大小约5.94MB,核心为C/C++源码(.h、.cpp)与Visual Studio工程文件(.sln、.vcxproj、.filters),另附静态库文件(.lib、.a)以及若干js/css/html辅助页面和图片素材,解压后结构完整,便于直接打开编译调试。已有188人学习浏览。源码围绕winpcap-udp-test示例展开,演示如何初始化WinPcap、获取本机网卡并构造UDP数据报进行发送,可帮助读者理解操作系统底层网络封包捕获与发送机制;UDP无连接、低延迟的特点使其适用于音视频传输、在线游戏等场景,读者也能在此基础上修改报文内容或重写发送逻辑,开展自己的网络实验与二次开发。
1. 基于WinPcap的UDP发包程序:为什么还要自己拼数据包
如果你用普通Socket发UDP,系统会帮你把IP头和UDP头都封装好,Wireshark抓到的包规规矩矩,但这也就意味着你看不到底层到底发生了什么。而这个基于WinPcap实现的UDP发包程序,最大的价值在于它绕过了操作系统协议栈,直接从数据链路层把一帧完整的以太网报文发出去——从目的MAC、源MAC,到IP头、UDP头,再到你填的负载数据,全都在自己手里握着。适合两类人:一类是刚入门网络协议、想知道“UDP包在线上到底是什么形态”的初学者;另一类是做网络测试、需要伪造源地址或构造特定报文的开发。这个项目是个很小的示例,但麻雀虽小,WinPcap拿网卡、开适配器、发原始帧这条链路是完整的。
2. 工程结构与编译环境:拿到源码先别急着跑
2.1 压缩包里都有什么
这个包里的文件结构和常见的VC++工程不太一样,它不是一堆.cpp摆在明面上,而是带着编译产物一起给的。看文件清单能发现几个关键东西:libwpcap.a和libpacket.a是WinPcap的静态库,前者是对外的主要API库,后者是底层Packet API库;.aps文件是VC++工程资源的二进制备份,说明这个项目在Visual Studio里被打开过。时间戳集中在2014年2月底的一天内,说明这是一个短时间内写出来的功能验证程序,不是那种上万行的工业级工具。
典型的文件构成和使用思路是这样的:
| 文件 | 作用 |
|---|---|
libwpcap.a | WinPcap核心API的静态库,提供pcap_open_live、pcap_sendpacket等接口 |
libpacket.a | 底层Packet.dll的导入库,普通开发一般不直接调用 |
WinPcap_UDP_Test.aps | VC++工程资源的附加产物,可以忽略 |
| 若干源码文件 | 发行包里一般有main.cpp、udp_packet.h之类的核心代码 |
注意一个坑:libwpcap.a这个后缀,在Windows下一般是给MinGW或Cygwin用的。如果你打算用Visual Studio直接打开工程,VC++默认认的是wpcap.lib,所以要确认编译工具链是MinGW还是MSVC,这决定了后面链接库时写的是.a还是.lib。
2.2 环境准备与基础库安装
跑这个项目之前,WinPcap的运行环境必须先装好。WinPcap由两部分组成:一个驱动服务(负责抓包和发包)和一个用户态库(wpcap.dll)。2014年的项目,现在装的话建议装WinPcap 4.1.3(最终版)或者换成Npcap(WinPcap的继任者,兼容模式能跑大部分老代码)。装完之后检查一下C:\Windows\System32\wpcap.dll是否出现,没有的话程序运行时会直接报“找不到wpcap.dll”。
如果你要在MinGW下自己编译,目录结构建议这样摆:
winpcap_udp_test/ ├── src/ # 源码目录(main.cpp、packet_builder.cpp等) ├── include/ # WinPcap的头文件(pcap.h、pcap-stdinc.h等) ├── lib/ # libwpcap.a、libpacket.a放在这里 └── Makefile编译命令大致是这样:
g++ -o udp_send.exe src/main.cpp \ -I./include \ -L./lib \ -lwpcap -lpacket \ -lws2_32这里链接ws2_32是因为WinPcap在Windows上和Winsock有交互(比如pcap_lookupnet里用到了socket地址转换)。如果你不链这个库,编译会报一堆inet_ntoa之类的未定义引用,这是很多人第一次编译时就翻车的点。
3. 核心模块拆解:从设备列表到链路层帧的完整组装
3.1 设备和网卡选择逻辑
UDP发包程序的第一步是拿到一张能发包的网卡。WinPcap负责这个的是pcap_findalldevs,它会枚举机器上的所有网络接口,返回一个pcap_if_t链表。每个节点里有两个字段必须注意:name是供pcap_open_live使用的设备名,description是给用户看的描述。如果程序里没有网卡选择这一步,直接硬编码\Device\NPF_{GUID},换个机器就废了。
常见的枚举实现长这样:
pcap_if_t *alldevs; char errBuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(&alldevs, errBuf) == -1) { fprintf(stderr, "枚举网卡失败: %s\n", errBuf); return -1; } for (pcap_if_t *dev = alldevs; dev != NULL; dev = dev->next) { printf("设备名: %s\n", dev->name); if (dev->description) printf("描述: %s\n", dev->description); }pcap_findalldevs会同时返回回环接口和物理网卡,如果你在虚拟机里跑,经常能看见一堆“Adapter for loopback traffic capture”。选设备的原则是:要发出去的包最终走哪块物理网卡,就选哪个设备的name。比如要发给局域网里另一台机器,就选连着交换机的以太网卡,别选回环接口,因为回环接口不走真实的以太网线。
3.2 打开设备与发送通道的初始化
选定设备之后,下一步是打开它。WinPcap打开设备的函数是pcap_open_live,这个函数会设置网卡为混杂模式(如果传入PCAP_OPENFLAG_PROMISCUOUS),并把驱动切换到抓包状态。但这个程序里有个区别:发包其实不需要混杂模式,混杂模式是为了抓别人的包才开的,所以这里传0(非混杂)在纯发包场景下就够了。
pcap_t *handle = pcap_open_live(devName, 65536, 0, 1000, errBuf); if (handle == NULL) { fprintf(stderr, "无法打开设备 %s: %s\n", devName, errBuf); return -1; }参数说明:
devName:上一步pcap_findalldevs拿到的设备名字符串。65536:抓包快照长度,发包场景用不到抓包,但这是函数签名必填项,填个MTU最大值最保险。0:promisc标志,这里关闭,表示不抓别人流量。1000:读取超时毫秒数,发包不依赖这个,但传0在某些老驱动下会有问题。errBuf:失败时错误信息会写到这里,固定大小PCAP_ERRBUF_SIZE(256字节)。
打开之后有个隐蔽问题:这块网卡如果正在被Wireshark占用(开启抓包时),pcap_open_live会返回失败,因为NPF驱动同一时间只允许一个进程独占打开。这是WinPcap二十多年没变过的限制,程序里没有做重试的话,把Wireshark的抓包停掉就能解决。
3.3 UDP报文构造:从MAC头到负载数据的逐字节拼装
这一步是整个项目最核心的代码,也是用WinPcap发包和用Socket发包的本质区别所在。Socket发包时,操作系统帮你填充MAC地址(通过ARP缓存)、IP头校验和、UDP长度,你只给数据就行。但现在我们要自己写一个字节数组,把一帧完整的以太网包拼出来。
#pragma pack(push, 1) typedef struct eth_header { uint8_t dest_mac[6]; uint8_t src_mac[6]; uint16_t ether_type; // 0x0800 表示IPv4 } eth_header_t; typedef struct ip_header { uint8_t version_ihl; // 版本4位 + 首部长度4位 uint8_t tos; uint16_t total_len; uint16_t ident; uint16_t flags_frag; uint8_t ttl; uint8_t protocol; // 17表示UDP uint16_t checksum; uint32_t src_addr; uint32_t dst_addr; } ip_header_t; typedef struct udp_header { uint16_t src_port; uint16_t dst_port; uint16_t length; uint16_t checksum; } udp_header_t; #pragma pack(pop)#pragma pack(push, 1)非常关键,它让编译器按1字节对齐结构体,不加的话编译器会按4字节对齐,在结构体字段之间塞填充字节,报文就全乱了。以太网帧头14字节(6+6+2),IP头20字节,UDP头8字节——这三个数加起来42字节,正好是UDP包在以太网上的最小头长度,如果你拆包后看到帧头长度不是42,说明对齐没设对。
3.4 组装完整帧并调用pcap_sendpacket
结构体定义好之后,要把它们填到一块内存里。这个项目里可以按地址偏移直接操作,也可以把结构体外层套个总帧结构体,我一般用后一种,代码更容易读:
uint8_t frame[14 + 20 + 8 + payload_len]; eth_header_t *eth = (eth_header_t *)frame; ip_header_t *ip = (ip_header_t *)(frame + 14); udp_header_t *udp = (udp_header_t *)(frame + 14 + 20); uint8_t *payload = frame + 14 + 20 + 8; // 填充MAC地址 memcpy(eth->dest_mac, dest_mac, 6); memcpy(eth->src_mac, src_mac, 6); eth->ether_type = htons(0x0800); // 填充IP头 ip->version_ihl = 0x45; // IPv4,20字节首部 ip->total_len = htons(20 + 8 + payload_len); ip->protocol = 17; // UDP ip->src_addr = htonl(src_ip); ip->dst_addr = htonl(dst_ip); // 填充UDP头 udp->src_port = htons(src_port); udp->dst_port = htons(dst_port); udp->length = htons(8 + payload_len); udp->checksum = 0; // 按照标准可以直接填0,但建议算 // 发送 if (pcap_sendpacket(handle, frame, sizeof(frame)) != 0) { fprintf(stderr, "发包失败: %s\n", pcap_geterr(handle)); }这段有三个容易写错的字段,挨个说明一下:
ip->total_len是整个IP包的长度,包含IP头本身,所以是20+8+payload_len,写错的话接收端会认为包不完整直接丢。udp->length是UDP头+数据的总长,即8+payload_len,这个值和IP头的total_len不要混淆。udp->checksum严格来说在IPv4下是可选的(接收端可以忽略),但很多网卡和系统默认开了UDP校验和验证,如果你填0,Wireshark会显示“UDP checksum offload”或直接标红,数据包仍然能发出去且对端能收到,但抓包分析时很不干净。
pcap_sendpacket内部会直接把这个帧交给NPF驱动,驱动从网卡队列里发出去。注意这个函数返回0才是发送成功,跟socket编程的习惯相反(send返回字节数>0才算成功),很多人刚上手时判断条件写反了,包发不出去还一脸懵。
3.5 校验和计算的正确姿势
上面的代码里校验和填0能跑,但作为一个认真做网络协议的测试工具,最好把IP校验和和UDP校验和都算出来。校验和算法很简单:把所有16位对齐的值相加,超出16位的部分回卷相加,最后取反。IP头校验和只覆盖IP头本身,UDP校验和覆盖UDP头、数据和一个伪IP头(伪头包含源/目的IP和协议字段,用于防止包被错误路由)。
uint16_t checksum(uint16_t *addr, int len) { uint32_t sum = 0; while (len > 1) { sum += *addr++; len -= 2; } if (len == 1) sum += *(uint8_t *)addr; while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return (uint16_t)(~sum); }这个函数是经典的RFC 1071实现,只依赖32位累加器,不依赖平台字节序,因为最终取反时用ntohs转换由调用方负责。UDP校验和需要构造伪头,伪头长这样:12字节(源IP4字节+目的IP4字节+0字节+协议1字节+UDP长度2字节),和UDP头、数据一起参与校验计算。伪头不发送到线上,只参与计算,这是包构造里最容易忘的一块。
4. 常见问题排查与避坑指南:WinPcap发包的四大翻车现场
4.1 “发送成功”但Wireshark里什么都抓不到
现象:程序在管理员权限下运行,pcap_sendpacket返回0,但Wireshark里就是看不到自己发的UDP包。
原因:分两种。第一种是Wireshark抓包时机不对——WinPcap发包时网卡是直接发送的,Wireshark如果是在另一块网卡上抓包(比如你发到以太网卡,但Wireshark监听在WiFi卡上),自然看不到。第二种是驱动层面丢包——很多USB外置网卡和老版本WinPcap驱动不兼容,包进了驱动队列但没真正上线路。
解决:先确认Wireshark监听的接口和pcap_open_live选的是同一个name;再关掉Wireshark的抓包,改用这个发包程序自收自发(发完立刻用pcap_next_ex去读同一块网卡),如果自己发自己收也看不到,基本就是驱动问题,换Npcap的WinPcap兼容模式重装驱动。
4.2 编译时出现大量“未定义引用”错误
现象:MinGW编译时,链接阶段报pcap_open_live、pcap_sendpacket未定义,但头文件明明包含了。
原因:库链接顺序不对。GNU工具链的静态库有依赖顺序,libpacket.a依赖libwpcap.a,而libwpcap.a又依赖libws2_32,你如果写成-lpacket -lwpcap -lws2_32就没事,写反了GCC就会丢符号。
解决:把库顺序固定成从底层到高层依赖关系排列:
g++ -o udp_send.exe main.c -lwpcap -lpacket -lws2_32如果还报错,就在命令末尾加-Wl,--start-group -lwpcap -lpacket -Wl,--end-group强制循环解析。
4.3 程序一打开就是“找不到网卡设备”
现象:在Windows 10/11上运行,pcap_findalldevs返回的设备列表为空,或者只有loopback接口。
原因:WinPcap的老驱动(4.1.3之前的版本)不支持新系统的网络栈。另一个隐藏原因是你装了Npcap但没有勾选“WinPcap API兼容模式”,这个选项默认不勾。
解决:卸载WinPcap,安装Npcap时勾上“Install in WinPcap API-compatible Mode”。装完重新跑程序,设备列表就正常了。
4.4 构造的包在Wireshark里显示“malformed”或“bad checksum”
现象:包发出去了,Wireshark也能看到,但解析不出来,报IP长度错误或校验和错误。
原因:绝大多数是htons/htonl用错了地方。以太网和IP头里的多字节字段在网络字节序是大端,而x86机器是小端,你要发的数字必须转成大端再写入。忘转的典型症状是:UDP目标端口填的5000,抓包里显示12880之类的怪数字。
解决:给所有写到结构体里的多字节字段加上htons或htonl,包括ether_type、total_len、udp->port、src_addr。注意version_ihl是单字节的0x45,不用转。
4.5 本机能收到,但目标机器收不到
现象:发包程序跑通,Wireshark本机也验证了包在线上的格式正常,但接收方的应用说没收到。
原因:目的MAC地址填错了,或目的IP不是本网段。如果你把目的MAC填成了网关的MAC,但目的IP填的是同网段另一台机器的IP,数据包会到达网关后被网关丢弃(因为网关发现这个IP不在它的路由表内)。反过来,如果目的IP是跨网段地址,但MAC填的是本网段主机的MAC,那包就只在那台主机上转一圈,上不了外网。
解决:写死MAC之前,用ARP先拿到目标IP对应的真实MAC。最简单的方法是在发包程序里加一个步骤:调用系统arp -a命令查询,或者直接假设目标在同网段,把MAC填成广播地址FF:FF:FF:FF:FF:FF,让交换机帮你广播出去,目标网卡收到广播帧再解包认IP。
5. 运行验证与效果观察:拿Wireshark当照妖镜
5.1 启动程序前的准备工作
这个程序没有图形界面,是个控制台工具。运行之前需要把三件事准备好:第一,用管理员身份打开命令提示符,因为WinPcap的NPF驱动要求程序提权才能访问网卡;第二,关闭正在跑Wireshark或其他占用网卡的抓包程序;第三,确定接收方——可以是本机的另一个IP,也可以是局域网里随便一台机器,但发跨网段包时要确认路由器允许转发。
启动后程序一般会打印出找到的网卡列表,让你输入序号选择,然后询问源IP、目的IP、端口、负载内容、发包数量。有的版本是硬编码的参数在源码里,需要重新编译才能改,看你拿到的源码里main函数怎么写的。
典型运行效果如下(举例):
[1] 以太网 - Realtek PCIe GBE [2] 本地连接 - VirtualBox Host-Only 请选择网卡编号: 1 请输入源IP地址: 192.168.1.100 请输入目的IP地址: 192.168.1.200 请输入源端口: 12345 请输入目的端口: 6789 请输入发送次数: 10 开始发送... 已发送 10 个UDP数据包5.2 Wireshark里验证包格式
发包的同时打开Wireshark监听同一块网卡,过滤规则直接写udp.port == 6789或者ip.src == 192.168.1.100。双击抓到的包,展开Ethernet II、Internet Protocol Version 4、User Datagram Protocol三层,手动核对几个关键字段:
- Ethernet层:Source显示的是你填的源MAC,Type是IPv4(0x0800)。
- IP层:Protocol是UDP(17),Header checksum没有标红。
- UDP层:Source Port和Destination Port和你填的一致,Length是8+负载长度。
- UDP Payload里能看到你输入的ASCII内容(如果有的话)。
这个验证过程本身就是WinPcap和Socket编程最大的区别体验:用Socket发UDP的人永远不会亲眼看到MAC头和IP头长什么样,而这个程序让这些细节裸奔在眼前。
5.3 数据包抓不到时的排查路径
如果Wireshark开了但一个包都没抓到,按这个顺序查:先看过滤条件是不是写反了(比如监听的是回环接口但包发在物理网卡上);再确认发包程序选的设备编号和Wireshark的接口名对应的是同一个适配器;然后看Wireshark左下角有没有显示“已捕获”的计数在涨——如果涨了但过滤栏空白,说明过滤写错了;如果计数也没涨,回去查驱动兼容性,换个网卡接口再试。
6. 二次开发思路:把这个小工具改造成自己的协议测试台
这套代码的骨架足够简洁,适合在上面做各种协议实验。我建议的改造路径是从“发固定包”到“发可控包”,逐步把它变成你自己的底层网络工具箱。
第一步,把硬编码的参数抽出来放到配置文件里。用Windows自带的INI文件读取就行,不用引第三方库。这样改的好处是你不用为每次测试都重新编译源码,输入不同的IP、端口、负载改成改INI文件。INI的格式很简单:
[target] src_ip=192.168.1.100 dst_ip=192.168.1.200 src_port=12345 dst_port=6789 dest_mac=FF-FF-FF-FF-FF-FF payload=hello_world_from_winpcap count=100 interval_ms=10count和interval_ms是控制发包压力和频率的,加这两个字段能让工具做简单的性能测试——往目标打1000个包,测它的接收丢包率。
第二步,加上自定义负载构造函数。现在的程序负载是用户输入的一段ASCII字符串,但实际做网络测试时经常要发二进制数据。改造办法是在main里加一个模式判断:如果配置文件里payload_hex=1,就把payload字段按十六进制解析,AABBCCDD转成四个字节,否则按ASCII原样传输。
if (cfg.hex_mode) { for (int i = 0; i < len / 2; i++) { sscanf(&cfg.payload[i * 2], "%2hhx", &payload[i]); } }这个%2hhx格式符在C99里才能用,MSVC从VS2013开始支持,老编译器如果你在用就换成手动解析两个字符拼字节。
第三步,给程序加一个简单的统计输出。每次发包后计算一下平均发包时延(用clock()或GetTickCount64),打印总共发了多少个字节。这一步看起来简单,但对排查网络性能问题很有用——如果你往千兆网卡上发小包,永远只会看到“发送成功”,有了时延统计就能发现驱动层面的拥塞。
第四步(进阶),把这次学的WinPcap发包能力做成一个DLL,供其他项目调用。接口可以这么设计:
int udp_send_init(const char *dev_name); int udp_send_frame(uint8_t *payload, int len, uint32_t dst_ip, uint16_t dst_port); void udp_send_cleanup(void);这三件套封装了“打开设备、构造包、发出去”的全过程。DLL的好处是,以后你做其他项目(比如模拟器、安全工具、网络测量程序)就能直接引这个库发底层包,不用把抓网卡的逻辑在各处重复。封装时记得把校验和计算写在内部,对上层调用者隐藏htons之类的细节,避免别人二次开发时踩第3.5节里说的坑。
还有个值得提的改造点:把捕获功能加回来。现在项目只有发包,但WinPcap最擅长的是抓包。你可以在同一个pcap_t句柄上先pcap_sendpacket发一帧,然后立刻调用pcap_next_ex读回来,实现“自收自发”的回环测试。用这个模式做验证时,连第二台电脑都不需要了。
这套代码我前后拆过好几遍,每次给新人讲WinPcap都拿它当起点。从小处说它是UDP发包,从大处看它是底层协议栈的练习板。这个工具跑通的那一瞬间,你才真正意识到平时那些Socket API替你干了多少活。从那以后我每次做网络编程,都强制自己在Wireshark里看一遍实际线上的包长什么样再往下写业务逻辑,这个习惯一直保留到现在。希望帮到你。
本文还有配套的精品资源,点击获取