简介:这是一份面向计算机网络课程设计的完整PDF方案,适合高校计算机专业学生完成ARP协议相关实验、理解地址解析机制时参考,也可用于广工等院校课程设计答辩前的查漏补缺。文档围绕使用ARP协议获取局域网内活动主机物理地址这一C++程序设计目标,依次讲解ARP工作原理、以太网与ARP帧结构、Winpcap发包收包流程,并附有完整代码分析和运行结果截图,可直接对照搭建Visual C++开发环境并复现关键步骤。资源包共1个PDF文件,约458KB,内容聚焦、便于打印阅读;目前已有169人学习浏览。通过这份材料,读者可以系统掌握从构造ARP请求、发送广播帧、监听并解析应答到输出IP-MAC映射表的完整实现链路,同时巩固IP与MAC地址映射、网卡工作模式等网络基础知识。
1. ARP 协议取局域网活动主机物理地址的课设:这个老题目为什么还值得拆
计算机网络课设里有一道经典题:使用 ARP 协议获取局域网内部活动主机物理地址的程序实现,C++ 环境。题目看着简单,真要交差却要过四关:数据帧怎么定义才符合公共标准、Winpcap 怎么把帧发出去、响应帧怎么收怎么解析、结果怎么显示。这份 PDF 恰好把四关全走了一遍——它是一份完整课设报告,含 ARP 协议原理梳理、数据帧结构定义、VC++ 工程代码、运行结果和心得总结,代码部分还专门做了字节对齐和双线程处理。适合正在做类似课设的学生、想用 Winpcap 写局域网扫描小工具的人,以及想快速回顾 ARP 协议原理和帧布局的工程师。我拆完的感觉是:代码可以直接抄,但字节序、内存对齐、混杂模式三处坑,抄的时候要特别小心,不然发出去的包没人理。
2. ARP 帧结构与原理:14+28 字节的链路拼图
2.1 以太网帧头与 ARP 报文:每个字段都有固定偏移
ARP 协议解决的是 IP 地址到物理地址的映射问题。物理层不认 IP 地址,只认 MAC 地址,所以 A 主机想和 D 主机通信,必须先拿到 D 的 MAC。A 广播一个 ARP 请求,局域网里所有在线主机都会收到,但只有 IP 匹配的那台会回一个 ARP 响应,把自己的 MAC 地址通报出来。A 收到响应后把 IP-MAC 对存进缓存,后续通信直接查缓存,这就是 ARP 高速缓存。原课设里扫描活动主机的思路,本质就是批量发 ARP 请求,然后等响应。
帧的布局是整道题的地基。以太网帧头 14 字节,ARP 报文 28 字节,两部分拼在一起正好是 42 字节,Winpcap 的 pcap_sendpacket 一次发出去。源码里定义了三个结构体:ethernet_head、arp_head、arp_packet,arp_packet 就是前两者的组合。各字段的偏移和取值如下表:
| 偏移 | 字段 | 长度 | 值/说明 |
|---|---|---|---|
| 0 | 目的 MAC | 6 | ARP 请求为 FF:FF:FF:FF:FF:FF(广播) |
| 6 | 源 MAC | 6 | 本机 MAC |
| 12 | 以太网帧类型 | 2 | 0x0806 表示 ARP |
| 14 | 硬件类型 | 2 | 1 表示以太网 |
| 16 | 协议类型 | 2 | 0x0800 表示 IP |
| 18 | 硬件地址长度 | 1 | 6 |
| 19 | 协议地址长度 | 1 | 4 |
| 20 | 操作字段 | 2 | 1 为 ARP 请求,2 为 ARP 响应 |
| 22 | 发送端 MAC | 6 | 源主机硬件地址 |
| 28 | 发送端 IP | 4 | 源主机 IP |
| 32 | 目的 MAC | 6 | 请求时为 0 |
| 38 | 目的 IP | 4 | 要解析的目标 IP |
注意看偏移 20 的操作字段和偏移 22 的发送端 MAC,接收线程判断是否收到 ARP 响应,就是靠偏移 20 的值是否为 2;提取源主机的 MAC 地址,则从偏移 22 开始连续读 6 字节。原源码里直接用*(unsigned short *)(pkt_data+20)这种裸指针强转来判定,虽然能跑,但换到 64 位系统或遇到非对齐访问时可能出问题,这一点在避坑章会展开讲。
2.2 字节序与结构体对齐:htons 和 pack(1) 是动手前提
第一次抄这份代码的人,最容易在字节序上翻车。机器上跑的是小端序,而网络传输规定是大端序,所以在填充 ARP 帧时,凡是多字节字段都要做一个转换。源码里反复出现的htons和htonl就是干这个的:eh.type = htons(ETH_ARP)把帧类型转成网络字节序,ah.hardware_type = htons(ARP_HARDWARE)转硬件类型,ah.source_ip_add = inet_addr(ip)则是把点分十进制的 IP 字符串直接转成网络字节序的 32 位值。注意inet_addr的返回值本身就是网络字节序,所以它不需要再套一层htonl。
另一个隐蔽的坑是 C/C++ 结构体默认对齐。ethernet_head 里有 6 字节数组、6 字节数组、2 字节 short,arp_head 里有 2+2+1+1+2+6+4+6+4 的混合类型。编译器默认会按 4 字节或 8 字节对齐填充空隙,导致结构体实际占用的内存和报文在链路上的真实布局不一致。源码第一行就写了#pragma pack(1),强制结构体按 1 字节对齐,这样struct arp_packet的sizeof才是干净的 42,直接memcpy进发送缓冲区才不会多出无用字节。这一点没有商量的余地:不写 pack(1),发出去的包全是废包。
接收方向同理。解析响应帧时,源码用pkt_data+12、pkt_data+20、pkt_data+38这类偏移来读字段,而不是把缓冲区强转成结构体指针,就是绕开对齐问题的一种写法。如果你自己改代码,我更推荐定义好结构体后用memcpy逐字段拷贝,稳得多。
2.3 网卡工作模式与混杂模式:为什么必须开 PROMISCUOUS
网卡有四种工作模式:广播模式收目的地址为全 1 的广播帧;多播模式收组播帧;直接模式只收目的 MAC 是自己网卡的帧;混杂模式则来者不拒,所有流过网卡的帧都收。普通上网场景网卡跑在直接模式加广播模式上,发给别人的单播帧根本不进你的接收队列。但 ARP 扫描的前提是「收到别人的 ARP 响应」,这台响应主机把帧发给的是源主机,不是你的网卡,所以必须把网卡切到混杂模式,才可能在链路上截获这些响应。
源码里打开设备时传的第三个参数就是PCAP_OPENFLAG_PROMISCUOUS,这一步是接收线程能抓到别人回包的关键。后面避坑章会讲一个常见现象:有人把混杂模式去掉,结果只能收到自己的 ARP 请求回包,其他主机的响应全丢了。另外 pcap_open 的 snaplen 参数在源码里是 65536,意思是每个包最多捕获 65536 字节,足够覆盖以太网帧加 VLAN 头的场景,一般照抄即可。超时时间 1000 毫秒是 pcap_next_ex 的阻塞上限,调大了扫描会变慢,调小了 CPU 占用会高。
3. Winpcap 工程搭建:VC++ 配置与第一个发包程序
3.1 Winpcap 运行时与开发包:驱动和 WpdPack 缺一不可
Winpcap 不是单一文件,它分两部分:运行时驱动给操作系统装上抓包能力,开发包 WpdPack 提供编译期需要的头文件和导入库。如果目标机器上没装过 Winpcap 驱动,程序里 pcap_findalldevs_ex 会直接返回失败,打印「找不到网卡」,所以跑这个课设前必须先确认驱动已经安装。比较老的 Winpcap 4.x 版本在 Win10 以上系统有兼容性问题,建议优先用能正常枚举到网卡的版本,装完后可以用它自带的 wpcap.dll 是否存在于 System32 目录来快速验证。
开发包解压后通常有 Include 和 Lib 两个目录,Include 里有 pcap.h、pcap-int.h、Packet32.h 这些头文件,Lib 下有 wpcap.lib、Packet.lib。源码 include 了 pcap.h 和 Packet32.h,这两个正是收发数据帧的核心。编译时还要注意字符集问题,老代码用的是 char 数组和 printf 输出,如果工程默认是 Unicode 字符集,某些 API 的宽窄字符转换会冒出警告甚至报错,建议把字符集设成「未设置」或多字节字符集,省去一堆麻烦。
3.2 Visual C++ 工程配置:Include、Lib 与附加依赖项
Visual C++ 6.0 时代的配置方式和 Visual Studio 稍有不同,但核心就三项:头文件目录、库文件目录、附加依赖库。在 VC6 的 Tools -> Options -> Directories 里把 Include 和 Lib 路径加进去;在项目设置 Project Settings -> Link -> Object/library modules 里追加 wpcap.lib 和 ws2_32.lib。后者是 Winsock 库,地址转换和 socket 相关函数要用到它。库文件的附加依赖项还可以写在代码里,用#pragma comment(lib, "wpcap.lib")代替手工配置,源码里没有写,但补上这行会让工程更省事,编译时少一步遗漏。
工程还建议设置成「使用多字节字符集」。具体在 Visual Studio 的 Project Properties -> Configuration Properties -> General -> Character Set 里选 Not Set。如果用较新版本的 Visual Studio 编译这份老代码,C4996 这类安全函数警告大概率会出现,可以在预处理器定义里加上_CRT_SECURE_NO_WARNINGS,把 scanf、strcpy 这些老调用的警告压下去,不影响功能。注意这个课设没有强调平台工具集,但实用角度看,Win7 和 Win10 上编译运行都没问题,关键依赖还是 Winpcap 驱动本身。
3.3 枚举网卡、选择适配器、打开设备:三步走
主程序的第一步是枚举网卡并让用户选择。源码用pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf)拿到全部设备列表,然后打印每张网卡的 name 和 description,由用户输入序号。这里有个小坑:PCAP_SRC_IF_STRING 在较新版本里已标记为 deprecated,但老代码里仍然能用,编译时会有提示,不影响运行。设备列表用完要调用pcap_freealldevs(alldevs)释放,源码里在正常流程和报错流程都做了处理。示例如下:
pcap_if_t *alldevs; // 设备链表头 pcap_if_t *d; int inum; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) { fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf); exit(1); } for (d = alldevs, i = 0; i < inum - 1; d = d->next, i++);pcap_findalldevs_ex的第二个参数 NULL 表示不从远程机器抓包,只枚举本机设备。循环语句for(d = alldevs, i = 0; i < inum - 1; d = d->next, i++);的作用是跳转到用户选中的那张网卡,这段代码把遍历和定位合并到了一行,初看有点绕,但效果等价于一个 while 循环。接着pcap_open打开设备,参数列表里 65536 是抓包长度上限,PCAP_OPENFLAG_PROMISCUOUS 开启混杂模式,1000 是读取超时毫秒数。打开失败时程序会打印「适配器不被 WinPcap 支持」,原因一般是驱动没装好或设备名带了特殊字符。打开成功后还要调用ifget和GetSelfMac拿到本机 IP、掩码和 MAC,这些信息后面扫描要用。
4. 双线程扫描实现:发广播包、收响应、解出 IP-MAC
4.1 包结构定义与构造:三个结构体拼出 42 字节
源码把帧结构定义成三个结构体:ethernet_head 表示以太网帧头,arp_head 表示 ARP 报文,arp_packet 把两者组合成最终完整的包。定义方式如下:
#pragma pack(1) struct ethernet_head { unsigned char dest_mac_add[6]; // 目的 MAC unsigned char source_mac_add[6]; // 源 MAC unsigned short type; // 帧类型,0x0806 为 ARP }; struct arp_head { unsigned short hardware_type; // 硬件类型,1 为以太网 unsigned short protocol_type; // 协议类型,0x0800 为 IP unsigned char hardware_add_len; // 硬件地址长度,6 unsigned char protocol_add_len; // 协议地址长度,4 unsigned short operation_field; // 操作字段,1 请求,2 响应 unsigned char source_mac_add[6]; // 发送端 MAC unsigned long source_ip_add; // 发送端 IP unsigned char dest_mac_add[6]; // 目的 MAC unsigned long dest_ip_add; // 目的 IP }; struct arp_packet { struct ethernet_head ed; struct arp_head ah; };#pragma pack(1)是这一切的前提,它在结构体定义之前取消编译器的自动填充。unsigned long在 32 位和 64 位 Windows 上长度不同,但 IP 地址固定是 4 字节,这份老代码在 64 位系统下编译时sizeof(arp_head)可能会变成 32 而不是 28,因为 unsigned long 在 LLP64 数据模型下是 4 字节不变,这里相对安全。构造包时先 memset 缓冲区为 0,再把 eh 和 ah 依次 memcpy 进 sendbuf,操作字段置为 ARP_REQUEST,目的 MAC 置为全 1 广播地址,源 MAC 和源 IP 填本机真实值,目的 IP 填要扫描的地址。这些代码在 GetSelfMac 和 SendArpPacket 里各出现一次,逻辑基本一致。
4.2 构造 ARP 请求:广播地址与真实源地址的填充顺序
构造请求包最关键的细节是:ARP 请求里的目的 MAC 是 0,源 MAC 是本机 MAC,源 IP 是本机 IP,目的 IP 是目标地址。源码里在 GetSelfMac 函数中故意把源 IP 设成100.100.100.100,这是一个「假 IP」,用来骗本机网卡回包,从而从响应帧里提取自己的真实 MAC。这个技巧值得解释一下:因为要拿到自己的 MAC,但标准库没有直接接口,就发一个目的 IP 为假地址的 ARP 请求,本机协议栈会认为这个 IP 不存在而丢弃,但网卡驱动层仍会回一个 ARP 应答,说「100.100.100.100 不存在」,应答帧里带着本机 MAC 地址,在混杂模式下就能收到。
真正扫描时,SendArpPacket 线程填充的是主程序传进来的真实 IP 和真实 MAC。每次循环把目的 IP 写进 ah.dest_ip_add,然后pcap_sendpacket发送。发送前要保证 eh.source_mac_add 和 ah.source_mac_add 一致,都填本机 MAC,否则某些严格实现的交换机或主机检测到帧内 MAC 不一致会丢弃。发送函数返回值是 0 表示成功,非 0 表示失败,失败时可以用 GetLastError 查看原因,实践中最常见的失败就是网卡未启用或驱动异常。
4.3 发送线程:从网络地址扫到广播地址,Sleep 控制频率
发送线程负责把 ARP 请求广播给整个 /24 网段。核心逻辑是先用本机 IP 和子网掩码按位与算出网络地址,然后从网络地址的下一个 IP 开始,逐个向后扫 255 个地址。代码里有一处隐蔽的字节序操作需要仔细看:
unsigned long myip = inet_addr(ip); // 二进制网络字节序 unsigned long mynetmask = inet_addr(netmask); unsigned long hisip = htonl((myip & mynetmask)); // 转成主机序再+1 for (int i = 0; i < HOSTNUM; i++) { ah.dest_ip_add = htonl(hisip + i); // 再转回网络序发送 memset(sendbuf, 0, sizeof(sendbuf)); memcpy(sendbuf, &eh, sizeof(eh)); memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah)); pcap_sendpacket(adhandle, sendbuf, 42); Sleep(50); }inet_addr返回的是网络字节序,myip & mynetmask得到网络地址,但此时的网络地址也是网络字节序。如果直接在这个值上做算术加 1,得到的结果是错的,因为字节序反了。所以源码先用htonl转成主机序,拿到可以正常加减的整数,得到目标 IP 后再用htonl转回网络字节序赋给 dest_ip_add。这一步很容易被忽略,但它决定扫描是从 10.0.0.1 开始还是从 10.0.0.16777216 这种错误地址开始。
Sleep(50)是限速手段。不 Sleep 的话,255 个包会在几十毫秒内全部抛出,轻则 CPU 飙高,重则被交换机或目标主机的防 ARP 风暴机制限流,大量请求根本到不了目的地。50 毫秒意味着整个扫描大约持续 13 秒,这个节奏在宿舍网、实验室交换机上都不会触发限速。如果你在更严格的网络环境,把 Sleep 提到 100 是更稳妥的选择,代价是扫描时间拉长到 25 秒。
4.4 接收线程:pcap_next_ex 轮询与响应帧解析
接收线程和发送线程并行跑。接收线程的任务是在循环里调用pcap_next_ex从网卡读取数据包,判断是不是 ARP 响应,是的话提取发送端 MAC 和 IP 打印出来。响应判定的代码写法值得逐行看:
if (*(unsigned short *)(pkt_data + 12) == htons(ETH_ARP)) { // 帧类型是 ARP if (*(unsigned short *)(pkt_data + 20) == htons(ARP_REPLY)) { // 操作字段是 2 printf("IP地址:%d.%d.%d.%d MAC地址:", recv->ah.source_ip_add & 255, recv->ah.source_ip_add >> 8 & 255, recv->ah.source_ip_add >> 16 & 255, recv->ah.source_ip_add >> 24 & 255); for (int i = 0; i < 6; i++) { Mac[i] = *(unsigned char *)(pkt_data + 22 + i); printf("%02x", Mac[i]); } printf("\n"); } }偏移 12 是帧类型字段,0x0806 说明载体是 ARP;偏移 20 是操作字段,2 说明这是 ARP 响应而不是请求。满足这两个条件后,发送端 MAC 在偏移 22 处连续 6 字节,发送端 IP 在偏移 28 处占 4 字节。源码里用recv->ah.source_ip_add的方式读 IP,这是把 pkt_data 强转成了 arp_packet 指针,依赖了#pragma pack(1)下结构体内存布局和帧一致这一点。我自己改代码时会用 memcpy 把 IP 的 4 个字节拷出来再拼,虽然多几行,但逻辑更直白,也不会受编译器设置影响。
4.5 主流程收尾:双线程与 flag 结束信号
主函数在准备好参数后创建两个线程,一个跑 SendArpPacket,一个跑 GetLivePC,然后主线程停在 getchar 上等待用户按键。这里有个不太显眼的细节:全局变量 flag 初始为 FALSE,发送线程在扫描完 255 个 IP 后再 Sleep(1000) 等待响应落袋,然后把 flag 置为 TRUE。接收线程每轮循环检查 flag,读到 TRUE 就打印「扫描完毕」并退出循环。这是一个朴素的生产者-消费者同步,用全局变量实现,不涉及锁,在课设场景下够用。
但要注意一个边界条件:发送线程最后一次发包后 Sleep(1000) 才置 flag,而接收线程在 flag 为 FALSE 时会持续pcap_next_ex收包,所以理论上 1 秒的缓冲足够让慢速主机的响应到达。如果目标网络有主机响应延迟超过 1 秒,那台主机就会被漏掉,这种场景在无线网络或跨交换机环境下偶尔会发生。想要更稳,可以把那个 Sleep(1000) 加到 2000,或者扫描完后再额外轮询两轮。线程退出机制也很粗糙,没有 WaitForSingleObject,主线程 getchar 后直接 return 0,进程结束后线程自然终止,这在课设验收场景下没有大问题,但拿到真实工具里用就要补线程回收。
5. 避坑与常见问题:六个最容易翻车的现场
5.1 结构体没按 1 字节对齐,发出去的包没人理
现象:程序提示发送成功,但局域网里所有主机都不回 ARP 响应,接收线程什么都收不到。有人把原因归结为网卡问题,折腾驱动半天发现没用。
原因:#pragma pack(1)被注释掉或挪到了结构体定义之后,编译器按默认规则在结构体内填充了对齐字节。此时sizeof(arp_packet)大于 42,memcpy把多余的空洞字节也作为帧内容发到了链路上,破坏了 ARP 报文格式,接收方解析失败直接丢弃。
解决:把#pragma pack(1)放在所有结构体定义之前,并且确认在使用sizeof(arp_packet)或sizeof(eh) + sizeof(ah)拼包时,计算结果严格等于 42。可以在 main 里加一句printf("%d\n", sizeof(struct arp_packet));,输出不是 42 就说明对齐没生效。
5.2 字节序没换算,扫描范围跑到奇怪网段
现象:程序正常发包,也能收到响应,但打印出来的 IP 段明显不对,比如本机是 192.168.1.100,扫描结果却落在 192.168.1.0 之外的其他网段,甚至出现 168.192.1.100 这种字节顺序颠倒的 IP。
原因:inet_addr返回网络字节序,直接拿它做按位与和算术加法,结果是小端视角下混乱的数字。扫描起点算错,后面 255 个目标全错,自然收不到正常响应。
解决:严格照抄源码的两步转换:先htonl(myip & mynetmask)转成主机序算起点,再htonl(hisip + i)转回网络序填入帧。想验证的话,可以在循环里用printf("%s", inet_ntoa(...))打印一下当前目的 IP 字符串,确认从 .1 开始递增到 .255。
5.3 混杂模式没生效,只能收到自己的回包
现象:发送线程自己的 ARP 请求能收到回包,但其他主机的 ARP 响应一个都抓不到,扫描结果只有本机一条记录,MAC 还是自己网卡的。
原因:网卡没切到混杂模式。pcap_open 的第三个参数没有传PCAP_OPENFLAG_PROMISCUOUS,或者某些虚拟网卡驱动对混杂模式支持不完整。直接模式下网卡只接收目的 MAC 是自己的帧,别人回给源主机的 ARP 响应根本进不了接收队列。
解决:确认打开设备时代码完整传入了混杂模式标志位。如果是 VMware 虚拟机,还要确认虚拟交换机设置允许混杂模式,或者直接把网卡模式切到桥接,NAT 模式下宿主机的 ARP 包不一定能透传到虚拟机。
5.4 扫描速度太快被交换机限速,大量主机漏报
现象:在部分网络环境下扫 255 个地址,回包寥寥无几,但单独 ping 某台主机又能通。把 Sleep(50) 去掉后问题更严重,几乎全部漏报。
原因:AR P 请求是以太网广播帧,交换机收到大量广播帧时会触发风暴控制或速率限制策略,超过阈值的帧直接丢弃。目标主机本身也有 ARP 速率限制,短时间内收到几十上百个请求会丢包。
解决:把Sleep(50)改成Sleep(100)或 200,让整个扫描在 25 到 50 秒内完成,绝大多数家用路由器和交换机不会限制这个频率。更稳的做法是分片扫描,比如一次只扫 64 个地址,停 2 秒再扫下一片。
5.5 64 位系统下裸指针解引用读取字段崩溃
现象:在 64 位 Windows 上编译运行,程序不定时崩溃,崩溃位置落在接收线程解析包的部分。把工程改成 32 位编译后问题消失。
原因:源码里大量使用*(unsigned short *)(pkt_data + 20)这类非对齐指针强转。x86 架构允许非对齐访问,但 x64 下某些情况下会触发保护异常;加上 unsigned long 在 64 位下仍是 4 字节,但编译器生成的访存指令可能按 8 字节对齐优化,导致越界读。这种问题表现不稳定,属于典型的「玄学崩溃」。
解决:把所有裸指针强转改成memcpy到局部变量后再解析,例如unsigned short op; memcpy(&op, pkt_data + 20, 2);判断 op == 2。这样段对齐、字节序都自己控制,代码也能同时在 32 位和 64 位下跑。顺带把打印 IP 的方式改成按字节printf("%d.%d.%d.%d", buf[28], buf[29], buf[30], buf[31]),彻底摆脱结构体强转。
5.6 杀毒软件或系统防火墙拦截 Winpcap 的抓包操作
现象:程序编译通过,运行后马上被 Windows Defender 或第三方杀毒软件拦截,提示「程序试图访问网络数据包」并终止进程。有些机器上 pcap_sendpacket 甚至直接返回错误码。
原因:Winpcap 的底层是内核驱动 npf.sys,它把自己挂到网卡协议栈上时行为比较敏感,杀毒软件默认把这种操作标记为抓包嗅探行为。老版本驱动在 Win10 以上的兼容性也一般,签名过期会导致加载失败。
解决:优先确认 Winpcap 运行时版本能正常加载,不行就换用 Npcap 作为替代运行时并适配头文件差异。跑课设时临时把杀毒软件的网络防护或实时监控关掉,但这种场景一般限制在学生本机,实验室估计不需要关。不需要用其他抓包工具,直接信源码那套就行。
6. 结果落地技巧:从打印 IP-MAC 到可复用的扫描输出
6.1 先用单 IP 定向发包验证链路,再上全量循环
拿到代码第一件事不要直接扫 255 个地址,我一般会把循环上限临时改成 1,只扫本机的网关或另一台已知设备,确认发送、接收、解析三个环节都通。这一步能排除掉至少一半的坑。验证时目的 IP 固定填网关地址,发送完请求后 Sleep(200),再打印接收线程解析出的 MAC,看是不是网关的物理地址。通了这个台阶,再放开循环,用 Sleep(50) 全量扫描,得到的列表就会干净很多。因为如果链路本身是断的,全量扫出来的只有一片空白,你根本分不清是代码问题还是环境问题。
6.2 把输出写进日志文件,方便课设报告截图和比对
打印到控制台的结果关掉窗口就没了,课设报告要贴数据、要前后对比,把输出重定向到文件更实用。接收线程里打印 IP-MAC 的地方,加一个文件写入分支,每次解析到合法响应就追加一行,格式用 CSV,方便后来导入 Excel:
FILE *fp = fopen("arp_result.csv", "a"); if (fp != NULL) { fprintf(fp, "%d.%d.%d.%d,%02x:%02x:%02x:%02x:%02x:%02x\n", ip1, ip2, ip3, ip4, Mac[0], Mac[1], Mac[2], Mac[3], Mac[4], Mac[5]); fclose(fp); }文件打开方式用追加模式"a",是因为接收线程在循环里会触发很多次,重复打开关闭虽然有小开销,但课设规模完全扛得住。CSV 的好处是可以用 Excel 直接打开,按 IP 排序,一眼看出哪些主机活跃。如果你不想每次运行都累积旧数据,改成"w"模式即可,每次从空文件开始。这个细节不是源码里自带的,是我做类似扫描工具时的常规加强,课设报告的「运行结果」部分贴这种格式化输出比贴控制台截图要规范得多。
6.3 扫描完成后多等一轮,别急着退出
源码用flag=TRUE当扫描结束信号,接收线程看到 flag 就退出。但这会造成一个实际问题:发送线程在 Sleep(1000) 后置位 flag,此时慢速主机刚到 ARP 请求,响应还没发回来,接收线程已经退出了,漏掉那台主机。解决思路很简单,把Sleep(1000)改成两次收包循环,或者置 flag 后不立即退出,而是多轮询 2 秒。我在改这份代码时会用下沉方式处理:置 flag 后,接收线程把while(true)改成while(counter < 20),每轮 Sleep(100),计数到 20 再退出,相当于多给了 2 秒的尾部窗口。如果你需要保证不漏报,这是最关键的一处改动。
6.4 扫描结果的验证技巧:ping 一下再做交叉比对
ARP 扫描得到的主机列表可以用 ping 命令快速验证。先把扫描到的 IP 记在一边,再对同一个网段批量 ping 一遍,对比 ARP 响应列表和 ICMP 响应列表的重合度。两者重合的主机基本就是活跃主机了;只在其中一侧出现的主机,要么是防火墙挡了 ICMP,要么是 ARP 缓存没更新,需要重扫一次。从那以后我每次做抓包程序都强制走一遍「单 IP 验证 → 全量扫描 → ping 交叉比对」的流程,确认输出不是靠运气跑出来的,才敢贴到报告里。希望这份拆解能帮你的课设少走几步弯路,动手时把帧结构和字节序看紧一点,大概率一次就跑通。
本文还有配套的精品资源,点击获取