简介这是一份面向网络编程初学者与系统开发者的C语言实战资料围绕ICMP协议与原始套接字讲解如何从零实现一个具备基本诊断能力的Ping程序帮助读者理解网络连通性检测的底层原理。压缩包共2个文件包含1个cpp源码与1个txt说明文档整体约2KB源码可直接编译运行文档则补充了实现思路与参考链接便于对照学习。资源重点覆盖ICMP回显请求与应答报文的构造、原始套接字的创建、校验和计算、数据包收发以及往返时间统计等关键环节并涉及网络不可达、权限不足等常见错误的处理思路。目前已有414人学习适合希望深入套接字编程、提升故障排查能力的中级开发者参考实践。1. 从一份 .rar 说起为什么网络排障老手都自己写过 Ping手里这份用C语言实现Ping程序功能.rar解压后核心就是一个ip.cpp加一份www.pudn.com.txt说明。它干的事很朴素不调用系统自带的 ping 命令而是用原始套接字自己构造 ICMP 回显请求发出去、收回来、算 RTT、统计丢包。很多人第一次看到会觉得多此一举——系统 ping 不香吗但真做过网络诊断、抓过包、被内核协议栈的“黑匣子”行为坑过的人往往都会自己撸一个。原因很简单只有自己拼过 ICMP 头部、自己算过校验和你才真正知道 ping 那一行timexx ms背后发生了什么。这份资源适合三类人正在学 C 语言网络编程、想找一个能跑通的原始套接字实战的人做运维或后端、需要把连通性探测嵌进自己程序里的人以及准备面试、被问到“ICMP 和 TCP/UDP 区别”“原始套接字权限”时想有底气的人。它不依赖第三方库Linux 下gcc直接编Windows 下走 Winsock属于那种“代码不长但知识点密度极高”的练手件。下面我按自己拆包复现的顺序把它讲透。2. ICMP 报文结构与原始套接字先把地基打对2.1 为什么必须用 SOCK_RAW 而不是普通 socket普通SOCK_STREAMTCP和SOCK_DGRAMUDP都由内核帮你封装好传输层头部你只管塞数据。但 ICMP 不是传输层协议它是 IP 层的一部分内核没有给你留一个“ICMP 专用 socket 类型”。要自己指定 ICMP 的类型、代码、校验和唯一的路子就是SOCK_RAW配合IPPROTO_ICMP。这意味着两件事第一你得自己填 ICMP 头部第二在 Linux 上这需要 root 权限或CAP_NET_RAW能力否则socket()直接返回 -1errno是EPERM。这是新手第一个翻车点后面避坑章节细说。常见做法是int sockfd socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd 0) { perror(socket); // 权限不足时这里会打印 Operation not permitted return -1; }AF_INET指定 IPv4SOCK_RAW表示原始套接字IPPROTO_ICMP告诉内核“我关心的是 ICMP 协议”。注意这里第三个参数不是 0写 0 在某些系统上会拿到一个能收所有协议的原始套接字反而更容易收到无关报文干扰解析。2.2 ICMP 头部字段逐个拆ICMP 回显请求类型 8和回显应答类型 0共用同一套头部结构共 8 字节后面跟可变长度的数据区。字段定义如下字段长度回显请求取值回显应答取值说明Type1 字节80报文类型Code1 字节00回显类报文固定为 0Checksum2 字节计算得出计算得出覆盖整个 ICMP 报文Identifier2 字节进程标识原样返回用来匹配请求与应答Sequence2 字节递增序号原样返回判断丢包与乱序Data可变任意填充原样返回常放时间戳用于算 RTTIdentifier 这一项很关键。同一台机器上可能同时有好几个程序在发 ICMP内核把应答分发给哪个原始套接字靠的就是 Identifier 和源地址匹配。我一般用getpid() 0xFFFF作为标识既唯一又省事。Sequence 从 1 开始递增每发一个加一收到应答时比对序号就能知道是哪个请求的回应。2.3 校验和算法那个经典的 16 位反码求和ICMP 校验和是绕不过去的坎算错了对方直接丢弃你只会看到“一直超时”非常玄学。算法本身是标准的 16 位反码求和把报文按 16 位分组累加进位回卷最后取反。unsigned short checksum(void *buf, int len) { unsigned short *p buf; unsigned int sum 0; while (len 1) { sum *p; // 逐 16 位累加 len - 2; } if (len 1) { // 奇数长度补一个字节 sum *(unsigned char *)p; } sum (sum 16) (sum 0xFFFF); // 进位回卷 sum (sum 16); // 再卷一次防止二次进位 return (unsigned short)(~sum); // 取反 }逻辑说明sum用 32 位是为了容纳累加过程中的进位(sum 16) (sum 0xFFFF)把高 16 位的进位加回低 16 位再执行一次是因为第一次回卷可能又产生进位。参数buf指向 ICMP 报文起始地址len是整个 ICMP 报文长度头部 8 字节加数据区。计算前必须先把 Checksum 字段置 0否则算出来是错的——这是第二个高频翻车点。3. 从发包到收包一次完整 Ping 的代码落地3.1 构造并发送 ICMP 回显请求把头部结构体定义好之后填充、算校验和、sendto三步走。结构体我习惯用紧凑定义struct icmp_hdr { unsigned char type; unsigned char code; unsigned short checksum; unsigned short id; unsigned short seq; }; struct icmp_pkt { struct icmp_hdr hdr; struct timeval tv; // 数据区放发送时间戳用于算 RTT };发送逻辑struct icmp_pkt pkt; memset(pkt, 0, sizeof(pkt)); pkt.hdr.type 8; // 回显请求 pkt.hdr.code 0; pkt.hdr.id getpid() 0xFFFF; // 进程标识 pkt.hdr.seq seq; // 递增序号 gettimeofday(pkt.tv, NULL); // 记录发送时刻 pkt.hdr.checksum 0; // 先清零 pkt.hdr.checksum checksum(pkt, sizeof(pkt)); // 再计算 struct sockaddr_in dst; memset(dst, 0, sizeof(dst)); dst.sin_family AF_INET; inet_pton(AF_INET, target_ip, dst.sin_addr); int n sendto(sockfd, pkt, sizeof(pkt), 0, (struct sockaddr *)dst, sizeof(dst));参数说明sendto的第四个参数 flags 填 0 即可目标端口对 ICMP 无意义sockaddr_in里的sin_port保持 0。sizeof(pkt)是 ICMP 报文总长包含 8 字节头部加timeval数据区。注意gettimeofday放在算校验和之前因为时间戳也是报文内容的一部分必须参与校验和计算。3.2 接收应答并过滤无关报文原始套接字有个特性它会收到内核递交给它的所有 ICMP 报文包括别人触发的、目标不可达的、甚至本机其他程序产生的。所以recvfrom回来之后不能无脑当成自己的应答必须逐层过滤。char recvbuf[1024]; struct sockaddr_in from; socklen_t fromlen sizeof(from); int n recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)from, fromlen); if (n 0) { perror(recvfrom); return -1; } struct iphdr *ip (struct iphdr *)recvbuf; // 原始套接字含 IP 头 int ip_hlen ip-ihl * 4; // IP 头长度按 4 字节为单位 struct icmp_hdr *icmp (struct icmp_hdr *)(recvbuf ip_hlen); if (icmp-type ! 0) return 0; // 只要回显应答 if (icmp-id ! (getpid() 0xFFFF)) return 0; // 只认自己的标识逻辑说明Linux 原始套接字收到的数据默认包含 IP 头部所以要先跳过ihl * 4字节再解析 ICMP。ihl字段单位是 4 字节最小值 5即 20 字节带选项时会更长所以不能写死 20。过滤条件两个类型必须是 0回显应答标识必须匹配本进程。少了任何一条你就可能把别人的应答算进自己的 RTT数据全乱。3.3 计算 RTT 与统计丢包率收到合法应答后从数据区取出原始发送时间戳和当前时间相减就是 RTT。统计部分维护三个量已发送数、已接收数、RTT 累加和。struct timeval now, *sent; gettimeofday(now, NULL); sent (struct timeval *)((char *)icmp 8); // 跳过 8 字节 ICMP 头 long rtt_us (now.tv_sec - sent-tv_sec) * 1000000L (now.tv_usec - sent-tv_usec); double rtt_ms rtt_us / 1000.0; sent_count; recv_count; rtt_sum rtt_ms; if (rtt_ms rtt_min) rtt_min rtt_ms; if (rtt_ms rtt_max) rtt_max rtt_ms; printf(%d bytes from %s: icmp_seq%d ttl%d time%.3f ms\n, n - ip_hlen, inet_ntoa(from.sin_addr), icmp-seq, ip-ttl, rtt_ms);参数说明rtt_us用微秒累加避免浮点误差ttl从 IP 头取能粗略反映经过的路由跳数。丢包率在循环结束后用(sent_count - recv_count) / sent_count * 100%算。这里有个细节recvfrom返回的n是包含 IP 头的总长度打印“字节数”时要减掉ip_hlen否则显示的数字会比标准 ping 大 20容易被误认为报文异常。4. 避坑与排查那些让你怀疑人生的报错4.1 socket 返回 -1errno 是 EPERM现象程序一启动就打印socket: Operation not permitted直接退出。原因Linux 从内核层面限制原始套接字普通用户无权创建SOCK_RAW。解决用sudo运行或者给可执行文件加能力sudo setcap cap_net_rawep ./myping后者更优雅不用每次提权。注意setcap对文件系统有要求某些挂载选项下不生效那就老老实实 sudo。4.2 一直超时但系统 ping 是通的现象自己写的程序发出去石沉大海终端里系统ping同一个地址却正常。原因八成是校验和算错了或者计算校验和之前没把 Checksum 字段清零。对方主机收到校验和错误的 ICMP 报文会直接静默丢弃你这边只能看到超时。解决先用tcpdump -i any icmp抓包看报文有没有发出去、对方有没有回再单独写个测试用例验证checksum函数对已知报文的输出。我一般会拿一个手工算好的样例比对确认算法无误再往下走。4.3 收到的应答 seq 对不上、RTT 忽大忽小现象打印出来的icmp_seq乱序或者 RTT 出现几百毫秒的异常值。原因原始套接字会收到本机其他 ICMP 流量过滤不严就会混进来另外多线程或多次运行同一程序时Identifier 如果都用固定值会互相干扰。解决Identifier 用getpid()保证进程间唯一过滤时同时校验 id 和源地址如果程序内部多线程发包再叠加一个线程号或随机数。RTT 异常值多半是混入了别人的应答过滤严了就正常。4.4 在 Windows 上编译报错找不到 iphdr现象代码从 Linux 搬到 Windowsstruct iphdr、struct icmp_hdr全部未定义。原因这些是 Linux 内核头文件里的结构Windows Winsock 不提供。解决Windows 下要自己定义这些结构体并且用WSAStartup初始化原始套接字创建方式也略有不同需要管理员权限。如果只是学习 ICMP 原理建议先在 Linux 下跑通要跨平台就把协议结构体抽出来自己定义别依赖系统头。4.5 目标不可达时程序卡死现象ping 一个不存在的地址程序一直阻塞在recvfrom不返回。原因recvfrom默认是阻塞模式没有应答就永远等下去。解决用setsockopt设置接收超时SO_RCVTIMEO超时后recvfrom返回 -1 且errno为EAGAIN或EWOULDBLOCK此时判定为丢包继续发下一个。超时时间一般设 1 到 2 秒和系统 ping 的默认行为对齐。struct timeval tv {2, 0}; // 2 秒超时 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, tv, sizeof(tv));5. 进阶技巧把玩具 Ping 改成能进工具箱的探测件跑通基础版之后这份代码还有不少可挖的空间。第一个方向是批量探测与并发。单线程一个一个 ping 效率太低可以把 socket 设成非阻塞用select或poll同时管理多个目标一次轮询里把该发的发出去、该收的收回来。这样扫一个 C 段254 个地址也就几秒钟比 shell 里循环调系统 ping 快一个数量级。实现要点是维护一张“已发送未应答”的表按 Identifier 加 Sequence 做键收到应答就查表匹配、算 RTT、标记完成。第二个方向是把结果结构化输出。系统 ping 的输出是给人看的嵌进监控系统还得正则解析。我一般会加一个--json开关把每个目标的sent、recv、loss、min/avg/max打成 JSON 一行直接喂给日志采集或告警管道。改动不大就是在统计结构体上套一层序列化。第三个方向是区分错误类型。基础版只统计“收到/没收到”但 ICMP 的类型 3目标不可达、类型 11超时携带了更具体的原因比如网络不可达、主机不可达、端口不可达。把这些类型解析出来排障时能直接告诉你“是路由问题还是对方防火墙挡了”比单纯一句“超时”有用得多。解析时注意这些差错报文的数据区里会嵌入原始 IP 头和前 8 字节 ICMP要按 RFC 规定偏移去取。验证自己实现是否正确有个笨但有效的办法同一时刻开两个终端一个跑你的程序一个跑tcpdump -i any -n icmp对比发出的报文数量、类型、校验和字段。如果 tcpdump 里能看到你的请求和对方的应答而你的程序却没打印那问题一定在接收过滤或解析环节而不是网络。这个对照法我用了很多年比盯着代码空想快得多。最后说个习惯。从那以后我每次写涉及报文构造的代码都会先拿一个已知正确的报文做单元测试校验和、长度、字段偏移全部对一遍再往下写业务逻辑。ICMP 这种协议错一个字节就是全盘静默没有后悔药可吃。希望这份拆解能帮你少走几个弯路把这份 .rar 里的东西真正变成自己的。本文还有配套的精品资源点击获取