资讯动态

C++实现Windows ping:ICMP报文与IcmpSendEcho避坑指南

发布时间:2026/9/30 5:34:36 来源:尧图企业网站定制
简介Windows平台下用C结合WinSock2实现ping命令的代码示例包面向网络编程入门者及需要开发ICMP探测工具的开发者可作课程设计或网络诊断功能开发的参考起点。资源共5个文件包括两个C源文件main.cpp与TPing.cpp、一个头文件TPing.h以及Qt工程文件Ping.pro和Ping.pro.user其中源文件分别承担主程序入口与ping逻辑封装头文件定义接口与常量工程文件便于在Qt环境中直接编译压缩包整体仅5KB结构精简适合快速阅读。目前已有3033人学习下载。代码覆盖WSAStartup初始化、原始套接字创建、ICMP回显请求与应答报文的构造解析、sendto与recvfrom收发流程等关键环节并包含超时判断与资源清理逻辑示例还演示了ICMP报文序列号、时间戳等字段的设置方式以及收到应答后计算往返时间的基本方法。通过阅读代码能直观理解ping命令在Windows下的底层实现机制同时注意到原始套接字运行时需要管理员权限等限制为后续扩展多线程、命令行参数解析等功能提供了可复用基础。1. 在 Windows 上复刻 ping一包一发之间的门道C实现ping命令的功能听起来就是发个ICMP包再等回复但真在Windows上动手时会发现平时一条命令背后的东西比想象多ICMP报文要自己拼吗为什么有人用Raw Socket还要管理员权限IcmpSendEcho和system(ping)有什么区别这份资源把Windows下的实现路径拆开讲清楚给出一套可直接编译的C工程能输出延迟、TTL、丢包率也可以改造成网络监控或主机存活探测工具。适合想搞懂ping命令底层逻辑的学生、准备网络岗面试的开发者以及需要在Windows上做内网连通性检测的运维脚本爱好者。我会先用原理把报文结构立住再给两份能跑的代码最后把权限、防火墙这些坑一次性说完。2. 先搞懂 ping 的底细ICMP 报文结构与 Windows 下的三条实现路径2.1 ICMP 回显请求与回显应答一个包出去一个包回来ping 命令与 ICMP 协议是绑定出现的。ICMP 报文封装在 IP 包里面网络抓包里看到的顺序是以太网头、IP 头、ICMP 头最后是数据。ping 发出的是 ICMP 回显请求类型值是 8目标主机收到后回一个类型 0 的回显应答。类型 11 代表超时通常是跳数超过 TTL或者中间设备回的错误通知。回显报文的 ICMP 头很固定类型、代码、校验和、标识符、序号、数据。类型和代码都是 1 字节请求是 8/0应答是 0/0校验和 2 字节覆盖从 ICMP 头开始到数据尾的所有内容标识符 2 字节表示是哪一进程发出的序号 2 字节用来匹配每一轮请求。负载区没有固定规定传统实现里放一串可打印字符也有工具把发送时的时间戳放进去用来算单向延迟。字段字节数作用类型18回显请求0回显应答11超时代码1配合类型使用回显报文固定为 0校验和2对 ICMP 头数据做补码校验标识符2标识发送进程回复时原样带回序号2标识第几轮请求用来对账数据变长负载内容常见是字符串或时间戳如果只是调 API这些字段你感知不到底层全被系统填好。但做 Raw Socket 版本时校验和算法、网络字节序、标识符匹配全部要自己写一遍。这也是面试官最爱追问的点ICMP 头里哪几个字段是网络字节序校验和怎么算回复怎么和请求对上答案都在上表里。TTL 字段在 IP 头里不在 ICMP 头但 IcmpSendEcho 返回的 ICMP_ECHO_REPLY 里带了回包的 TTL可以直接从 Options.Ttl 取。RTT 是发送到收到回复之间的时间Windows 系统内部用毫秒计时普通局域网场景下这个精度够用跨公网时抖动会明显一些但只要不要求纳秒级统计IcmpSendEcho 的结果和系统自带 ping 基本在同一个量级。2.2 三条实现路径的对比为什么推荐系统 API 而不是原始套接字Windows 上实现一个 ping实际有三条路。第一条最粗暴直接用 system(ping www.baidu.com) 调现成的系统命令再去解析控制台文本。优点是代码量极小缺点是控制台窗口来回闪解析结果还得兼容中文和英文系统拿不到结构化的 RTT线上脚本里基本是下策。第二条是调用 Windows 的 ICMP API 家族核心是 IcmpCreateFile、IcmpSendEcho、IcmpCloseHandle。这套接口由系统的 iphlpapi.dll 提供自己完成 ICMP 报文组装、校验和计算、超时重传这些脏活我们拿到的是填充好的 ICMP_ECHO_REPLY 结构体里面有状态码、往返时间、TTL、返回数据。普通用户权限就能跑Windows XP 到现在都稳定。第三条是 Raw Socket自己用 socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) 建原始套接字自己拼 ICMP 头、算校验和、sendto 出去再 recvfrom 收回来。好处是协议细节完全可控不用依赖系统 ICMP 驱动的行为代价是 Windows Vista 之后原始套接字权限收紧必须管理员身份运行还容易被防火墙和安全软件拦。路径权限RTT 获取复杂度适用场景system(ping)普通解析文本低临时验证不推荐IcmpSendEcho API普通结构体字段中绝大多数 Windows 工具Raw Socket管理员自己计时高学习协议、造轮子我给这份资源的建议是工程主干用 IcmpSendEcho协议讲解用 Raw Socket。先把能跑的东西跑起来再往底层拆。一上来就啃原始套接字很容易卡在校验和和权限两层上最后连程序都跑不起来项目就烂尾了。另外一个容易忽视的选择是 IcmpSendEcho2它是异步版本可以在等待回包时不阻塞主线程适合后面做并发扫描。代价是调用参数更多需要自己管理事件句柄。单机小工具用同步的 IcmpSendEcho 足够不要为了“异步”两个字提前上复杂度。2.3 工程模块怎么拆发送、接收、统计各管一摊动手写代码之前先把工程拆成三个模块。发送模块负责解析目标地址、创建 ICMP 句柄、设置 TTL 和分片选项、调用发送函数接收模块负责接收回包、判断状态码、从 ICMP_ECHO_REPLY 里取出 RTT 和 TTL统计模块负责累计发送次数、计算丢包率、求最小最大平均延迟并打印。API 版里发送和接收是同步在一个函数调用里的IcmpSendEcho 发出去之后一直等到超时或收到回包才返回。看起来不像两个模块但划分清楚之后后面换成 Raw Socket 版本时只需要把发送层和接收层的内部实现替换掉统计模块不用动。再往后想加 -t 持续模式、多主机并发探测都是在这几个模块的边缘加控制逻辑不用推倒重来。我一般习惯先把统计模块的交互格式定下来也就是最终打印出来的那几行日志长什么样再去填空。因为网络探测程序的用户界面就是控制台输出输出格式定了模块之间的数据接口也就定了。以 ICMP_ECHO_REPLY 结构体为中心发送层产出“目标 IP 句柄 请求数据”接收层产出“状态码 RTT TTL 数据大小”统计层只消费这些字段不关心包是怎么发的。3. 让代码跑起来IcmpSendEcho 完整实现与原始套接字版本3.1 环境准备VS2022 工程、两个静态库和第一次编译这份资源按 Visual Studio 2022 的空控制台工程来组织。新建项目之后源文件直接替换成工程里的 main.cpp然后确认两个链接库ws2_32.lib 和 iphlpapi.lib。前者是 Winsock 的库WSAStartup、gethostbyname、socket 这些函数都从它里面来后者是 IP Helper API 的库IcmpCreateFile、IcmpSendEcho 的实现在里面。为了避免每次新建工程都手动加依赖库代码里直接用 #pragma comment(lib, ...) 写在源文件顶部这样工程属性里不用管编译时链接器也会自动带上。用 VSCode 配 MSVC 工具链的同学也可以但 tasks.json 里要自己加 /link ws2_32.lib iphlpapi.lib或者用 cl 命令行的时候手动拼没有 VS 里省事。我用 VSCode 的体验是编译命令一长就懒得维护小工具还是 VS 工程最省心。提示程序要部署到没装 VS 的机器时把项目属性里的运行库改为“多线程(/MT)”静态链接掉 VC 运行库否则目标机器缺 Microsoft Visual C Redistributable 会导致双击闪退。第一次编译只要不报 LNK2019 就算过了。如果报“无法解析的外部符号 _IcmpSendEcho28”那就是 iphlpapi.lib 没链接上。这个问题后面避坑章节还会单独说这里先记住一个结论IcmpSendEcho 的函数名是带 stdcall 修饰的链接错误信息里带 数字是正常的。3.2 主流程代码循环发送四条 ping 并统计结果下面这份代码是完整的 API 版实现结构上覆盖了前面说的三个模块可以直接放进 VS 的 main.cpp 编译。命令行用法是myping 目标主机 [次数] [超时毫秒]默认 ping 4 次、每次超时 2000 毫秒。#pragma comment(lib, ws2_32.lib) #pragma comment(lib, iphlpapi.lib) #include winsock2.h #include ws2tcpip.h #include iphlpapi.h #include icmpapi.h #include stdio.h #include string.h #include stdlib.h static void usage(const char* prog) { printf(usage: %s host [count] [timeout_ms]\n, prog); printf(example: %s www.baidu.com 4 2000\n, prog); } int main(int argc, char* argv[]) { if (argc 2) { usage(argv[0]); return -1; } const char* target argv[1]; int count argc 2 ? atoi(argv[2]) : 4; // 发包次数 int timeout argc 3 ? atoi(argv[3]) : 2000; // 单包超时毫秒 WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), wsa) ! 0) { printf(WSAStartup failed\n); return -1; } // 先按点分十进制解析失败再走 DNS避免第一包卡在解析上 DWORD ip inet_addr(target); if (ip INADDR_NONE) { hostent* ph gethostbyname(target); if (!ph) { printf(cannot resolve %s\n, target); WSACleanup(); return -1; } memcpy(ip, ph-h_addr_list[0], ph-h_length); printf(resolve %s - %s\n, target, inet_ntoa(*(in_addr*)ip)); } // 打开 ICMP 句柄整个循环共用一个用完再关 HANDLE hIcmp IcmpCreateFile(); if (hIcmp INVALID_HANDLE_VALUE) { printf(IcmpCreateFile failed\n); WSACleanup(); return -1; } // 设置 TTL64禁止分片模拟系统 ping 的默认行为 ICMP_OPTIONS opts; memset(opts, 0, sizeof(opts)); opts.Ttl 64; opts.DontFragment 1; char sendData[] hello from myping; // 回复缓冲区必须留够 ICMP_ECHO_REPLY 结构体 数据区的空间 int replySize sizeof(ICMP_ECHO_REPLY) sizeof(sendData) 64; char* replyBuf new char[replySize]; memset(replyBuf, 0, replySize); int lost 0; long total 0; long fastest 0x7fffffff; long slowest 0; for (int seq 1; seq count; seq) { // 参数依次是句柄、目标IP网络字节序、发送数据、数据长度、 // 选项指针、回复缓冲区、缓冲区大小、超时毫秒 DWORD ret IcmpSendEcho(hIcmp, ip, sendData, (WORD)sizeof(sendData), opts, replyBuf, replySize, timeout); if (ret 0) { // 失败0 表示没有收到任何回显应答具体原因看错误码 long err WSAGetLastError(); if (err 11010L) // IP_REQ_TIMED_OUT printf(Request timed out (seq%d)\n, seq); else printf(IcmpSendEcho failed, error%ld (seq%d)\n, err, seq); lost; } else { ICMP_ECHO_REPLY* reply (ICMP_ECHO_REPLY*)replyBuf; if (reply-Status 0) // IP_SUCCESS { long rtt (long)reply-RoundTripTime; printf(%d bytes from %s: icmp_seq%d ttl%d time%ld ms\n, reply-DataSize, target, seq, reply-Options.Ttl, rtt); total rtt; if (rtt fastest) fastest rtt; if (rtt slowest) slowest rtt; } else { printf(reply status error: 0x%lx (seq%d)\n, (unsigned long)reply-Status, seq); lost; } } if (seq count) Sleep(1000); // 仿系统 ping 每秒发一个 } int received count - lost; double loss lost * 100.0 / count; printf(\n--- %s ping statistics ---\n, target); printf(%d packets transmitted, %d received, %.1f%% packet loss\n, count, received, loss); if (received 0) printf(rtt min/avg/max %ld/%ld/%ld ms\n, fastest, total / received, slowest); delete[] replyBuf; IcmpCloseHandle(hIcmp); WSACleanup(); return 0; }这段代码有几个容易写错的地方。IcmpSendEcho 的第二个参数是目标 IP 的网络字节序它和平时打印用的点分十进制正好反着来所以 inet_addr 的返回值直接传进去就行如果中间用 htonl 再转一次目标就变成反的表现为永远超时。第四个参数 RequestSize 被系统声明为 WORD最大 65535我们的负载只有十几个字节没问题。第七个参数 ReplySize 必须是整个回复缓冲区的大小不是结构体大小接收到的数据会紧跟在 ICMP_ECHO_REPLY 结构体后面所以分配时要加数据区的余量。第五个参数是 ICMP_OPTIONS 结构的指针用来控制 TTL 和 Dont Fragment 标志传空指针时系统用默认值我在上面显式设成 64 和禁止分片避免和系统 ping 行为产生偏差。最后是返回值和错误码的关系。IcmpSendEcho 返回 0 时用 WSAGetLastError() 取具体原因IP_REQ_TIMED_OUT 的数值是 11010直接写常量比依赖宏更直观。返回非 0 时回复缓冲区里第一个 ICMP_ECHO_REPLY 的 Status 字段才是准的如果状态不是 0说明收到了 ICMP 错误回包比如目标不可达这时候也计入丢包。3.3 自己拼 ICMP 包Raw Socket 的校验和与字节序细节API 版能跑通之后想看底层的话转到 Raw Socket 版。这个版本不再依赖 icmpapi.h直接用 Winsock 的 SOCK_RAW 套接字。核心工作变成三件事拼接 ICMP 头、计算校验和、解析收到的回包。先看关键代码。// ICMP 回显头按 1 字节对齐后面直接拼数据 #pragma pack(push, 1) struct IcmpHeader { unsigned char type; // 8 请求0 应答 unsigned char code; // 固定 0 unsigned short checksum; // 网络字节序 unsigned short id; // 标识符网络字节序 unsigned short seq; // 序号网络字节序 // 后面是负载数据 }; #pragma pack(pop) // 补码校验和16 位按字累加最后取反 unsigned short ComputeChecksum(unsigned short* buf, int len) { unsigned long sum 0; while (len 1) { sum *buf; len - 2; } if (len 1) sum *(unsigned char*)buf; while (sum 16) sum (sum 0xffff) (sum 16); return (unsigned short)(~sum); }发送前填充type8code0checksum 先置 0id 填本进程号seq 从 1 开始累加。填充完整体调用 ComputeChecksum把计算出来的值放回 checksum 字段然后用 htons 处理 id、seq、checksum 三个字段。x86 是小端机不转的话协议栈解析出来的标识符和序号是反的回包对不上号。发送和接收用标准的 UDP 风格sendto 发出去recvfrom 收回来。区别在于 recvfrom 收到的缓冲区从 IP 头开始不是直接从 ICMP 头开始所以要先取 IP 头长度。IPv4 头第一个字节的低 4 位是头长单位是 4 字节偏移到 ICMP 头之后再检查 type 是不是 0、id 是不是自己进程的都满足才确认是本次请求的回显应答。Raw Socket 版本的代价很明确socket() 创建时必须管理员身份运行否则返回 INVALID_SOCKETICMP 出站也更容易被防火墙拦截。如果只是想做一个内部使用的工具我建议还是用 API 版。但想彻底搞懂 ping 命令与 ICMP 协议分析Raw Socket 这版值得花时间读校验和函数和字节序处理在面试里也是高频题。4. Windows 下实现 ping 的常见问题排查5 个亲测踩坑记录4.1 编译与权限链接库、管理员身份、首次调用慢坑 1编译链接报 LNK2019无法解析 IcmpSendEcho。现象代码和文档对得上编译也过了链接时报“unresolved external symbol _IcmpSendEcho28”。原因icmpapi.h 只给了函数声明实现体在 iphlpapi.lib 里工程没有链接这个库。解决在附加依赖项里加 iphlpapi.lib 和 ws2_32.lib或者直接在源文件顶部写 #pragma comment(lib, iphlpapi.lib)。这是刚开始用这套 API 时翻车最多的一处十次有八次是建工程时忘了加库。坑 2Raw Socket 版一运行 socket() 就失败。现象套接字创建返回 INVALID_SOCKETWSAGetLastError 报 WSAEACCES。原因Windows Vista 之后原始套接字不能再像 XP 那样随便开必须管理员权限另外某些安全软件会对 SOCK_RAW 做拦截。解决开发时用管理员身份启动 Visual Studio部署时右键以管理员身份运行 exe。如果公司机器上有安全加固软件先把进程加白名单再说别硬刚。坑 3程序启动后第一个包特别慢甚至直接超时。现象日志里第一行 “Request timed out”后面几包正常或者第一包延迟明显偏高。原因有两个一是目标域名没有预解析gethostbyname 卡在 DNS 上二是 IcmpCreateFile 首次调用时系统需要加载 ICMP 驱动这个初始化有额外开销。解决像第 3 章代码那样先 inet_addr 再 gethostbyname解析成功后先打印目标 IPICMP 句柄在整个进程里只创建一次不要在循环里反复创建。4.2 运行时与网络环境防火墙、多网卡、句柄泄漏坑 4系统 ping 通自己实现的程序却超时。现象同一个目标命令行里系统 ping 正常换 myping 就丢包或全超时。原因多数是 Windows 防火墙对 ICMP 的入站规则没放行或者安全软件拦了程序的出站 ICMPIcmpSendEcho 底层虽然由系统接管但到达对端的回显请求如果被对端防火墙丢掉状态码就是超时。解决按 127.0.0.1 → 同网段 IP → 网关 → 公网 IP 的顺序逐级测试。确认是本机防火墙问题时用管理员权限执行下面的命令放行 ICMPv4 入站回显请求测完再删netsh advfirewall firewall add rule nameICMPv4-Echo protocolicmpv4:8,any dirin actionallow netsh advfirewall firewall delete rule nameICMPv4-Echo注意这是往防火墙里开规则的操作只在自己测试机或明确授权的环境里执行公司内网机器动了防火墙规则要提前和网络管理员确认。坑 5装了 VMware 虚拟网卡的机器上 ping 结果不稳定。现象同一台 Windows 上跑 myping延迟忽高忽低偶尔超时系统 ping 表现得也不稳定。原因装了 VMnet1、VMnet8 之后系统路由表里多了好几个网卡和网段回显报文走的出口可能不是物理网卡RTT 波动就被放大了。这种“虚拟机 ping 不通百度”的排查思路也一样先看宿主机物理网卡通不通再在虚拟机里分别 ping 网关、局域网 IP、公网 IP逐段缩小范围。解决用 route print 看路由表临时禁用 VMnet 网卡再测如果确认是虚拟网络环境的问题该用 NAT 还是桥接要按虚拟化平台的网络模型来调整这不是代码能修的。坑 6长时间 -t 运行时内存和句柄数一直在涨。现象程序挂着跑一晚上任务管理器里句柄数肉眼可见往上走。原因ICMP 句柄每次循环里都 IcmpCreateFile 而没有 IcmpCloseHandle或者回复缓冲区每次循环重新 new 没有 delete。解决句柄和缓冲区都提到循环外面退出前统一释放。这类泄漏用任务管理器一抓一个准我一般在循环里打印当前 GetProcessHandleCount() 的值来复现逻辑简单但很有效。5. 进阶命令行参数与批量探测把 myping 变得比系统 ping 更好用5.1 解析 -n / -w / -t让命令行习惯保持一致第 3 章的 main 函数是固定顺序的位置参数用久了会觉得别扭。系统 ping 的习惯是 -n 指定次数、-w 指定超时、-t 持续 ping。我的做法是在进入主循环前做一个简单解析不再关心参数顺序for (int i 1; i argc; i) { if (strcmp(argv[i], -n) 0 i 1 argc) count atoi(argv[i]); else if (strcmp(argv[i], -w) 0 i 1 argc) timeout atoi(argv[i]); else if (strcmp(argv[i], -t) 0) count -1; // -1 表示无限循环 }解析完在循环条件里判断 count-t 模式用 CtrlC 中断中断后照常打印统计结果。这一层很薄但让工具的手感和系统 ping 对齐团队里其他人拿去用也不需要重新学命令行。5.2 内网存活扫描把单点 ping 变成批量探测命令行层面已经通了下一步就是做批量探测。类似超级 ping 工具的思路把目标地址从单个 IP 扩展到一个网段循环调用 IcmpSendEcho把超时设短一点比如 500 毫秒对设备在线率做扫描。串行扫描 254 个地址要一两分钟能接受就用它要更快就开线程池8 个并发差不多能把耗时压到十几秒。做并发之前有一个必须先处理好的问题IcmpSendEcho 不保证多个线程共用一个句柄时行为正确稳妥做法是每个线程各建一个 ICMP 句柄互不共享。这个细节不处理好并发扫出来的结果会随机丢包而且是那种“看起来没问题、跑起来不准”的玄学问题。批量扫描输出也不再逐行打日志改成一行一个在线 IP方便直接重定向到文件再交给自动化脚本处理。以前我为了做一次设备巡检在批处理里循环调 system(ping /n 1)一个网段扫下来要十几分钟控制台还要闪几十次黑窗口。后来改成这个 API 版批量工具之后几分钟的活缩到十几秒数据还是结构化的 RTT可以直接入库。那次之后我每次写完网络工具都会强制自己过一遍三件事防火墙规则有没有动过、目标程序是否需要管理员权限、句柄和缓冲区是不是在循环外统一管理的。这三件事在 Windows 网络编程里翻车概率远比算法高提早确认比事后排错省时间。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑