ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ICMP原始套接字实现Tracert路由追踪原理与VC6实战

ICMP原始套接字实现Tracert路由追踪原理与VC6实战 简介这份Tracert程序设计报告面向计算机网络课程学习者与网络编程初学者围绕原始套接字编程与ICMP协议展开帮助读者理解路由跟踪的底层原理并完成课程设计或实验报告撰写。资源包共1个doc文档约194KB内容涵盖设计目的与要求、设计说明、系统详细设计、程序源代码及注释等完整章节结构贴近高校实验报告规范。文档重点讲解Tracert如何借助TTL字段逐跳探测中间路由器记录各跳IP地址与到达时延并涉及Ping连通性检测、WinSock2环境初始化、gethostbyname域名解析及itracert.h自定义函数等实现细节同时给出程序流程图与主要函数说明。已有168人学习适合需要参考报告框架、梳理ICMP报文机制或调试VC6.0网络程序的读者借鉴。1. 从一条 Tracert 命令说起ICMP 原始套接字到底怎么把路由跳数挖出来在 Windows 命令行敲下tracert www.example.com屏幕上会一行行蹦出每一跳的 IP 和三次延迟。很多人用了好几年却从没想过这背后到底发生了什么——为什么它不用 TCP不用 UDP 端口探测偏偏靠 ICMP为什么每一跳恰好发三个包为什么有的跳显示* * *如果你正在做「Tracert 程序设计报告」这类课程设计或工程作业光会敲命令远远不够你得自己用原始套接字把 ICMP 报文拼出来、发出去、收回来再解析 TTL 超时报文里的源地址。这篇笔记就按一线实现的顺序把 ICMP 原始套接字、TTL 递增、超时报文解析、VC6.0 环境下的 Winsock 编程这几件事讲透让你能真正跑出一个自己的 tracert 程序而不是抄一份报告交差。适合有 C 语言基础、正在学网络编程、需要交程序设计报告或想搞懂 ICMP 协议的人。2. 先搞懂 Tracert 的底层逻辑TTL 递增与 ICMP 超时2.1 为什么是 ICMP而不是 TCP/UDPTracert 的核心机制其实非常朴素利用 IP 头里的 TTLTime To Live字段。每经过一个路由器TTL 减 1减到 0 时路由器丢弃该包并向源地址回一个 ICMP Time Exceeded类型 11代码 0报文。Tracert 就是靠这个「报错」来发现路径上的每一跳。那为什么不用 TCP 或 UDP 来探测因为你要的是「路由器主动告诉你我是谁」而只有 IP 层协议才会触发 TTL 超时回包。ICMP 本身就是 IP 层的控制协议天然适合做这件事。用 UDP 发高端口也能触发 ICMP Port Unreachable但那只在到达目的主机时才回中间路由器不会理你。所以做 TracertICMP 是正路。这里有个容易混淆的点Tracert 发出的探测包本身是 ICMP Echo Request类型 8和 ping 用的是同一种报文。区别在于 Tracert 会故意把 TTL 设成 1、2、3……逐跳递增而 ping 用默认 TTL通常 128 或 64。所以你可以理解为Tracert 带不同 TTL 的一串 ping 解析中间路由器回的 Time Exceeded。2.2 原始套接字为什么必须用管理员权限要自己构造 ICMP 报文普通 Socket 不够用必须用原始套接字SOCK_RAW。原始套接字让你能直接操作 IP 头以下的字节自己填 ICMP 类型、代码、校验和。代价是操作系统不再帮你封装校验和要自己算而且 Windows 下创建原始套接字需要管理员权限否则WSASocket或socket会直接返回INVALID_SOCKET错误码WSAEACCES10013。我见过太多人程序跑不起来第一反应是代码写错了其实只是没以管理员身份运行。这个坑后面避坑章节还会细说。2.3 一次完整探测的报文流转把一次探测拆开看流程是这样的构造 ICMP Echo Request类型 8代码 0标识符填进程 ID序列号填当前跳数。计算 ICMP 校验和覆盖整个 ICMP 报文。设置 IP 头的 TTL 为当前跳数第 1 跳 TTL1第 2 跳 TTL2……。发送到目标地址。用recvfrom接收回包解析 IP 头看协议字段是不是 1ICMP。如果是 ICMP 类型 11超时取出源 IP这就是当前跳的路由器地址。如果是 ICMP 类型 0Echo Reply说明到达目标结束。如果超时没收到打印*。这个流程里第 3 步设置 TTL 是关键。Windows 下用setsockopt的IP_TTL选项就能改不需要自己拼 IP 头。但如果你要完全控制也可以开IP_HDRINCL自己填 IP 头那样更接近协议本质但复杂度高不少。课程设计里我一般建议先用IP_TTL把 ICMP 部分做扎实。提示TTL 最大值 255实际公网路径很少超过 30 跳循环上限设 30 或 64 都行。3. 用 VC6.0 Winsock 写出可运行的 Tracert 骨架3.1 环境准备与工程配置VC6.0 虽然老但很多学校的网络编程课还在用。新建一个 Win32 Console Application空工程添加一个.cpp文件。关键配置在Project Settings里Link选项卡的Object/library modules加上ws2_32.lib。不加这个链接时会报unresolved external symbol一堆WSAStartup、socket找不到。代码开头要#include winsock2.h和#include ws2tcpip.h并且注意如果同时包含windows.h要把winsock2.h放在前面否则会有一堆重定义错误。这是 VC6.0 时代的经典血泪经验。3.2 初始化 Winsock 与创建原始套接字#include winsock2.h #include ws2tcpip.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main(int argc, char* argv[]) { WSADATA wsaData; // 初始化 Winsock 2.2 if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return 1; } // 创建原始套接字协议指定 IPPROTO_ICMP SOCKET sock socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return 1; } // 设置接收超时避免 recvfrom 永久阻塞 int timeout 3000; // 3 秒 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)timeout, sizeof(timeout)); // ... 后续发送与接收逻辑 closesocket(sock); WSACleanup(); return 0; }这段代码做了三件事初始化 Winsock、创建原始套接字、设置接收超时。socket的第三个参数必须是IPPROTO_ICMP不能写 0否则创建出来的可能是别的协议套接字。SO_RCVTIMEO设 3 秒是因为网络不通时recvfrom会一直等程序看起来像卡死。超时后recvfrom返回SOCKET_ERROR错误码WSAETIMEDOUT这时打印*即可。参数说明timeout单位是毫秒3000 对应 3 秒。实际公网探测建议 2000 到 5000 之间局域网可以设 1000。设太短会误判丢包设太长程序响应慢。3.3 构造 ICMP Echo Request 与校验和计算// ICMP 报文结构简化版 struct ICMPHeader { BYTE type; // 类型8 请求0 应答11 超时 BYTE code; // 代码0 USHORT checksum; // 校验和 USHORT id; // 标识符 USHORT seq; // 序列号 }; // 计算校验和16 位反码求和 USHORT checksum(USHORT* buffer, int size) { unsigned long cksum 0; while (size 1) { cksum *buffer; size - sizeof(USHORT); } if (size) { cksum *(UCHAR*)buffer; } cksum (cksum 16) (cksum 0xffff); cksum (cksum 16); return (USHORT)(~cksum); } // 构造并发送一个 ICMP Echo Request void sendEcho(SOCKET sock, sockaddr_in* dest, int ttl, int seq) { char packet[sizeof(ICMPHeader) 32]; memset(packet, 0, sizeof(packet)); ICMPHeader* icmp (ICMPHeader*)packet; icmp-type 8; // Echo Request icmp-code 0; icmp-id (USHORT)GetCurrentProcessId(); // 用进程 ID 做标识 icmp-seq (USHORT)seq; icmp-checksum 0; icmp-checksum checksum((USHORT*)packet, sizeof(packet)); // 设置 TTL setsockopt(sock, IPPROTO_IP, IP_TTL, (char*)ttl, sizeof(ttl)); sendto(sock, packet, sizeof(packet), 0, (sockaddr*)dest, sizeof(*dest)); }校验和函数是 ICMP 编程里最容易写错的地方。原理是把报文按 16 位分组求和进位回卷最后取反。注意计算前checksum字段必须清零否则算出来的校验和是错的对方收到会直接丢弃你永远收不到回包。id用进程 ID 是为了区分自己发的包和其他进程的 ICMP 包seq用跳数方便匹配。IP_TTL选项每次发送前都要重设因为它是套接字级别的属性设一次会一直生效。如果你在第 1 跳设了 TTL1第 2 跳忘了改那第 2 跳的包也会在第一个路由器超时结果就是每一跳都显示同一个 IP。3.4 接收并解析 ICMP 超时报文// 接收回包并解析 void recvReply(SOCKET sock, int seq) { char buf[1024]; sockaddr_in from; int fromLen sizeof(from); int ret recvfrom(sock, buf, sizeof(buf), 0, (sockaddr*)from, fromLen); if (ret SOCKET_ERROR) { printf( *); return; } // 跳过 IP 头原始套接字收到的包含 IP 头 int ipHeaderLen (buf[0] 0x0f) * 4; ICMPHeader* icmp (ICMPHeader*)(buf ipHeaderLen); if (icmp-type 11) { // Time Exceeded中间路由器 printf( %s, inet_ntoa(from.sin_addr)); } else if (icmp-type 0) { // Echo Reply到达目标 printf( %s (目标到达), inet_ntoa(from.sin_addr)); } else { printf( 未知 ICMP 类型 %d, icmp-type); } }这里有个关键细节原始套接字收到的数据是包含 IP 头的所以要先算 IP 头长度再定位 ICMP 头。IP 头长度在第一个字节的低 4 位乘以 4 就是字节数通常是 20。跳过之后才是 ICMP 报文。from地址就是回包的路由器或目标主机地址直接inet_ntoa转成点分十进制。注意icmp-type 11是超时type 0是目标应答。有些系统回的是type 3Destination Unreachable那说明目标不可达也要处理。课程设计里至少把 0、11、3 三种处理掉报告才完整。4. 把跳数循环、超时重试和输出格式串起来4.1 主循环TTL 从 1 到 30// 主探测循环 for (int ttl 1; ttl 30; ttl) { printf(%2d, ttl); for (int probe 0; probe 3; probe) { sendEcho(sock, dest, ttl, ttl); recvReply(sock, ttl); } printf(\n); // 如果已经到达目标提前结束 if (reached) break; }外层循环控制 TTL内层循环发 3 个包对应 tracert 默认的「三次探测」。每次探测独立发送、独立接收这样能看出丢包情况。reached标志在收到type 0时置位跳出循环。为什么是 3 次这是 tracert 的默认行为给每个跳提供三个延迟样本方便判断网络稳定性。你也可以改成 1 次或 5 次但报告里最好说明为什么选 3。4.2 延迟计算与输出对齐要算 RTT得在发送前GetTickCount()记时间收到回包后再取一次差值就是毫秒延迟。输出时用printf(%4d ms, rtt)对齐看起来才像正经工具。DWORD start GetTickCount(); sendEcho(sock, dest, ttl, ttl); // ... recvfrom 之后 DWORD rtt GetTickCount() - start; printf( %4d ms, rtt);GetTickCount精度约 15.6 毫秒对 tracert 够用。要更高精度可以用QueryPerformanceCounter但课程设计没必要。4.3 域名解析与参数处理用户输入可能是域名得先用getaddrinfo或gethostbyname转成 IP。VC6.0 时代gethostbyname更常见hostent* host gethostbyname(argv[1]); if (host NULL) { printf(无法解析域名\n); return 1; } dest.sin_family AF_INET; dest.sin_addr *(in_addr*)host-h_addr_list[0];gethostbyname返回的h_addr_list是地址列表取第一个即可。注意它是阻塞的DNS 慢的时候程序会等可以加个提示。5. 避坑与排查Tracert 程序跑不通的 5 个真实原因5.1 现象socket 返回 INVALID_SOCKET错误码 10013原因没有以管理员权限运行。Windows 从 Vista 开始创建原始套接字需要提升权限。解决右键 VC6.0 或编译出的 exe选「以管理员身份运行」。或者用管理员权限打开 cmd再从 cmd 启动程序。5.2 现象程序发出包但一个回包都收不到原因防火墙拦截了 ICMP 回包或者目标网络禁 ping。也可能是校验和算错对方直接丢弃。解决先关掉 Windows 防火墙测试用 Wireshark 抓包确认包发出去了没有、校验和字段对不对。如果校验和是 0 或明显不对检查checksum函数里有没有先清零。5.3 现象每一跳显示的都是同一个 IP原因IP_TTL没有在每次发送前重设套接字一直用第一次设的 TTL。解决把setsockopt(IP_TTL)放进sendEcho函数里每次发送都设一遍。5.4 现象recvfrom 一直阻塞程序像死机原因没有设SO_RCVTIMEO或者超时时间设得太长。解决创建套接字后立刻设SO_RCVTIMEO3 秒左右。超时后recvfrom返回SOCKET_ERROR判断错误码WSAETIMEDOUT就打印*。5.5 现象收到的 ICMP 类型是 3 而不是 11 或 0原因目标不可达中间路由器回了 Destination Unreachable。解决在解析里加上type 3的分支打印「目标不可达」并结束。不要当成未知类型忽略否则报告里会漏掉一种重要情况。6. 进阶技巧用 Wireshark 验证你的 Tracert 程序写完程序别急着交报告先用 Wireshark 抓一次包对照你的输出看。过滤条件写icmp然后运行你的程序。你应该能看到你发出的 Echo RequestTTL 依次是 1、2、3……中间路由器回的 Time Exceeded源 IP 和你程序打印的一致目标主机回的 Echo Reply类型 0如果 Wireshark 里看到的 TTL 和你设的不一样说明setsockopt没生效如果看到校验和错误说明校验和函数有问题。这一步能把「玄学」变成「实锤」。另一个技巧把每次探测的 RTT 存到数组里最后算平均延迟和丢包率输出成表格。报告里有一张这样的表比干巴巴的代码有说服力得多。跳数IP 地址探测1探测2探测3平均延迟1192.168.1.12ms1ms2ms1.7ms210.0.0.15ms4ms6ms5.0ms3****超时最后说个我自己的习惯每次调 ICMP 程序我一定先在本机ping 127.0.0.1确认协议栈正常再tracert一个局域网地址最后才上公网。这样出问题时能快速定位是代码问题还是网络问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表