拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Tracert程序设计实战:从ICMP原理到原始套接字实现

Tracert程序设计实战:从ICMP原理到原始套接字实现

简介:这是一份计算机网络课程设计Tracert程序设计报告,面向计科专业学生以及需要理解路由跟踪原理的网络初学者。报告围绕原始套接字编程、ICMP协议、TTL机制与路由跟踪算法展开,完整覆盖设计目的、系统实现、详细流程与主要函数分析,并附有可参考的程序源代码。资源包共1个doc文档,约194KB,内容组织紧凑,适合作为实验报告或课程设计参考。文档通过展示Tracert逐步探测目标路径的过程,帮助读者定位数据包传输中的故障节点,同时理解与Ping工具的异同。已有167人学习下载,适合计算机网络课程学习者用于巩固ICMP原理、熟悉Windows Socket编程,并为后续网络诊断实践提供思路。

1. Tracert程序设计报告:不是背命令,是把网络层拆开揉碎

课程设计抽到《Tracert程序设计报告.doc》这个题目,很多人的第一反应是去网上找一份现成的报告改个名。但你仔细看一眼题目就明白,Tracert 不是那种能靠背命令糊弄过去的题目。Windows 下敲一句tracert baidu.com,几秒钟就能输出一条跨省的路径列表;可要是让你亲手把这十几行结果跑出来,你必须在数据链路之上亲手构造 UDP 报文、处理 ICMP 超时差错,还得跟原始套接字的权限和校验和死磕。这个题目最大的价值,就是逼你把 IP 协议里的 TTL(生存时间)字段和 ICMP 差错报文彻底吃透。这篇笔记我按自己当年做完这个课程设计、以及后来工作中做链路质量分析时的方案来拆解:原理怎么落到代码、代码怎么变成报告、报告怎么通过答辩。

2. Tracert原理与报文选型:TTL递减的路由器间链路

2.1 一个数据包的“旅程”:TTL递减与差错报文的生成

Tracert 的原理如果只记一句话,就是“把一个数据包的 TTL 从 1 开始递增,让路径上的每一台路由器都‘拦’它一次”。IP 协议规定,每经过一台路由器,TTL 字段的数值就减 1;当 TTL 减到 0 时,路由器不再转发这个包,而是向源地址回送一个 ICMP 超时差错报文。Tracert 正是靠这个 ICMP 差错报文里的源 IP 地址,确定第一跳路由器的位置。发送 TTL=1 的包,第一台路由回差错;发送 TTL=2 的包,第二台路由回差错。依次类推,最终当包的 TTL 大于等于到达目标主机的跳数时,目标主机收到包,回送一个正常响应,Tracert 就知道“路径探测到此结束”。

这个机制里有两个关键点,很多人在写报告时容易忽略。第一,ICMP 超时差错报文的 IP 头部里,会包含被丢弃的那个原始 IP 包的部分内容。你的程序必须能从差错报文中解析出自己当初发的报文标识,才能确认这个差错不是别人家的报文串扰进来的。第二,Tracert 对每一跳都有个超时计时,通常是 3 到 5 秒。如果收不到 ICMP 差错报文,就显示一个*;连续三个*就判定这一跳不可达。

2.2 三种探测方案:ICMP Echo、UDP探测、TCP SYN 的优劣

实现 Tracert,业内最常见的方案有三类:UDP 探测、ICMP Echo 探测、TCP SYN 探测。Windows 系统自带的tracert用到的是 ICMP Echo 请求,而 Linux 系统默认的traceroute则是 UDP 探测。这背后不是习惯差异,而是对不同网络环境的适配。

方案探测报文字段最终判定条件优点缺点
ICMP EchoICMP Type 8收到 Type 0 Echo Reply解析逻辑最简单,Windows / Linux 通用路由器和防火墙对 ICMP 限制最严格,容易被限速或丢弃
UDP 探测UDP 高段端口(默认 33434+)收到 Type 3 端口不可达遇到限速 ICMP 的路径时,往往能侥幸穿透需要构造 IP 首部校验和,稍复杂
TCP SYNTCP SYN 到 80/443 端口收到 RST 或 SYN ACK能穿过部分只开放 HTTP/HTTPS 的中间设备依赖目标端口状态,路径特征不稳定

光看原理,ICMP 似乎最简单;但放到真实的公网环境里,ICMP 的差差错率极高。很多运营商路由会对 ICMP 报文的处理做优先级降级,甚至直接做限速。我在写课程设计时用的是 UDP 探测,这有两个主要原因。第一,UDP 探测最终的“目标可达”标志非常明确:目标主机会因为你发送到了一个不存在的 UDP 端口而返回“端口不可达”ICMP 报文。这算是一个极其干净的终止信号。第二,UDP 穿透中间设备的成功率,在弱网环境下明显高于 ICMP Echo。

2.3 课程设计的落地选型:Windows与Linux的原始套接字差异

决定用 UDP 探测后,下一个问题是操作系统选哪个。写课程设计报告,绝大多数同学会在 Windows 上跑 Visual C++ 或 Code::Blocks,另一部分人则选了 Linux + gcc。这两个平台的原始套接字 API 同名但行为不同。

在 Linux 上,构建 UDP 探测 Tracert 的逻辑顺序是:先用socket(AF_INET, SOCK_DGRAM, 0)创建 UDP 套接字,用setsockopt设置 IP_TTL;再用socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)创建原始套接字,用来接收所有发往本机的 ICMP 报文。Windows 上则有些区别:创建原始套接字时,需要加载 Winsock 库,且必须用SOCK_RAW和IPPROTO_IP组合进行setsockopt调用,设置 IP 头选项而不只是 TTL。为了让报告的通用性更强,我通常建议选 Linux + UDP 探测,因为 Linux 的原始套接字行为更接近教科书标准,实验过程中出错时,tcpdump工具能直接看到 ICMP 差错报文,调试成本低一半。

3. Tracert核心代码实现:校验和、原始套接字与逐跳解析

3.1 端口与校验和:构造一个不被路由器丢掉的UDP包

UDP 探测的核心,是往目标端口发一个随机的 UDP 数据报,这个数据报必须经过 IP 层封包后送入网络。按照标准 Tracert 的实现习惯,源端口用本进程的唯一标识,目标端口从 33434 开始递增,这样做的目的是让中间路由丢弃或目标主机拒绝时,能通过 ICMP 差错报文里的原始端口号来匹配到底是哪一次探测。

很多初学者以为 UDP 数据报不需要自己做校验和,这完全错误。互联网程序设计里,UDP 校验和是强制项,虽然 IPv4 的 UDP 校验和可以填 0,但一旦网络路径上有硬件设备开启校验检查,填 0 的包会被直接丢弃。更关键的是,ICMP 差错报文里包含的“原始报文”只有 8 个字节 + IP 头长度,如果你的 UDP 报文内容过多,回程的差错报文解析时就会发现“原始包被截断”,导致无法获取到完整的目的端口,程序就会卡死在最后的停止判断上。

下面是我画过最核心的一段代码,用于计算 UDP 伪头部校验和:

unsigned short checksum(void *b, int len) { unsigned short *buf = (unsigned short *)b; unsigned int sum = 0; unsigned short result; for (sum = 0; len > 1; len -= 2) { sum += *buf++; } if (len == 1) { sum += *(unsigned char *)buf; } sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); result = ~sum; return result; }

这段代码严格遵循 RFC 1071 的校验和算法。逻辑上是将整个报文按 16 bit 为单位累加,累加完的高 16 位回卷到低 16 位,最后取反。参数b是指向 UDP 伪头部结构体的指针,len是伪头加 UDP 头的总长度。伪头部的构造里有一个极易踩坑的点:伪头部不参与实际传输,它只是在计算校验和时拼接在 UDP 头部前面的临时数据,源 IP 地址、目的 IP 地址、协议号 (UDP 为 17) 以及 UDP 报文总长度都必须按网络字节序排列。很多人在这里直接把主机字节序塞进去,算出来的校验和即使看起来正确,发到公网后也会被路由器丢弃。

3.2 原始套接字接收:解析来自中间节点的ICMP Time Exceeded

UDP 数据报发出去后,核心的接收过程在原始套接字上。原始套接字能收到所有发往本机的 ICMP 报文,包括路由器返回的 Time Exceeded 和最终目标返回的 Port Unreachable。以下代码展示主循环里的一跳探测:

int ttl = 1; int max_ttl = 30; int send_sock = socket(AF_INET, SOCK_DGRAM, 0); int recv_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); struct timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; setsockopt(recv_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); for (; ttl <= max_ttl; ttl++) { // 设置发送套接字的TTL,让IP包每经过一跳就超时 setsockopt(send_sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)); // 构造UDP目的地址,目标端口从33434开始随TTL递增 dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(33434 + ttl - 1); sendto(send_sock, pkt, pkt_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); // 在recv_sock上等待ICMP差错报文 n = recvfrom(recv_sock, buf, sizeof(buf), 0, (struct sockaddr *)&from, &from_len); if (n < 0) { // 超时未收到,打印一个星号后继续 printf("%2d * * *\n", ttl); continue; } // 从返回包中解析IP头和ICMP头 ip = (struct iphdr *)buf; icmp = (struct icmphdr *)(buf + ip->ihl * 4); if (icmp->type == ICMP_TIME_EXCEEDED) { // 这里是中间路由器的地址 printf("%2d %s\n", ttl, inet_ntoa(from.sin_addr)); } else if (icmp->type == ICMP_DEST_UNREACH) { // 这里是最终目标主机的地址 printf("%2d %s (Destination reached)\n", ttl, inet_ntoa(from.sin_addr)); break; } }

这段代码的逻辑是教科书式的:每轮把 TTL 加 1,等待 3 秒。SO_RCVTIMEO参数决定了单跳等待时长,这个值调到 5 秒会更保守,但整个探测 30 跳的最坏情况会拉长到 150 秒,不利于演示。生产环境中我习惯于设 2 秒,课程设计答辩场景则建议 3 秒,平衡演示流畅度和漏包率。注意ICMP_TIME_EXCEEDED和ICMP_DEST_UNREACH这两个分支,中间路由反馈 Time Exceeded 时,源地址是路由器的地址;目标主机反馈 Port Unreachable 时,源地址才是目标主机的地址。这与 Tracert 显示路径的最后一跳是目标本身完全对应。

3.3 主循环与超时参数:TTL递增策略与超时判定

把上面的单跳逻辑放进循环,并不能直接得到标准的 Tracert 输出。第一次跑这个程序时,一个典型的玄学问题是“TTL 已经设了,但路由器回送的差错包里总是带着旧 TTL”。其实原因不在路由器,而在你自己的局部变量。setsockopt的设置是针对套接字的,但每次 UDP 发送时,操作系统会保留上一次对该套接字的 TTL 设置,所以循环里必须每轮重新调用setsockopt,否则全部探测包都会携带同一个 TTL 值,结果就是所有包都从第一跳路由返回。

还有个必须处理的细节:路由器的 ICMP 差错报文可能包含你不关心的旧包数据。在解析recvfrom返回的数据时,必须用原始 IP 包里的“标识字段”和“目的端口”来校验是自己发出的包。只判断 ICMP 类型而不校验字段,很容易在多个探测进程同时跑的时候拿到别的进程的差错包。我的习惯是发出报文前,给 IP 头里的id字段打上一个随机种子,然后解析差错包内层嵌的原始报文头部时,比对id是否一致,不一致直接丢弃。

4. 把代码变成《Tracert程序设计报告.doc》:六段式结构怎么写

4.1 摘要与需求分析:怎么把“能跑”翻译成功能需求指标

课程设计的报告框架,通常按“摘要—需求分析—概要设计—详细设计—测试调试—总结”的六段式来写。很多同学直接拿代码块填充报告,这是最亏的写法。Tracert 这个题目的需求分析,至少要覆盖两个维度的具体描述。

功能需求方面,要明确程序接受一个目标主机名或 IP,支持用户指定最大 TTL(默认 30)、指定单跳超时(默认 3 秒),并且能对每一跳输出序号、IP 地址和往返时延。非功能需求方面,则要写清楚原始套接字需要管理员权限,程序在 Windows 下需要链接ws2_32.lib,在 Linux 下需要链接libpthread,同时要求程序在超时或 Ctrl+C 中断时能释放套接字资源。这一点在答辩时常被问到,你把它写进去,报告质量立刻和普通作业拉开差距。

4.2 详细设计:时序图、数据结构和函数接口的呈现技巧

详细设计是报告里最占篇幅、也最容易被抄袭弄砸的部分。Tracert 的时序其实不复杂,但画出来很能糊弄人:发送模块(UDP 套接字)和接收模块(原始套接字)并行,主循环在发送一个 TTL 包后进入阻塞接收,接收线程解析 IP 头部和 ICMP 头部,再返回给主循环打印。

我用表格来收口“数据结构”这一小节,比贴大段代码更直观:

数据项类型对应协议字段说明
目标地址struct sockaddr_inIP 首部 Destination可通过inet_pton或getaddrinfo获取
探测端口intUDP Destination Port从 33434 开始逐跳递增
TTL 值intIP 首部 Time To Live从 1 递增到 max_ttl
报文标识unsigned shortIP 首部 Identification用于差错包与原始包匹配
ICMP 类型unsigned charICMP Type11 表示超时,3 表示目标不可达

函数接口的设计上,一定不要写成“一个大 main 一直干到底”。拆出来的send_probe()、recv_icmp()、parse_icmp()三个函数,能让报告的流程图和代码注释都更清晰。答辩时老师只要问一句“你如何扩展成支持 IPv6”,你就能顺势说出“只需要把 AF_INET 换成 AF_INET6 并调整 ICMPv6 报文类型”这种线性回答,整个报告的完成度直接上升。

4.3 测试与结果分析:用三组端点把报告撑起来

测试章节是 Tracert 报告的重头戏。为了避免老师说“你这就是网上抄的”,建议实际做三组对比实验,并把输出粘贴进报告。

第一组测试直接用本机网关,比如tracert 192.168.1.1(或你的实际网关),这能立刻验证程序最基本的单跳通信能力。第二组测试用本省的一个公共 DNS,比如114.114.114.114,通常能跑通 5 到 12 跳,覆盖了跨运营商和骨干网场景。第三组测试用异地的知名网站域名,比如www.baidu.com,验证域名解析、多跳转发、目标端口不可达的完整链路。报告里截图的三个输出,要分别说明“第一跳 MAC 地址是网关,所以延迟极低”“中间跳延迟跳变是因为 ICMP 限速”这类分析。这比空泛的“结果符合预期”有说服力得多,也能体现你真的动手跑了。

5. Tracert调试避坑:五个一踩一个准的翻车现场

5.1 现象一:发出去的包石沉大海,收不到任何ICMP响应

现象:程序运行后,每一跳都打印超时,套接字一直没有数据可读。

原因:最常见的是 root 权限缺失。原始套接字的创建在 Linux 上要求CAP_NET_RAW,普通用户直接socket()会返回EPERM。另一个原因是测试环境的防火墙把 ICMP 差错报文屏蔽了,尤其在云服务器上,安全组入方向默认禁止 ICMP。

解决:先sudo运行程序排除权限问题。再在另一台机器上启动tcpdump -i eth0 icmp抓包,看有没有路由器回传的 Time Exceeded。如果抓包能看到但程序收不到,说明本机防火墙拦截了 ICMP,用iptables -A INPUT -p icmp -j ACCEPT或firewall-cmd --add-protocol=icmp放行即可。这算是互联网程序设计环境里最常见的前置坑。

5.2 现象二:原始套接字创建失败,提示Operation not permitted

现象:在 Linux 下编译通过,但运行时提示socket: Operation not permitted,程序退出。

原因:创建原始套接字需要 root 或相应的 capability。很多同学用的 IDE 没有以提权模式运行调试会话,就会遇到这个经典报错。

解决:课程设计环境下最简单直接的解决方法是sudo ./tracert。如果要在 IDE 里调试,记得给 IDE 的启动脚本加上提权设置。更好的工程化做法是把套接字创建写在setuid外部,但这对课程设计没必要。

5.3 现象三:中间路由显示* * *,但用系统tracert能通

现象:自己的程序输出从第 3 跳开始全是星号,但 Windows 自带的tracert却能完整跑完。

原因:两个程序用的探测报文不同。Windows 默认用 ICMP Echo,你的程序多半是 UDP。某些路由器实现了 QoS 策略,对 UDP 高段端口限速,却放行 ICMP;反过来也有可能。

解决:把你的程序改成同时支持 ICMP Echo 和 UDP 两种模式。这不仅是避坑,也是报告里的加分项。实现方法是把发送套接字从SOCK_DGRAM换成SOCK_RAW+IPPROTO_ICMP,同时把 ICMP 报文头的 Identifier 和 Sequence 填好,其余逻辑完全复用。在报告中说明这个双模式设计,一句话就能讲明白兼容性问题。

5.4 现象四:TTL已经设置,但发出去的包还是TTL=0

现象:在sendto之前打印ttl变量的值是正常的 1、2、3,但抓包看到的 IP 头 TTL 始终是默认值。

原因:setsockopt的optlen传错了。很多参考代码写成sizeof(int),在 Linux 上参数类型其实是int,但一旦写成sizeof(char),内核会拒绝读取或只读取一字节,导致设置静默失败。

解决:严格检查setsockopt的调用原型。正确的写法是:

setsockopt(send_sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));

ttl定义为int。另外,Windows 下需要用setsockopt(sock, IPPROTO_IP, IP_TTL, (const char*)&ttl, sizeof(ttl)),类型转换不同,但本质相同。这是个无声的错误,强烈建议用抓包工具确认。

5.5 现象五:多网卡场景下,源地址错误导致路径“漂移”

现象:本机同时连着无线网卡和有线网卡,Tracert 结果里第一跳的延迟和路径不稳定,甚至在同一跳出现不同 IP。

原因:UDP 套接字在没有绑定本地地址时,操作系统会依照路由表选择源地址和出接口。如果目标地址是公网 IP,系统可能走无线网卡;但某些路由策略又会让应答包从有线网卡进来,两个出口的路径完全不同。

解决:创建 UDP 套接字后,主动绑定需要探测的网卡 IP:

struct sockaddr_in local; local.sin_family = AF_INET; local.sin_addr.s_addr = inet_addr("192.168.3.25"); // 指定本机网卡IP local.sin_port = htons(0); bind(send_sock, (struct sockaddr *)&local, sizeof(local));

在多网卡机器上,这是一步必不可少的工程化操作。在报告中提一句“通过 bind 绑定出口 IP”,能给老师留下实践细节充足的好印象。

6. 从C++到Go语言实现Tracert:并发探测与结果验证

6.1 并发模型:goroutine替代for循环,TTL并行递减

工作中做链路巡检时,逐跳串行探测太慢了。30 跳的最坏情况要等 3 秒每跳,总耗时接近 90 秒。Go 语言的天然并发模型很适合解决这个痛点。用 goroutine 并发发送所有 TTL 的探测包,再统一收集结果,整体耗时可以被压缩到单跳超时级别。核心代码片断如下:

for ttl := 1; ttl <= max; ttl++ { wg.Add(1) go func(ttl int) { defer wg.Done() // 设置TTL、发送UDP探测包、等待ICMP响应 // 将结果写入channel }(ttl) } wg.Wait() close(results)

并发版的注意点在于:路由器返回的 ICMP 差错报文顺序可能错乱,因此结果必须按 TTL 排序后再展示,否则后续的逐跳对比验证会被打乱。Go 语言实现 Tracert 时,golang.org/x/net/icmp包提供了跨平台的原始套接字封装,在 Windows 上也能稳定捕获 ICMP 差错报文。这比我当初用 C++ 在 Windows 上折腾 Winsock 顺畅得多。

6.2 验证你的实现与系统tracert的“像素级”一致性

写完程序,最忌讳自己觉得自己对。我验证一个 Tracert 实现是否正确的办法,是拿系统自带的tracert命令和我的程序跑同一个目标,然后把两边的输出做逐跳差异对比。

对比工具我一般用diff或简单的awk脚本,列出每一跳的 IP 是否一致、延迟是否在合理波动范围。如果发现某一跳 IP 不同,最先怀疑的是本地路由策略或 DNS 解析结果不同,而不是程序逻辑。比如tracert www.baidu.com每次解析出的 IP 可能不同,这就是 CDN 调度导致的正常现象,不能错怪代码。验证时的最佳实践是直接指定 IP 作为目标,比如tracert 180.101.50.242,跳过 DNS 解析环节,这样得到的结果才是严格可复现的。

这个方向我从课程设计一路做到生产环境,最深的教训是:所有网络诊断工具,第一版跑通都靠运气,真正可靠全靠校验和、超时和报文匹配这三件套做到严格。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表