ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零实现ping:ICMP报文、raw socket与网络排障实战

从零实现ping:ICMP报文、raw socket与网络排障实战 简介一份面向计算机网络课程设计的《Ping程序的设计与实现》完整报告文档适合计算机类专业学生在完成网络编程、ICMP协议与Winsock编程相关课题时参考。资源共1个doc文件压缩包仅189KB内容紧凑涵盖从需求分析到代码实现与界面美化的全流程。已有402人学习/下载。报告详细讲解了基于原始套接字实现Ping的原理包括创建SOCK_RAW套接字、构造ICMP请求报文、计算校验和、调用sendto与recvfrom收发数据并给出了checksum函数的完整实现。课程设计任务覆盖ping基本功能及ping -t命令同时设计了可视化界面可帮助读者快速掌握C语言网络编程中库文件导入、Winsock初始化与注销、Socket选项设置、主机名与IP地址转换等关键技巧。文档还包含课程设计任务书、进度安排与报告框架便于直接借鉴或扩展为个人课程设计成果。1. ping 程序设计与实现从课程设计到真正理解网络协议栈如果你在网上搜索“ping 程序设计”多半会看到两种东西一是谢希仁《计算机网络》课后习题里对 ICMP 报文格式的默写要求二是一堆可以直接复制粘贴的 C 语言源码。但它们之间缺了一条线为什么 ping 的输出格式是64 bytes from 1.1.1.1: icmp_seq1 ttl52 time12.3 ms这条线连不上课程设计就容易写成“套一个 raw socket 循环发报文”答辩时被问一句“超时重传你怎么实现的”就卡住。课程设计的价值不在于把 ping 写出来而在于通过 ping 把「应用层调用 → 传输层封装 → 网络层 ICMP 构造 → 链路层发送 → 接收解析」这条完整链路跑通。这篇文章按我实际做这个课程设计时的思路来写先用抓包数据反推 ICMP 的结构约定再实现 raw socket 收发和超时重传最后给出 Linux 和 Windows 双平台的测试矩阵以及常见问题的定位路径。读者可以是还没开工的学生也可以是打算拿 ping 做网络诊断工具、需要补协议细节的工程师。2. ICMP 回显协议与 ping 的报文结构先搞清楚要发什么2.1 从一次抓包反推 ICMP 报文布局在写代码前我建议先在 Wireshark 里抓一次真实的 ping 包。过滤表达式icmp然后对百度或局域网网关执行ping -c 4 10.0.0.1观察 Echo Request 报文。你会看到三个关键字段Type8、Code0、Identifier 和 Sequence Number。Type8 是请求Type0 是回显应答Code 在回显场景恒为 0。这个设计很有意思回显请求和应答共用一套报文格式只靠 Type 区分方向这正是 ICMP 高效的地方——解析器和构造器可以复用同一段代码。Identifier 和 Sequence Number 各占 2 字节合起来 4 字节加上 Type 和 Code 的 2 字节以及 Checksum 的 2 字节ICMP 报文头就是固定的 8 字节。头后面是数据区也就是我们常说的 payload。Windows ping 默认发送 32 字节 payloadLinux 默认 56 字节。Wireshark 里显示的data字段其实就是填充进去的任意内容通常是时间戳和序号。有些教材把这个数据区称为“可选数据”但为了兼容性和可观测性课程设计里最好填上时间戳——后续计算 RTT 会用到直接读clock_gettime的结果写入 payload接收端就能算出往返时间不需要额外状态表。2.1.1 校验和为什么是“反码求和”代码怎么写ICMP 校验和计算是整个报文构造里最容易错、也最能体现基础功底的部分。它的算法是把 ICMP 报文含头部和数据按 16 位一组划分不足 16 位的尾部补零然后做反码求和最后取反。所谓“反码求和”是指加法过程中出现最高位进位时要把进位值加回到最低位end-around carry。很多初学实现只做sum *(unsigned short*)ptr而忘记回卷进位导致计算出的校验和总是差一点。一个正确的参考实现uint16_t icmp_checksum(const uint8_t *data, size_t len) { uint32_t sum 0; uint16_t *ptr (uint16_t *)data; size_t n len / 2; while (n--) { sum *ptr; if (sum 0xFFFF0000) { sum 0xFFFF; // 保留低16位 sum 1; // 把溢出进位回卷到最低位 } } if (len 1) { // 奇数长度补零处理 uint8_t tail[2] {data[len - 1], 0}; sum *(uint16_t *)tail; sum (sum 0xFFFF) (sum 16); } return (uint16_t)~sum; }这段代码有两处世故之处。第一if (sum 0xFFFF0000)的处理把单次 32 位累加中可能累积的多个进位链处理掉了比某些教材里只做一次回卷的实现更稳健第二奇数长度不是常见情况ICMP 头 8 字节payload 通常是偶数长度但写进去说明你对网络字节流的边界有意识。在构造报文时先将校验和字段置 0 再计算填回到偏移 2 的位置。2.2 DNS 解析与 IP 地址显示ping 的输出里那些括号是什么ping 命令在第一次运行时会先对目标主机名做getaddrinfo。ping www.baidu.com的典型输出里PING www.baidu.com (110.242.68.66)括号里的 IP 就是解析结果。在课程设计里这一步直接决定后续 raw socket 的sendto目标地址。注意getaddrinfo可能返回多个地址实际 ping 命令通常只使用第一个返回的 IPv4 地址这也解释了为什么有时连续 ping 一个负载均衡域名IP 会变——因为 DNS 响应是轮询的。struct addrinfo hints {0}; struct addrinfo *result NULL; hints.ai_family AF_INET; hints.ai_socktype SOCK_RAW; if (getaddrinfo(target_host, NULL, hints, result) ! 0) { perror(DNS resolution failed); return -1; } struct sockaddr_in *addr (struct sockaddr_in *)result-ai_addr; inet_ntop(AF_INET, addr-sin_addr, ip_buf, sizeof(ip_buf)); printf(PING %s (%s)\n, target_host, ip_buf);这里需要区分两种场景如果你要 ping 的是一个 IP 地址如192.168.1.1getaddrinfo也不会失败它会把点分十进制字符串解析成二进制地址这属于正常的协议栈路径。课程设计的验收标准里如果要求支持“域名 ping 通”那getaddrinfo的失败分支必须单独处理并打印unknown host这正好对应实践里常见的ping: www.baidu.com: Name or service not known错误。3. 基于 raw socket 的实现方案从创建套接字到回显接收3.1 在 Linux 上用 SOCK_RAW 构造 ping 主循环的最小骨架Linux 平台实现 ping 需要三个权限路径要么以 root 运行要么给可执行文件设置cap_net_raw能力要么在用户命名空间里创建 raw socket。主循环的逻辑结构看起来简单但写清楚并不容易——至少要有「构造 ICMP 回显请求 → 发送 → 设置接收超时 → recvfrom → 校验匹配 → 输出统计」这六步。下面这段代码是我课程设计里最核心的发送部分整理出来可直接编译验证int ping_send(int sockfd, const struct sockaddr *dest, uint16_t seq) { struct icmp_echo_packet { uint8_t type; // 8: echo request uint8_t code; // 0 uint16_t checksum; uint16_t id; // 标识通常取 PID uint16_t seq; // 本次序号 struct timespec ts; // payload 中的发送时间戳16字节 } packet; memset(packet, 0, sizeof(packet)); packet.type 8; packet.id htons(getpid() 0xFFFF); packet.seq htons(seq); clock_gettime(CLOCK_MONOTONIC, packet.ts); // 填入单调时钟避免系统时间调整影响 RTT 计算 packet.checksum icmp_checksum((uint8_t *)packet, sizeof(packet)); ssize_t n sendto(sockfd, packet, sizeof(packet), 0, dest, sizeof(struct sockaddr_in)); if (n -1) { perror(sendto failed); return -1; } return 0; }关键点有三个第一id字段用getpid() 0xFFFF来填充这是 ping 命令的惯例目的在于多进程并发时通过 id 区分不同 ping 实例的应答第二时间戳放在 payload 而不是头部因为 ICMP 头只有 8 字节硬塞时间戳会破坏协议兼容性第三CLOCK_MONOTONIC比gettimeofday更适合测 RTT因为前者不受ntpdate或手动改系统时间的影响。接收端要处理的不仅仅是 ICMP 报文本身。raw socket 接收到的数据是完整的 IP 包包含了 IP 头20 字节以上所以读到的缓冲区前 20 字节是 IP 头偏移 20 之后才是 ICMP 头。很多初学者直接用recvfrom拿到的数据去解析 ICMP 时把type读成了 IP 头的版本号或 TOS 字段造成校验失败。正确做法是先用iphdr-protocol判断是否为IPPROTO_ICMP再按偏移 20 访问 ICMP 结构体。3.1.1 超时重传这不是 TCP别发重传需要想明白一个概念标准 ping 的「超时重传」并不是真正重发数据包而是只输出一个Request timeout for icmp_seq 0提示然后让序号继续递增。这和 TCP 的重传语义完全不同。从协议设计上看ICMP 回显是一种尽力而为的诊断信号不需要可靠交付保证。如果网络拥塞导致丢包重发只会加剧拥塞。课程设计报告里如果能把这个设计意图写清楚比贴几百行代码更能体现对协议栈的理解。接收超时用setsockoptSO_RCVTIMEO实现struct timeval timeout {1, 0}; // 1秒 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, timeout, sizeof(timeout));这样设置后如果 1 秒内没有收到匹配的应答recvfrom会返回 -1 且errno EAGAIN或EWOULDBLOCK。此时打印超时信息然后 seq进入下一次循环。注意不要调用sendto重发同一 seq 的报文——除非你设计的是「超时重发但序号保持不变」的增强模式那已经偏离标准 ping 行为了。3.2 Windows 平台差异ICMP.dll vs raw socket 的权限墙Windows 对 raw socket 的限制比 Linux 严格得多。从 Windows XP SP2 开始SOCK_RAW类型在创建后会拒绝sendtoICMP 报文除非额外调用WSAIoctl设置SIO_RCVALL这要求管理员权限且容易触发防火墙拦截。所以 Windows 上实现 ping 课程设计业界最常见的做法是调用系统提供的ICMP.dll导出函数IcmpSendEcho。HANDLE hIcmp IcmpCreateFile(); if (hIcmp INVALID_HANDLE_VALUE) return -1; char send_data[32] course design payload; char reply_buf[sizeof(ICMP_ECHO_REPLY) 32]; DWORD reply_size sizeof(ICMP_ECHO_REPLY) 32; DWORD status IcmpSendEcho(hIcmp, dest_ip_netorder, send_data, 32, NULL, reply_buf, reply_size, 1000); if (status 0) { DWORD err GetLastError(); // 127: 目标不可达 11001: 找不到主机 11003: 超时 } PICMP_ECHO_REPLY reply (PICMP_ECHO_REPLY)reply_buf; // reply-RoundTripTime 即是 RTT(ms)reply-Address 是响应方 IP IcmpCloseHandle(hIcmp);这里有个隐藏的性能细节IcmpSendEcho是同步阻塞的内部实现帮你处理了防碎片化和应答校验所以课程设计里的 Windows 版本可以重点展示「同一时间探测多个目标」的工程能力而不是重复实现协议逻辑。如果指导老师要求必须自己封装 IP 头那就得做好心理准备——Windows 的SIO_RCVALL会收到本机所有进出的流量过滤逻辑比 Linux 复杂一个量级通常课程设计计时不允许这么干。4. 统计信息、TTL 解码与排错实战4.1 用表格比对 Linux/Windows 的 ping 输出差异操纵细节差异是排错的基础。写一个兼容两平台的 ping 程序时至少要在代码里做两个平台的输出适配输出字段Linux (iputils-ping)Windows (ping.exe)序列号显示icmp_seq1Reply from 1.1.1.1: bytes32 time12ms TTL52超时提示Request timeout for icmp_seq 1Request timed out.统计数据1 packets transmitted, 1 received, 0% packet lossLost 0 (0% loss)TTL 显示ttl52TTL52TTL 字段值得单独讲透。它位于 IP 头偏移 8 的位置每经过一个路由器减 1减到 0 时路由器丢弃该包并发回 ICMP Time ExceededType11。ping 输出里ttl52是响应包里的 TTL不是请求包发出去时的 TTL。这带来一个推算初始 TTL 的技巧如果响应 TTL 是 52且你假设远端 Linux 主机的初始 TTL 是 64则64 - 52 12说明路径上有 12 跳但这个假设不一定成立Windows 初始 TTL 是 128某些网络设备初始 TTL 是 255。课程设计的进阶验收里可以加一个-t参数开启 TTL 递减追踪但警告这已经接近traceroute的实现了。4.2 虚拟机 ping 不通网关和百度的快速定位顺序网上的搜索引擎热词里「虚拟机 ping 不通百度」「centos ping 不通网关」出现频率极高这正好是课程设计完成后的自测场景。我总结的定位顺序先ip addr看网卡有没有 IP再ip route show看默认网关在不在然后 ping 网关 IP 测二层连通性网关通了再 ping 公网 IP如114.114.114.114测 NAT 和路由公网 IP 通了再 ping 域名测 DNS。这个顺序的数学逻辑是二分聚集每次前进一层协议栈抽象定位精确到网络栈的某一层。# 在虚拟机内依次执行观察输出在哪一步卡住 ip addr show eth0 ip route show default ping -c 2 192.168.1.1 # 网关 ping -c 2 114.114.114.114 # 公网IP ping -c 2 www.baidu.com # 域名如果网关不通查虚拟机网卡是否为 NAT 模式、宿主机的虚拟网卡是否被禁用如果网关通但公网 IP 不通优先查宿主机的防火墙是否拦截了 ICMPWindows 防火墙默认拦截入站 ping但允许出站。这个技巧在课程设计报告里作为「测试与验证」章节非常加分——它证明你不仅会写代码还能用它诊断真实网络。4.3 校验应答包防串包的三重过滤接收端不能「收到什么都算数」。必须做三重过滤第一IP 头里的源地址必须和ping的目标地址相同在recvfrom里比对src_addr.sin_addr和目标地址第二ICMP 头的id字段必须等于getpid() 0xFFFF第三seq字段必须等于当前期望的seq。这三重过滤对应三种实际情况其他主机恰好发来的 ICMP 报文、同机上其他 ping 进程的应答、以及上一个轮次的迟到应答。如果你忽略第三重过滤一个迟到的旧应答会被当作本次的响应导致 RTT 被错误标成几百毫秒甚至几秒。5. 一个实打实的进阶点用 cmsg 读取双程 TTL 与丢包统计的滑动窗口课程设计交完验收之后如果你想让它继续长成项目可以做两个补充模块——它们不会改变 ping 的核心语义但会明显提升程序的工程可用性。第一个是利用recvmsg的辅助数据ancillary data读取 IP TTL。普通recvfrom只能拿到 ICMP 内容拿不到 IP 头而recvmsg配合IP_RECVTTL套接字选项可以拿到实际响应包的 TTL。这段辅助数据不仅是展示信息还能反向推算链路跳数适合加在-v详细模式里。int on 1; setsockopt(sockfd, IPPROTO_IP, IP_RECVTTL, on, sizeof(on)); char control[64]; struct iovec iov {buf, sizeof(buf)}; struct msghdr msg {0}; msg.msg_iov iov; msg.msg_iovlen 1; msg.msg_control control; msg.msg_controllen sizeof(control); if (recvmsg(sockfd, msg, 0) -1) return -1; struct cmsghdr *cmsg CMSG_FIRSTHDR(msg); for (; cmsg; cmsg CMSG_NXTHDR(msg, cmsg)) { if (cmsg-cmsg_level IPPROTO_IP cmsg-cmsg_type IP_TTL) { int ttl *(unsigned char *)CMSG_DATA(cmsg); printf(ip_ttl%d\n, ttl); } }第二个进阶点是丢包率的滑动窗口计算。标准 ping 最终输出的统计是整体累计的packet loss但这掩盖了网络抖动的时间分布。你可以把最近 20 个 seq 的到达情况放进一个环形数组每次收到或超时后重新计算窗口内的丢包率并画一条简单的文本折线空格代表到达.代表丢失。这样连续 ping 10 分钟一眼就能看出丢包是集中在某段时间还是均匀分布——对训练排障直觉很有价值。代码实现不复杂核心是维护uint32_t loss_flags这个位掩码用位运算替代环形数组#define WINDOW_SIZE 20 uint32_t loss_flags 0; // 每个 bit 代表一个 seq 的到达情况1到达0丢失 // 新 seq 到达时处理 loss_flags (loss_flags 1) | 1; if (seq WINDOW_SIZE) { // 只保留最近 WINDOW_SIZE 位用掩码截断 loss_flags (1u WINDOW_SIZE) - 1; } // 超时处理移位并补 0 // 窗口内丢包率 (WINDOW_SIZE - __builtin_popcount(loss_flags)) * 5 / 100__builtin_popcount在 GCC 和 Clang 下都可以用MSVC 下可以改成手动查表。这个技巧的巧妙之处在于ping 的 sequence number 是严格递增的所以每个 seq 在loss_flags里的位置是固定的无需存储 seq 本身。把它作为课程设计的 Bonus 部分写进报告答辩老师会认为你已经跳出“实现一个命令”的层面开始思考“可观测性指标该怎么可视化”了。课程设计结束的标准不是程序能跑而是你能回答三个问题ICMP 校验和的进位回卷为什么不能用普通加法替代为什么 raw socket 收到的数据要跳过 IP 头才能解析 ICMP同样是超时为什么 ping 不按 TCP 的方式重传把这三个问题想透这份设计就真正属于你了。本文还有配套的精品资源点击获取
返回列表