尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手写Ping程序:Winsock原始套接字与ICMP协议实战

从零手写Ping程序:Winsock原始套接字与ICMP协议实战 简介这份计算机网络课程设计报告面向高校计算机相关专业学生聚焦用Winsock技术实现Ping应用程序这一典型网络编程课题适合正在完成课设、需要理解套接字编程与ICMP协议原理的学习者参考。资源包内含1个doc文档大小约333KB内容按绪论、需求分析、详细设计、设计小结、参考文献与附录组织结构完整可直接作为课设撰写与实现的参考模板。报告从Ping、Winsock、ICMP三个基础概念切入梳理了WSAStartup、socket、inet_addr、gethostbyname、recv、sendto等关键函数的用途与调用方式并给出Ping程序的设计流程与实现思路能帮助读者理解应用层如何直接调用网络层ICMP完成回送请求与应答的收发。目前已有235人学习适合需要快速搭建课设框架、理清Winsock编程脉络并补充排错思路的读者。1. 从零手写 Ping为什么 Winsock 课设绕不开 ICMP很多人做计算机网络课程设计选题第一反应就是“用 Winsock 写个 Ping 程序”。听起来简单——不就是发个包收个包吗真动手才发现卡住你的不是 C 语言语法而是 ICMP 协议在 Windows 下的原始套接字权限、校验和计算、超时重传逻辑以及“为什么我 ping 本机通、ping 网关就不通”这类玄学问题。这个题目的本质是让你用 Winsock 的原始套接字SOCK_RAW直接构造并解析 ICMP 报文绕过系统自带的 ping.exe从字节层面理解“连通性探测”到底发生了什么。它适合已经学过 TCP/IP 协议栈、想真正把“IP 层以上、传输层以下”那层黑匣子撬开的学生也适合工作中需要自己写网络探测工具、不想只会调 ping 命令的工程师。热词里“ping 命令与 ICMP 协议分析”“ping 命令的功能和用法”反复出现说明大家真正想搞懂的是那条ping 192.168.1.1背后Winsock 到底替你做了什么、又没替你做什么。2. 动手之前ICMP 报文结构与 Winsock 原始套接字选型2.1 ICMP 回显报文长什么样每个字段为什么不能填错Ping 用的是 ICMP 的 Echo Request类型 8和 Echo Reply类型 0。一个完整的 ICMP 回显报文由 8 字节固定头和可变长度数据组成。固定头里类型和代码各占 1 字节校验和占 2 字节标识符和序列号各占 2 字节。标识符通常填进程 ID用来区分同一台机器上多个 ping 实例序列号从 1 开始递增用来匹配请求和应答。数据部分随便填常见做法是填 32 字节的递增模式或时间戳方便肉眼核对。这里最容易翻车的是校验和。ICMP 校验和覆盖整个 ICMP 报文头数据算法是 16 位反码求和再取反。很多人从网上抄一段 checksum 函数结果 ping 本机通、ping 外网不通就是因为校验和算错了但本机回环不校验。下面这段代码是我反复用过、在 Windows 和 Linux 下都验证过的校验和实现// 计算 ICMP 校验和16 位反码求和后取反 unsigned short checksum(unsigned short *buf, int len) { unsigned long sum 0; while (len 1) { sum *buf; // 逐 16 位累加 len - 2; } if (len 1) { sum *(unsigned char *)buf; // 奇数长度补一个字节 } sum (sum 16) (sum 0xFFFF); // 回卷高位 sum (sum 16); // 再回卷一次 return (unsigned short)(~sum); // 取反 }参数说明buf指向 ICMP 报文起始地址len是报文总长度。注意传入前必须把校验和字段清零否则算出来的结果是错的。这个函数对奇数字节的处理是很多教材忽略的细节但实际抓包对比时能看出差别。2.2 为什么必须用 SOCK_RAW以及 Windows 下的权限门槛Winsock 里能发 ICMP 的套接字类型只有SOCK_RAW协议参数填IPPROTO_ICMP。创建方式SOCKET sock socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock INVALID_SOCKET) { printf(socket() failed: %d\n, WSAGetLastError()); return -1; }这段代码在 Windows 上有一个硬门槛必须用管理员权限运行。普通用户调用socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)会返回WSAEACCES10013。这不是代码写错了是 Windows 从 Vista 开始收紧的原始套接字策略。解决办法只有两个右键“以管理员身份运行”或者在 manifest 里声明requireAdministrator。很多同学在 IDE 里直接 F5 调试忘了提权然后对着 10013 错误码查半天这是血泪经验里排第一的坑。另外SOCK_RAW收到的数据包含 IP 头默认情况下所以解析 ICMP 报文时要先跳过 20 字节的 IP 头如果 IP 头有选项则更长但一般没有。发送时则不需要自己构造 IP 头Winsock 会替你填。这个“收包含 IP 头、发不包含 IP 头”的不对称性是第二个容易搞混的地方。2.3 超时与重传别让 recvfrom 把整个程序卡死Ping 的核心交互是发一个 Echo Request等 Echo Reply超时就重发或报“请求超时”。如果用阻塞式recvfrom网络不通时程序会一直卡住。正确做法是用setsockopt设置接收超时int timeout 1000; // 单位毫秒 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)timeout, sizeof(timeout));这样recvfrom最多等 1 秒超时返回SOCKET_ERROR错误码WSAETIMEDOUT。然后你可以在外层循环里决定是重发还是记录超时。一般课设要求发 4 个包每个包超时 1 秒总共不超过 4 秒。注意SO_RCVTIMEO在 Windows 下参数类型是const char *在 Linux 下是struct timeval跨平台移植时要改。还有一个细节收到的不一定是自己的 Echo Reply。网络上可能有其他 ICMP 包比如目标不可达所以收到包后要检查IP 头里的源地址是否等于目标地址、ICMP 类型是否为 0、标识符是否等于自己的进程 ID。三个条件都满足才算有效应答。只检查类型不检查标识符在多实例同时 ping 时会串包。3. 从代码到可运行Winsock Ping 的完整实现路径3.1 初始化 Winsock 与解析目标地址Windows 下用 Winsock 必须先调WSAStartup退出时调WSACleanup。目标地址可以用inet_addr把点分十进制字符串转成in_addr也可以用getaddrinfo支持域名。课设一般要求支持 IP 和域名两种输入所以推荐getaddrinfoWSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); // 请求 Winsock 2.2 struct addrinfo hints {0}, *res NULL; hints.ai_family AF_INET; // 只用 IPv4 hints.ai_socktype SOCK_RAW; // 原始套接字 hints.ai_protocol IPPROTO_ICMP; // ICMP 协议 int ret getaddrinfo(www.example.com, NULL, hints, res); if (ret ! 0) { printf(getaddrinfo failed: %d\n, ret); WSACleanup(); return -1; } struct sockaddr_in *target (struct sockaddr_in *)res-ai_addr;参数说明MAKEWORD(2,2)表示 Winsock 2.2 版本这是目前最通用的。hints里指定AF_INET就只返回 IPv4 地址避免拿到 IPv6 地址后SOCK_RAW行为不一致。getaddrinfo返回的链表用完要freeaddrinfo释放。如果解析失败常见原因是 DNS 配置问题或域名拼写错误可以先在命令行nslookup验证一下。3.2 构造并发送 Echo Request构造 ICMP 报文时我习惯定义一个结构体但要注意字节对齐问题。Windows 下默认按 4 字节对齐而 ICMP 头是 8 字节刚好对齐所以直接sizeof没问题。但如果你在数据部分放了char数组整个结构体大小可能被填充。稳妥做法是用#pragma pack(1)或者手动拼字节。下面用结构体方式清晰易读#pragma pack(push, 1) // 按 1 字节对齐避免填充 typedef struct { unsigned char type; // 类型8 表示 Echo Request unsigned char code; // 代码0 unsigned short checksum; // 校验和 unsigned short id; // 标识符填进程 ID unsigned short seq; // 序列号 char data[32]; // 数据部分 } ICMPHeader; #pragma pack(pop) ICMPHeader packet; packet.type 8; packet.code 0; packet.checksum 0; packet.id (unsigned short)GetCurrentProcessId(); packet.seq htons(seq); // 序列号转网络字节序 memset(packet.data, A (seq % 26), sizeof(packet.data)); // 填充可识别数据 packet.checksum checksum((unsigned short *)packet, sizeof(packet)); int sent sendto(sock, (char *)packet, sizeof(packet), 0, (struct sockaddr *)target, sizeof(*target));逻辑说明#pragma pack(1)保证结构体没有填充字节否则sizeof(ICMPHeader)可能变成 40 而不是 4083240刚好对齐但换个数据长度就会出问题。id用进程 ID 是为了区分不同 ping 进程。seq用htons转网络字节序因为网络协议是大端。checksum必须在所有字段填好、且校验和字段为 0 时计算。sendto的最后一个参数是目标地址长度SOCK_RAW下必须传sizeof(struct sockaddr_in)。发送成功后sendto返回实际发送的字节数。如果返回SOCKET_ERROR用WSAGetLastError()看错误码。常见的有WSAEHOSTUNREACH目标不可达和WSAENETUNREACH网络不可达这两个通常意味着路由问题不是代码问题。3.3 接收应答并计算 RTT接收用recvfrom缓冲区要足够大因为包含 IP 头。一般 65535 字节的缓冲区肯定够char recvbuf[65535]; struct sockaddr_in from; int fromlen sizeof(from); int recvlen recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)from, fromlen); if (recvlen SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) { printf(请求超时\n); } else { printf(recvfrom 错误: %d\n, err); } continue; } // 跳过 IP 头通常 20 字节但 ihl 字段可能大于 5 int ipHeaderLen (recvbuf[0] 0x0F) * 4; ICMPHeader *reply (ICMPHeader *)(recvbuf ipHeaderLen); if (reply-type ! 0 || reply-id ! (unsigned short)GetCurrentProcessId()) { continue; // 不是自己的 Echo Reply丢弃 } // 计算 RTT DWORD rtt GetTickCount() - sendTime; printf(来自 %s 的回复: 字节%d 时间%lu ms TTL%d\n, inet_ntoa(from.sin_addr), recvlen - ipHeaderLen, rtt, recvbuf[8]);逻辑说明recvbuf[0] 0x0F取 IP 头长度字段IHL乘以 4 得到字节数。这样即使 IP 头带选项也能正确跳过。reply-type ! 0过滤掉非 Echo Reply 的 ICMP 包。reply-id比对进程 ID防止串包。TTL 在 IP 头的第 9 个字节偏移 8直接取recvbuf[8]。RTT 用GetTickCount()计算精度毫秒级课设够用如果要微秒级改用QueryPerformanceCounter。注意inet_ntoa返回的是静态缓冲区多次调用会覆盖如果要保存多个地址需要拷贝。另外recvlen - ipHeaderLen才是 ICMP 报文长度不要直接拿recvlen当 ICMP 长度。3.4 统计输出与命令行参数解析一个完整的 Ping 程序最后要输出统计信息发送几个包、收到几个、丢包率、最小/最大/平均 RTT。这些用几个全局变量累加即可。命令行参数解析可以用argc/argv手动判断也可以支持-n指定次数、-l指定数据长度、-w指定超时。课设一般要求支持ping host和ping -n 4 host两种形式。int count 4; // 默认发 4 个包 if (argc 3 strcmp(argv[1], -n) 0) { count atoi(argv[2]); argv 2; argc - 2; } if (argc 2) { printf(用法: ping [-n 次数] 目标地址\n); return 1; } char *host argv[1];参数说明atoi不检查错误如果用户输入-n abc会得到 0最好用strtol并检查。argv 2是一种简化写法把参数指针往后移后面统一用argv[1]取目标。这种写法在课设里够用但生产代码里建议用getopt。统计输出示例正在 Ping www.example.com [93.184.216.34] 具有 32 字节的数据: 来自 93.184.216.34 的回复: 字节32 时间12ms TTL56 来自 93.184.216.34 的回复: 字节32 时间11ms TTL56 请求超时。 来自 93.184.216.34 的回复: 字节32 时间13ms TTL56 93.184.216.34 的 Ping 统计信息: 数据包: 已发送 4已接收 3丢失 1 (25% 丢失) 往返行程的估计时间(以毫秒为单位): 最短 11ms最长 13ms平均 12ms这个输出格式和 Windows 自带 ping 基本一致课设答辩时比较讨巧。4. 避坑与排查那些让课设卡三天的典型问题4.1 坑一socket() 返回 10013程序直接退出现象编译通过运行后打印socket() failed: 10013或者直接闪退。原因Windows 下创建SOCK_RAW需要管理员权限普通用户被拒绝。解决右键可执行文件“以管理员身份运行”或者在 Visual Studio 里把调试配置的“链接器→清单文件→UAC 执行级别”改成requireAdministrator。如果用的是 Code::Blocks 或 Dev-C需要手动写 manifest 文件并嵌入。这个坑在热词“windows11 已关闭 windowsdefender ip 地址无法 ping 通”里也有类似体现——权限和防火墙是两回事但都会让 ping 失败。4.2 坑二能收到包但 RTT 显示为 0 或负数现象GetTickCount()相减得到 0 或者一个很大的数。原因GetTickCount()返回 DWORD如果发送时间在接收时间之后多线程或时间回绕相减会得到一个巨大的正数。另外如果发送后没有立即记录时间而是先做其他操作再记录RTT 会偏小。解决发送前记录sendTime GetTickCount()收到后立即rtt GetTickCount() - sendTime。如果担心 49.7 天回绕用GetTickCount64()。课设运行时间短一般不会回绕但养成好习惯没坏处。4.3 坑三ping 域名通、ping IP 不通或者反过来现象ping www.baidu.com正常ping 8.8.8.8超时或者反过来。原因前者通常是 DNS 解析成功但 ICMP 被中间设备拦截后者是 DNS 配置问题。还有一种情况是getaddrinfo返回了 IPv6 地址而你的SOCK_RAW只处理 IPv4导致发送失败。解决在hints里强制AF_INET并且打印解析到的 IP 地址确认。如果 IP 能通但域名不通检查nslookup是否正常。热词里“centos7 无法 ping 通百度”“虚拟机 ping 不通百度”多半是网络配置或防火墙问题和代码无关但课设答辩时老师可能会问提前用ipconfig /all和route print确认本机网络状态。4.4 坑四校验和算错本机通外网不通现象ping 127.0.0.1正常ping 192.168.1.1超时。原因回环接口不校验 ICMP 校验和所以校验和错了也能收到回复但真实网卡和中间路由器会校验错了直接丢弃。解决用 Wireshark 抓包对比你的程序发出的 ICMP 包和系统 ping 发出的包看校验和字段是否一致。常见错误包括计算前没清零校验和字段、字节序没转换、奇数字节处理遗漏。我一般会在发送前打印校验和值和 Wireshark 里的值比对一次就能定位。4.5 坑五recvfrom 收到自己的请求包现象程序打印的回复类型是 8 而不是 0或者 RTT 异常小。原因SOCK_RAW在某些配置下会收到本机发出的所有 ICMP 包包括自己的 Echo Request。解决检查reply-type 0且reply-id GetCurrentProcessId()。两个条件缺一不可。另外如果绑定了具体网卡也可能收到其他程序的 ICMP 包所以标识符比对是必须的。5. 进阶技巧用 Wireshark 验证与跨平台移植的取舍课设做完能跑只是及格线真正让老师眼前一亮的是你能证明“我的包和系统 ping 的包在字节层面一致”。我一般会同时开 Wireshark 抓回环口或真实网卡过滤icmp然后对比三样东西类型/代码、校验和、数据部分。如果校验和一致、数据模式一致说明你的构造逻辑没问题。Wireshark 里还能看到 TTL 和 IP 头的 DSCP 字段这些是系统 ping 默认填的你的程序可以不填但要知道差异在哪。跨平台方面Winsock 和 Linux 的 socket API 有三大差异一是初始化Windows 要WSAStartupLinux 不用二是错误码Windows 用WSAGetLastError()Linux 用errno三是SO_RCVTIMEO的参数类型不同。如果课设只要求 Windows不用考虑移植但如果想放到 GitHub 上当作品集建议用宏区分#ifdef _WIN32 WSADATA wsaData; WSAStartup(MAKEWORD(2,2), wsaData); #define CLOSE_SOCKET closesocket #define GET_ERR WSAGetLastError() #else #define CLOSE_SOCKET close #define GET_ERR errno #endif这样核心逻辑不用改只替换初始化和错误处理部分。Linux 下SOCK_RAW同样需要 root 权限或者设置CAP_NET_RAW能力。另外 Linux 下recvfrom收到的包是否包含 IP 头取决于是否设置了IP_HDRINCL默认包含和 Windows 一致。最后一个技巧把 RTT 统计做成直方图输出或者把每次 ping 的结果写入 CSV 文件方便写课设报告时画图。这个工作量不大但报告里有一张 RTT 变化曲线比纯文字描述有说服力得多。我当年做课设时就是靠一张 Wireshark 对比截图和一张 RTT 折线图答辩多拿了 5 分。希望帮到你。本文还有配套的精品资源点击获取
返回列表