【Linux网络#21】ICMP协议与Traceroute、Ping工具原理解析
本文深入剖析网络诊断的核心——ICMP协议,从报文结构、作用到IPv4/IPv6差异,为你构建清晰的知识框架。文章重点拆解Traceroute与Ping两大经典工具的工作原理,揭示其背后依赖的TTL机制与Echo请求/应答模型。同时,详解RFC 1071校验和算法及关键C语言实现,并介绍getaddrinfo等实用函数。最后通过代码模块,手把手教你实现ping与traceroute功能,助你从理论

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
1. ICMP 报文
ICMP(Internet Control Message Protocol,互联网控制消息协议)是 TCP/IP 协议族中的核心网络层协议之一,定义在 RFC 792(IPv4)和 RFC 4443(IPv6,称为 ICMPv6)中。它不用于传输用户数据,而是为 IP 协议提供差错报告、网络诊断和控制功能,是保障网络健壮性的“幕后英雄”。
| 维度 | 核心要点 |
|---|---|
| 定位 | IP 的差错与控制协议,网络层“反馈系统” |
| 结构 | Type + Code + Checksum + (原始 IP 包片段) |
| 类型 | 差错报告(3, 11, 12) + 查询(0, 8) |
| 作用 | ping/tracert、差错通知、PMTUD |
| 安全 | 需防火墙管控,防 Flood 和隧道 |
| 演进 | IPv6 中 ICMPv6 成为关键基础设施 |
1.1 ICMP 概述
ICMP 的定位:IP 的“助手”协议
- 层级:位于网络层(与 IP 同层),是 IP 协议的补充。
- 封装:ICMP 报文作为 IP 数据报的数据部分传输,IP 头部的
Protocol字段值为1(表示 ICMP)。 - 无端口:ICMP 不使用传输层端口号,通信基于 IP 地址。
- 不可靠:ICMP 本身不保证可靠传输(它自己也可能被丢弃),依赖上层协议(如 ping 程序)实现重传。
- 理解:“IP 负责发,ICMP 负责说” —— 当 IP 无法完成投递时,ICMP 会告诉你“为什么”。
✅ 类比: 如果 IP 是“邮递员”,ICMP 就是“邮局反馈系统”——当信件无法投递时,邮局会寄回一张“退信通知单”(ICMP 差错报文)。
ICMP 报文通用结构
所有 ICMP 报文都包含以下头部(至少前 4 字节):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Rest of Header (可变) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data (可变) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 长度 | 说明 |
|---|---|---|
| Type | 8 位 | 报文类型(如 0=Echo Reply, 8=Echo Request, 11=Time Exceeded) |
| Code | 8 位 | 类型的细化(如 Type=3 时,Code=0 表示网络不可达,Code=1 表示主机不可达) |
| Checksum | 16 位 | 整个 ICMP 报文(含头部和数据)的 RFC 1071 校验和 |
| Rest of Header | 可变 | 类型相关字段(如 Echo 的 Identifier + Sequence Number) |
| Data | 可变 | 通常包含触发该 ICMP 报文的原始 IP 数据报的前 8 字节(用于匹配请求) |
🔑 关键设计: Data 字段包含原始 IP 包的头部和部分数据,使接收方能关联到具体出错的数据流。
1.2 ICMP 报文主要类型(Type)
ICMP 报文分为两大类:差错报告报文 和 查询报文。
2.2.1 差错报告报文(Error Reporting) —— 被动触发
| Type | Code | 名称 | 触发条件 |
|---|---|---|---|
| 3 | 0–15 | Destination Unreachable(目标不可达) | 路由器/主机无法投递数据包 (如网络不可达、端口不可达、协议不可达) |
| 11 | 0 | Time Exceeded(超时) | TTL=0(用于 tracert)或分片重组超时 |
| 12 | 0–1 | Parameter Problem(参数问题) | IP 头部字段错误 |
| 5 | 0–3 | Redirect(重定向) | 路由器通知主机“有更优路径”(现代网络已禁用) |
⚠️ 重要限制(防止风暴):
- ICMP 差错报文不会对 ICMP 差错报文再生成差错(避免无限循环)
- 不会对广播/多播包生成差错
2.2.2 查询报文(Query) —— 主动请求/响应
| Type | 名称 | 用途 |
|---|---|---|
| 8 | Echo Request | ping 请求 |
| 0 | Echo Reply | ping 响应 |
| 13 | Timestamp Request | 请求时间戳(已废弃) |
| 14 | Timestamp Reply | 时间戳响应 |
| 15/16 | Information Request/Reply | 已废弃 |
✅
ping工作流程:
- 主机 A 发送 Type=8, Code=0(Echo Request)
- 主机 B 收到后回复 Type=0, Code=0(Echo Reply)
- A 通过 Identifier + Sequence Number 匹配请求/响应
1.3 ICMP 作用
-
网络诊断
-
ping:测试连通性与延迟(Echo Request/Reply) -
tracert/traceroute:路径追踪(利用 TTL 超时 + Time Exceeded)
-
-
差错通知
- 当数据包无法到达目标时,中间路由器或目标主机返回 ICMP 差错,帮助源主机调整行为。
- 例:目标端口无服务 → 返回 Type=3, Code=3(Port Unreachable) → UDP 应用得知“对方没开此端口”
- 当数据包无法到达目标时,中间路由器或目标主机返回 ICMP 差错,帮助源主机调整行为。
-
路径优化(历史)
- ICMP Redirect 曾用于动态路由,但因安全风险(可被用于中间人攻击)现代系统默认禁用。
-
MTU 发现(PMTUD)
- 主机发送大包,若中间链路 MTU 小,路由器返回 Type=3, Code=4(Fragmentation Needed),通知源主机降低 MTU。
1.4 ICMP 与安全
🔒 风险
- ICMP Flood:攻击者发送大量 Echo Request 耗尽目标资源(拒绝服务)
- ICMP Tunneling:通过 ICMP Data 字段隐蔽传输数据(绕过防火墙)
- 网络探测:攻击者用
ping/tracert扫描存活主机和网络拓扑
🛡 防护
- 防火墙策略:限制 ICMP 类型(如只允许 Echo Reply,禁止 Redirect)
- 速率限制:对 ICMP 报文做 QoS 限速
- 禁用不必要的 ICMP:如 Linux 可通过
net.ipv4.icmp_echo_ignore_all=1忽略 ping
💡 最佳实践:
允许入站 Echo Request(用于运维诊断),但限制速率;禁止出站 Redirect。
1.5 IPv4 vs IPv6 中的 ICMP
| 特性 | ICMPv4 | ICMPv6 |
|---|---|---|
| 协议号 | IP Protocol = 1 | Next Header = 58 |
| 邻居发现 | 依赖 ARP | 用 ICMPv6 替代 ARP(Type=135/136: Neighbor Solicitation/Advertisement) |
| 组播管理 | IGMP | 用 ICMPv6 替代 IGMP(Type=130–132: MLD) |
| 必须启用 | 可选(如 UDP 校验和可关闭) | 强制启用(如 NDP、PMTUD 依赖 ICMPv6) |
✅ ICMPv6 更重要:在 IPv6 中,ICMP 不再是“辅助协议”,而是核心组成部分。
2. Tracert
2.1 什么是 Tracert
Tracert(Trace Route)是一个网络诊断工具,用于追踪数据包从源主机到目标主机所经过的路由路径(即中间经过哪些路由器)。
- Windows 系统命令:
tracert - Linux/Unix/macOS 命令:
traceroute(或tracepath)
核心机制——利用 IP 报文的 TTL(Time To Live)字段触发沿途路由器返回 ICMP 超时报文,从而逐跳探测路径
探测包的类型(虽然原理相同,但不同系统实现差异)
| 系统 | 默认探测包类型 | 说明 |
|---|---|---|
Windows tracert |
ICMP Echo Request | 与 ping 相同,目标主机需响应 ICMP |
Linux traceroute |
UDP 数据包(到高端口,如 33434–33534) | 目标主机因端口不可达返回 ICMP Port Unreachable,标志到达终点 |
| 新版本 traceroute | 支持 ICMP、TCP、UDP 多种模式 | 更灵活,可绕过防火墙限制 |
2.2 核心原理:TTL + ICMP 差错机制
1. TTL(Time To Live)的作用
- TTL 是 IP 头部的一个 8 位字段(最大值 255),每经过一个路由器,TTL 减 1。
- 当 TTL 减为 0 时,路由器丢弃该数据包,并向源主机发送 ICMP “Time Exceeded”(超时)差错报文。
- 这一机制本用于防止数据包在网络中无限循环。
2. Tracert 如何利用 TTL?
通过递增 TTL 值,利用路由器在 TTL=0 时返回 ICMP 超时消息的特性,逐跳探测路径,最终由目标主机返回 ICMP 应答结束过程。
Tracert 主动控制 TTL 值,从 1 开始逐次递增,实现逐跳探测:
| 步骤 | 操作 | 结果 |
|---|---|---|
| 第 1 跳 | 发送 TTL=1 的探测包 | 第 1 个路由器 TTL→0,返回 ICMP 超时,暴露其 IP |
| 第 2 跳 | 发送 TTL=2 的探测包 | 第 2 个路由器 TTL→0,返回 ICMP 超时,暴露其 IP |
| … | … | … |
| 第 N 跳 | 发送 TTL=N 的探测包 | 到达目标主机,目标主机返回 ICMP Echo Reply(回显应答),探测结束 |
✅ 关键点:
- 每次探测都让 TTL 恰好在目标跳耗尽,从而“诱使”该跳返回 ICMP 消息。
- 目标主机收到 TTL>0 的回显请求时,不会返回超时,而是正常回复 ICMP Echo Reply,标志路径终点。
2.3 Tracert 作用
-
路径可视化:显示从本地到目标主机经过的所有中间路由器(IP 地址,有时可解析为域名)。
-
网络故障定位
- 若某跳长时间无响应(* * *),说明该路由器可能:
- 丢弃了探测包(安全策略)
- 网络拥塞或宕机
- 若延迟突然增大,可定位瓶颈节点。
- 若某跳长时间无响应(* * *),说明该路由器可能:
-
验证路由策略:检查数据是否按预期路径传输(如 BGP 路由、CDN 调度)。
-
辅助安全分析:了解目标主机的网络拓扑(但常被防火墙限制)。
2.4 重要细节补充
为什么每跳发多个包(通常是 3 个)?
- 减少偶然丢包导致的误判。
- 可计算平均延迟,反映链路质量。
为什么有些跳显示 * * *(请求超时)?
- 路由器配置为不发送 ICMP 超时报文(出于安全或性能考虑)。
- 防火墙过滤了 ICMP/UDP 探测包。
- 不代表链路中断!后续跳若能收到响应,说明路径是通的。
✅ 如何知道已到达目标主机?
- Windows:收到 ICMP Echo Reply(说明目标响应了 ping)。
- Linux (UDP 模式):收到 ICMP Port Unreachable(说明包已到达目标,但端口无服务)。
局限性
| 限制 | 说明 |
|---|---|
| 非对称路由 | 去程与回程路径可能不同,Tracert 只显示去程 |
| ICMP 被过滤 | 若中间路由器或目标主机屏蔽 ICMP/UDP 探测,结果不完整 |
| NAT/防火墙干扰 | 企业网络常阻断探测包 |
| 不反映真实应用路径 | 应用流量(如 TCP 443)可能走不同路由 |
2.5 windows vs linux
演示一:Linux 下演示
traceroute www.baidu.com

演示二:windows 下
tracert www.baidu.com

可以看到:这里 windows 只能 Tracert 起点和终点,路径上都超时了
核心原因:Windows tracert 使用 ICMP,而大多数中间路由器不响应 ICMP Time Exceeded
📌 Windows tracert 的工作原理:
- 发送一个 ICMP Echo Request 包(类似 ping),TTL = 1。
- 第一跳路由器收到后,TTL 减为 0 → 返回 ICMP Time Exceeded (Type 11, Code 0)。
tracert收到这个响应,就知道第一跳是谁。- 接着发送 TTL=2 的包,得到第二跳地址……以此类推,直到目标主机返回 ICMP Echo Reply。
⚠️ 问题就出在第 2 步:很多路由器/防火墙/云网关会静默丢弃或不响应 ICMP Time Exceeded 报文,尤其是出于安全考虑(防止网络探测、减少攻击面)。
🌐 为什么中间跳都“请求超时”?
| 原因 | 说明 |
|---|---|
| 安全策略屏蔽 ICMP | 企业网关、ISP 边界、云服务商(如阿里云、腾讯云、AWS)常默认关闭对 ICMP Time Exceeded 的响应。这是标准安全实践。 |
| 防火墙规则 | 中间节点可能只允许 ICMP Echo Request/Reply(ping),但拒绝其他类型(如 Type 11)。 |
| 性能优化 | 大量 TTL 超时报文会消耗路由器 CPU,所以很多设备直接丢弃而不回复。 |
| 虚拟化环境限制 | 在云主机中,虚拟网关(VPC 网关、NAT 网关)通常不会暴露内部拓扑,也不会响应 traceroute。 |
🔍 对比 Linux traceroute
Linux 的 traceroute 默认使用 UDP(从端口 33434 开始递增),而不是 ICMP:
- 它发送 UDP 数据报到目标主机的一个“不存在”的高端口。
- 当 TTL 超时,路由器返回 ICMP Time Exceeded。
- 当到达目标主机时,目标主机返回 ICMP Port Unreachable(因为端口没开)→ 这样就完成了追踪。
✅ 很多网络设备对 UDP 的处理更宽松,或者至少对 “UDP 到高端口” 的 TTL 超时响应更积极 —— 所以 Linux 的
traceroute经常能“看到”更多跳数。
💡 为什么 Windows 只看到第 1 跳和最后 1 跳?
- 第 1 跳:通常是你的本地网关(如 10.152.0.1),它在局域网内,一般会响应 ICMP。
- 中间跳:跨越公网、经过多个 ISP 或云厂商边界,这些节点大多屏蔽 ICMP TTL 超时响应 → 显示
* * *(请求超时)。 - 最后一跳:目标主机(如
111.45.11.5)收到了你的 ICMP Echo Request,并返回了 ICMP Echo Reply → 成功显示。
✅ 附加知识: 在 Windows 上看到的“请求超时”,其实是 tracert 在等待 ICMP Time Exceeded 报文,等不到就显示超时。它并不表示“网络不通”,只是“中间节点不想告诉你它在哪里”。
3. ping
ping 是最基础、最常用的网络诊断工具之一,用于测试主机之间的连通性、测量网络延迟(RTT)。其核心机制基于 ICMP(Internet Control Message Protocol) 协议,由 Mike Muuss 于 1983 年首次实现,名字灵感来自声呐(sonar)的“ping”声。
3.1 核心原理:ICMP Echo 请求/应答
ping 的本质是:向目标主机发送 ICMP Echo Request 报文,并等待其回复 ICMP Echo Reply 报文。
- 发送方:源主机(运行
ping命令的机器) - 接收方:目标主机(被 ping 的机器)
- 协议:ICMP(IP 协议号 = 1)
- 通信模式:无连接、无端口、基于 IP 地址
✅ 关键点:
ping不使用 TCP/UDP(无端口)- 依赖目标主机主动响应 ICMP Echo Request
3.2 ICMP Echo 报文结构
ping 使用两种 ICMP 报文:
-
ICMP Echo Request(Type=8, Code=0)
-
ICMP Echo Reply(Type=0, Code=0)
通用格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier | Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 作用 |
|---|---|
| Type/Code | 8/0 表示请求,0/0 表示应答 |
| Checksum | 整个 ICMP 报文的 RFC 1071 校验和 |
| Identifier | 通常设为进程 ID,用于区分不同 ping 进程 |
| Sequence Number | 序列号(1, 2, 3…),用于匹配请求/应答 |
| Data | 可选填充数据(常用于计算 RTT 的时间戳) |
💡 匹配机制: 接收方通过 Identifier + Sequence Number 确认该回复对应哪个请求。
3.3 工作流程
这里以 ping www.example.com 为例
-
域名解析
- 将
www.example.com解析为 IPv4 地址(如93.184.216.34)
- 将
-
创建原始套接字(Raw Socket)
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);⚠️ 需要 root 权限(Linux/Unix)或管理员权限(Windows)
-
构造 ICMP Echo Request
- Type=8, Code=0
- Identifier = getpid()
- Sequence = 1, 2, 3…
- Data 可包含发送时间戳(用于精确 RTT)
- 计算校验和
-
发送请求
- 通过
sendto()发送 ICMP 报文 - IP 层封装:目标 IP = 解析出的地址,Protocol = 1
- 通过
-
等待应答
- 使用
recvfrom()或select()等待 ICMP Echo Reply - 超时通常为 1–3 秒(如未收到,显示 “Request timed out”)
- 使用
-
处理应答
- 验证 Identifier 和 Sequence Number
- 计算 RTT(Round-Trip Time)
- 打印结果:
Reply from <IP>: bytes=... time=... TTL=...
-
重复
- 默认发送 4 次(Windows)或持续发送(Linux,需 Ctrl+C 停止)
ping 的输出解析(Windows 风格)
Pinging www.example.com [93.184.216.34] with 32 bytes of data:
Reply from 93.184.216.34: bytes=32 time=45ms TTL=56
Request timed out.
...
Ping statistics for 93.184.216.34:
Packets: Sent = 4, Received = 3, Lost = 1 (25% loss)
Approximate round trip times in milli-seconds:
Minimum = 45ms, Maximum = 47ms, Average = 46ms
| 字段 | 含义 |
|---|---|
| bytes=32 | ICMP 数据部分大小(不含 IP/ICMP 头) |
| time=45ms | 往返延迟(RTT) |
| TTL=56 | 目标主机回复时 IP 包的 TTL 值(初始 TTL - 路由跳数) |
| Request timed out | 超时未收到回复(可能:防火墙屏蔽、主机宕机、网络丢包) |
3.4 实现细节
-
权限要求
- 必须使用 原始套接字(SOCK_RAW) → 需要特权(
sudo)
- 必须使用 原始套接字(SOCK_RAW) → 需要特权(
-
校验和计算
- 必须正确实现 RFC 1071 校验和,否则目标主机会丢弃报文
-
超时控制
- 使用
select()或poll()避免recvfrom()永久阻塞
- 使用
-
RTT 精确测量
-
在 Data 字段嵌入高精度时间戳(
struct timeval) -
接收时取出时间戳,计算差值
-
-
TTL(Time To Live)
-
ping显示的 TTL 是目标主机回复时 IP 包的 TTL 值 -
初始 TTL 通常为 64(Linux)、128(Windows)、255(路由器)
-
每经过一跳减 1,可用于粗略判断目标距离
-
3.5 ping 的局限性与注意事项
| 问题 | 说明 |
|---|---|
| 防火墙拦截 | 目标主机或中间设备可能丢弃 ICMP Echo Request → ping 失败,但实际服务(如 HTTP)可用 |
| 不反映应用层状态 | ping 通 ≠ 服务可用(如 Web 服务崩溃但 ICMP 仍响应) |
| 无法穿越某些 NAT | 部分 NAT 设备不转发 ICMP |
| ICMP 速率限制 | 主机可能限制 ICMP 回复频率,导致高延迟或丢包 |
| IPv6 支持 | 现代 ping 工具通常自动尝试 IPv6(ping6 或 ping -6) |
✅ 最佳实践:
ping用于初步连通性测试- 服务可用性需用
telnet、curl、nc等应用层工具验证
扩展:ping 与其他工具的关系
| 工具 | 协议 | 用途 |
|---|---|---|
ping |
ICMP | 测试 IP 层连通性 |
traceroute |
ICMP/UDP/TCP | 路径追踪 |
telnet <ip> <port> |
TCP | 测试端口是否开放 |
curl http://... |
HTTP | 测试 Web 服务 |
3.6 小结
ping 机制的核心是:“发一个 ICMP Echo Request,等一个 ICMP Echo Reply,算一次 RTT”
它简单、高效、跨平台,是网络运维的“第一把尺子”。尽管有局限性,但其直观性、标准化和广泛支持使其成为不可替代的基础工具。
💡 记住:
ping成功 → 网络层可达ping失败 → 不一定网络不通(可能 ICMP 被屏蔽)- 用
ping看延迟,用telnet/curl看服务
4. 谈谈RFC 1071标准
RFC 1071,全称为 “Computing the Internet Checksum”(计算 Internet 校验和),由 David C. Plummer 于 1988 年 9 月发布,是 TCP/IP 协议族中关于校验和(Checksum)计算方法的权威标准文档。
- 它定义了 TCP/IP 协议族中广泛使用的校验和(Checksum)算法,用于检测 IP、ICMP、TCP、UDP 等协议头部和数据在传输过程中是否发生错误
4.1 为什么需要校验和?
网络传输可能因噪声、硬件故障、软件 bug 等导致比特错误(如 0 变 1)。
校验和是一种轻量级错误检测机制,用于:
- 检测数据在传输或处理过程中是否被篡改或损坏
- 若校验失败,接收方直接丢弃该数据包(由上层协议重传)
✅ 注意:校验和不提供加密或认证,仅用于错误检测(Error Detection),非错误纠正(Error Correction)。
4.2 RFC 1071 校验和的核心原理
📌 基本思想:16 位反码和(One’s Complement Sum)
- 将待校验的数据视为一系列 16 位(2 字节)无符号整数。
- 对所有 16 位字进行反码加法(One’s Complement Addition):
- 普通加法
- 若产生进位(carry),则将进位加回低位(称为 “end-around carry”)
- 最终结果取反码(按位取反),即为校验和。
🔑 关键特性:
- 发送方:计算校验和并填入协议头
- 接收方:对整个数据(含校验和字段) 重新计算校验和
- 若结果为 0xFFFF(反码和为 0),则认为无错误
4.3 算法步骤(以发送方为例)
假设待校验数据为字节数组 buf,长度为 len 字节。
步骤 1:补齐字节
- 若
len为奇数,在末尾补一个字节 0(仅用于计算,不实际发送)
步骤 2:16 位反码累加
-
将数据按 大端序(Big-Endian) 解释为一系列 16 位无符号整数
W[0], W[1], ..., W[k-1]。 -
计算反码和:
unsigned long sum = 0; for (int i = 0; i < len; i += 2) { // 将两个字节组合为 16 位整数(大端序) unsigned short word = (buf[i] << 8) + (i+1 < len ? buf[i+1] : 0); sum += word; }
步骤 3:处理进位(end-around carry)
- 反码加法规则:普通加法若产生进位(carry out),则将进位加回结果的最低位(称为 “end-around carry”)。
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
步骤 4:取反码
unsigned short checksum = ~sum;
✅ 此
checksum填入协议头的校验和字段。
步骤 5:接收方验证
接收方将整个数据包(包括校验和字段) 按同样方式计算反码和:
- 若结果为
0xFFFF(即反码和为 0),则校验通过 - 否则,数据损坏,丢弃
💡 为什么包含校验和字段?(即为什么结果为
0xFFFF(即反码和为 0),则校验通过)
因为发送方填入的是~sum,接收方计算sum + (~sum) = 0xFFFF(在反码系统中等于 -0(-0表示为0xFFFF),视为 0)。
4.4 RFC 1071 的关键设计特点
| 特性 | 说明 |
|---|---|
| 高效 | 仅需加法和移位,适合硬件/软件实现 |
| 可增量更新 | 修改 IP 头(如 TTL)时,可快速更新校验和,无需重算全部 |
| 字节序无关(在特定条件下) | RFC 提供了处理大小端的建议,但实际实现需注意 |
| 支持任意长度数据 | 通过末尾补 0 处理奇数字节 |
增量更新示例(IP TTL 减 1):
- 原 TTL 字段:
old_val - 新 TTL 字段:
new_val = old_val - 1 - 新校验和 =
old_checksum + (~old_val) + new_val(再处理进位和取反)
4.5 经典 C 实现(RFC 1071 推荐)
unsigned short checksum(unsigned short *buf, int len) {
unsigned long sum = 0;
// 累加 16 位字
while (len > 1) {
sum += *buf++;
len -= 2;
}
// 处理奇数字节
if (len == 1) {
sum += *(unsigned char*)buf;
}
// 处理进位
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
// 取反码
return (unsigned short)(~sum);
}
⚠️ 注意:此实现假设输入是 16 位对齐 且 大端序(网络序)。实际使用前需将数据转为网络字节序(
htons)。
5. 相关函数
5.1 getaddrinfo
- POSIX 标准中用于网络地址解析的核心函数,它优雅地替代了已废弃的
gethostbyname()。它将人类可读的主机名和服务名转换为可用于socket()通信的地址结构。
extern int getaddrinfo (
const char *__restrict __name, // 主机名或IP地址字符串
const char *__restrict __service, // 服务名或端口号字符串
const struct addrinfo *__restrict __req, // hints - 过滤条件
struct addrinfo **__restrict __pai // result - 结果链表
);
参数
-
__name(主机名)-
"www.example.com":域名 -
"192.168.1.1":IPv4地址字符串 -
"::1":IPv6地址字符串 -
NULL:返回本机地址(localhost)
-
-
__service(服务/端口)-
"http":标准服务名(查/etc/services) -
"80":直接指定端口号字符串 -
NULL:不指定服务
-
-
__req(hints - 过滤条件),关键字段如下:-
ai_family:AF_INET(IPv4),AF_INET6(IPv6),AF_UNSPEC(不限) -
ai_socktype:SOCK_STREAM(TCP),SOCK_DGRAM(UDP) -
ai_protocol:IPPROTO_TCP,IPPROTO_UDP或 0 -
ai_flags: 常用AI_PASSIVE(用于绑定监听)
-
-
__pai(结果):函数成功时分配链表,需用freeaddrinfo()释放。 -
返回值
0:成功- 非零:错误码,用
gai_strerror()转换为人可读字符串
示例代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
int main() {
struct addrinfo hints, *res, *p;
char ipstr[INET6_ADDRSTRLEN];
int status;
// 1. 配置过滤条件
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET; // IPv4
hints.ai_socktype = SOCK_STREAM; // TCP
// 2. 解析地址
if ((status = getaddrinfo("www.example.com", "http", &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return 1;
}
// 3. 遍历结果链表
for (p = res; p != NULL; p = p->ai_next) {
void *addr;
if (p->ai_family == AF_INET) { // IPv4
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
} else { // IPv6
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
}
inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
printf("IP: %s\n", ipstr);
}
// 4. 释放内存
freeaddrinfo(res);
return 0;
}
输出:
lighthouse@VM-8-10-ubuntu:2.tracert$ sudo ./tracert
IP: 104.18.27.120
IP: 104.18.26.120
IP: 2606:4700::6812:1a78
IP: 2606:4700::6812:1b78
5.2 inet_ntop
POSIX 标准中用于将二进制网络地址转换为人类可读的字符串格式的核心函数
extern const char *inet_ntop (
int __af, // 地址族:AF_INET 或 AF_INET6
const void *__restrict __cp, // 指向二进制地址结构(如 struct in_addr)
char *__restrict __buf, // 用户提供的输出缓冲区
socklen_t __len // 缓冲区长度
);
参数详解
-
__af(Address Family)-
AF_INET:IPv4 地址,此时__cp应指向struct in_addr -
AF_INET6:IPv6 地址,此时__cp应指向struct in6_addr
-
-
__cp(Binary Address),指向网络字节序的二进制地址结构:-
IPv4:
struct in_addr(通常是32位整数) -
IPv6:
struct in6_addr(通常是128位数组)
-
-
__buf和__len(Output Buffer),必须由调用者提供足够大的缓冲区:-
IPv4 地址字符串最大长度:
INET_ADDRSTRLEN(16) -
IPv6 地址字符串最大长度:
INET6_ADDRSTRLEN(46) -
推荐使用系统定义的宏,而非硬编码数值。
-
-
返回值
- 成功:返回指向
__buf的指针 - 失败:返回
NULL,并设置errno
- 成功:返回指向
示例代码
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
// IPv4 示例
struct in_addr ipv4_bin;
inet_pton(AF_INET, "192.168.1.100", &ipv4_bin); // 先转换二进制
char ipv4_str[INET_ADDRSTRLEN];
const char *result = inet_ntop(AF_INET, &ipv4_bin, ipv4_str, sizeof(ipv4_str));
if (result) {
printf("IPv4: %s\n", ipv4_str); // 输出: 192.168.1.100
}
// IPv6 示例
struct in6_addr ipv6_bin;
inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", &ipv6_bin);
char ipv6_str[INET6_ADDRSTRLEN];
result = inet_ntop(AF_INET6, &ipv6_bin, ipv6_str, sizeof(ipv6_str));
if (result) {
printf("IPv6: %s\n", ipv6_str); // 输出: 2001:db8:85a3::8a2e:370:7334
}
// 错误处理示例
if (inet_ntop(AF_INET, &ipv4_bin, ipv4_str, 5) == NULL) {
perror("inet_ntop failed"); // 缓冲区太小
}
return 0;
}
输出
lighthouse@VM-8-10-ubuntu:Code-network$ ./test
IPv4: 192.168.1.100
IPv6: 2001:db8:85a3::8a2e:370:7334
inet_ntop failed: No space left on device
5.3 getnameinfo
extern int getnameinfo(
const struct sockaddr *__restrict __sa, // 指向socket地址结构体的指针
socklen_t __salen, // 地址结构体的长度
char *__restrict __host, // 主机名缓冲区
socklen_t __hostlen, // 主机名缓冲区长度
char *__restrict __serv, // 服务名缓冲区(可为nullptr)
socklen_t __servlen, // 服务名缓冲区长度
int __flags // 控制标志位
);
参数详解
| 参数 | 说明 |
|---|---|
__sa |
指向sockaddr结构体的指针(IPv4用sockaddr_in,IPv6用sockaddr_in6) |
__salen |
地址结构体的长度,通常用sizeof()获取 |
__host |
存储解析后主机名的字符缓冲区 |
__hostlen |
主机名缓冲区的长度,应使用NI_MAXHOST(防止缓冲区溢出) |
__serv |
存储服务名的字符缓冲区(如"http"、“ssh”),不需要可设为nullptr |
__servlen |
服务名缓冲区长度,应使用NI_MAXSERV |
__flags |
控制解析行为的标志位(可组合使用) |
关键标志位(Flags)
NI_NAMEREQD // 要求必须成功解析主机名,否则返回错误(EAI_NONAME)
NI_NUMERICHOST // 直接返回IP地址字符串,不进行DNS反向解析(用于调试)
NI_NUMERICSERV // 直接返回端口号字符串,不查询服务名
NI_NOFQDN // 仅返回主机名部分(去掉域名),如返回"mail"而非"mail.example.com"
NI_DGRAM // 指定服务为UDP协议(影响服务名查询)
返回值
- 0:成功
- 非0:错误码,可通过
gai_strerror()获取错误描述
6. 代码实现
6.1 主要部分
#include <iostream>
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/ip_icmp.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/time.h>
#include <netdb.h>
#include <iomanip>
#include <cstdlib>
#include <ctime>
#include <vector>
#include <algorithm>
const int ICMP_HDR_SIZE = 8;
const int PACKET_SIZE = 64; // 32 字节数据 + 8 字节 ICMP 头
const int MAX_HOPS = 30;
const int TIMEOUT_SEC = 3;
const int PING_COUNT = 4;
// 1. 计算ICMP数据包的校验和
unsigned short checksum(unsigned short *buf, int len);
// 2. 将主机名(域名)解析为IPv4地址字符串
std::string resolveHostname(const std::string& hostname);
// 3.计算ICMP数据包的校验和
unsigned short checksum(unsigned short *buf, int len);
// 4. RTT(往返时间)计算函数
long calculateRTT(struct timeval start, struct timeval end);
// 5. 将主机名(域名)解析为IPv4地址字符串
std::string resolveHostname(const std::string& hostname);
// 6. 获取 IP 地址对应的主机名(反向 DNS)
std::string getHostnameByIP(const std::string& ip);
// 7. ping 功能
void ping(const std::string& hostname);
// 8. tracert 功能
void tracert(const std::string& hostname)
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " [ping|tracert] <hostname>" << std::endl;
return 1;
}
std::string mode = argv[1];
std::string hostname = argv[2];
if (mode == "ping") {
ping(hostname);
} else if (mode == "tracert") {
tracert(hostname);
} else {
std::cerr << "Unknown mode: " << mode << std::endl;
return 1;
}
return 0;
}
6.2 ICMP 校验和RTT计算
// 计算ICMP数据包的校验和
unsigned short checksum(unsigned short *buf, int len) {
unsigned long sum = 0;
// 1. 每次处理两字节(16 位)
while (len > 1) {
sum += *buf++;
len -= 2;
}
// 2. 处理奇数长度情况:将最后一个字节作为高8位,低8位补0
if (len == 1) sum += *(unsigned char*)buf;
sum = (sum >> 16) + (sum & 0xFFFF); // 将32位累加和的高16位与低16位相加
sum += (sum >> 16); // 处理可能产生的进位(确保结果不超过16位)
// 3. 返回按位取反结果
return (unsigned short)(~sum);
}
// RTT(往返时间)计算函数
long calculateRTT(struct timeval start, struct timeval end) {
// 计算总差值:秒差*1000 + 微秒差/1000
long diff = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
return diff < 0 ? 0 : diff;
}
1. checksum - 校验和计算
- 采用RFC 1071标准的Internet校验和算法
作用:计算ICMP数据包的校验和,确保数据完整性。
工作流程:
- 以16位为单位累加所有数据
- 处理奇数长度字节的边界情况
- 将高16位进位加回低16位
- 最后取反得到校验和
网络协议中的重要性:ICMP协议要求必须填充正确的校验和,否则数据包会被丢弃。
2. calculateRTT - 计算往返时间
作用:计算从发送到接收的时间差(毫秒)。
工作流程:
- 计算秒级差值并转换为毫秒
- 计算微秒级差值并转换为毫秒
- 合并结果,确保不为负数
6.3 域名和IP相互转换
// 将主机名(域名)解析为IPv4地址字符串
std::string resolveHostname(const std::string& hostname) {
// 1. 声明 addrinfo 结构体: hints用于设置查询条件,res用于存储查询结果
struct addrinfo hints, *res;
// 2. 初始化结构体
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET; // IPv4
// 原始套接字类型, 不能用 SOCK_STREAM, 因为 ICMP 不能运行在 SOCK_STREAM 上
hints.ai_socktype = SOCK_RAW;
hints.ai_protocol = IPPROTO_ICMP;
// 3. DNS 解析: 函数成功时分配链表 pai,需用 freeaddrinfo() 释放。
if(getaddrinfo(hostname.c_str(), nullptr, &hints, &res)!=0){
std::cerr << "Cannot resolve hostname: " << hostname << std::endl;
return ""; // 返回空字符串表示失败
}
char ip_str[INET_ADDRSTRLEN]; // INET_ADDRSTRLEN是IPv4地址字符串的最大长度 (通常为16,包含终止符)
inet_ntop(AF_INET, &((struct sockaddr_in*)res->ai_addr)->sin_addr, ip_str, INET_ADDRSTRLEN);
std::string ip(ip_str);
freeaddrinfo(res);
return ip;
}
// 获取 IP 地址对应的主机名(反向 DNS)
std::string getHostnameByIP(const std::string& ip) {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
char host[NI_MAXHOST];
if (getnameinfo((struct sockaddr*)&addr, sizeof(addr), host, NI_MAXHOST, nullptr, 0, NI_NAMEREQD) == 0) {
return std::string(host);
}
return "";
}
1. resolveHostname - 主机名解析
作用:将域名(如www.baidu.com)转换为IP地址。
工作流程:
- 调用
getaddrinfo()进行DNS解析 - 从结果中提取
sockaddr_in结构 - 使用
inet_ntop()将二进制IP转换为字符串格式 - 释放资源并返回IP字符串
注意:出错时返回空字符串。
2. getHostnameByIP - 反向DNS解析
作用:将IP地址转换回主机名(反向DNS查询)。
工作流程:
- 将字符串IP转换为
sockaddr_in结构 - 调用
getnameinfo()尝试获取主机名 - 成功返回主机名,失败返回空字符串
特点:使用NI_NAMEREQD标志,强制要求返回主机名。
6.4 ping 功能函数
/**
* @brief 向指定主机发送ICMP Echo请求并统计网络状态
* @param hostname 目标主机名或IP地址字符串
*
* 功能特性:
* - 解析主机名到IP地址
* - 发送ICMP Echo请求报文
* - 接收ICMP Echo应答报文
* - 计算往返时间(RTT)
* - 统计丢包率、最小/最大/平均延迟
*/
void ping(const std::string& hostname) {
// ===== 1. 主机名解析 =====
std::string ip = resolveHostname(hostname); // 需要自定义的DNS解析函数
if (ip.empty()) {
// 解析失败,无法获取有效IP地址
return;
}
// ===== 2. 创建原始套接字 =====
// SOCK_RAW: 直接操作网络层,需要root权限
// IPPROTO_ICMP: 只接收ICMP协议报文
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock < 0) {
perror("socket"); // 打印错误信息,如"Permission denied"
return;
}
// ===== 3. 配置目标地址结构 =====
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
// 将字符串IP转换为网络字节序的二进制格式
inet_pton(AF_INET, ip.c_str(), &dest_addr.sin_addr);
// ===== 4. 输出Ping开始信息 =====
std::cout << "Pinging " << hostname << " [" << ip << "] with "
<< PACKET_SIZE - ICMP_HDR_SIZE << " bytes of data:" << std::endl;
// ===== 5. 初始化统计变量 =====
int sent = 0, received = 0; // 发送/接收包计数
std::vector<long> rtt_times; // 存储所有RTT值用于统计
// ===== 6. 主循环:发送和接收ICMP报文 =====
for (int i = 0; i < PING_COUNT; ++i) {
// --- 6.1 构建ICMP Echo请求报文 ---
char packet[PACKET_SIZE] = {0}; // 全零初始化
struct icmphdr* icmp_hdr = (struct icmphdr*)packet; // 强制类型转换
icmp_hdr->type = ICMP_ECHO; // ICMP类型:8 (Echo请求)
icmp_hdr->code = 0; // 代码:0
icmp_hdr->un.echo.id = getpid() & 0xFFFF; // 使用进程ID标识,只取低16位
icmp_hdr->un.echo.sequence = i + 1; // 报文序列号
icmp_hdr->checksum = 0; // 先清零
icmp_hdr->checksum = checksum((unsigned short*)packet, PACKET_SIZE); // 计算校验和
// --- 6.2 记录发送时间 ---
struct timeval send_time;
gettimeofday(&send_time, nullptr); // 获取微秒级精度时间
// --- 6.3 发送报文 ---
if (sendto(sock, packet, PACKET_SIZE, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr)) <= 0) {
perror("sendto");
continue; // 发送失败,继续下一次循环
}
sent++; // 发送计数+1
// --- 6.4 接收应答或超时 ---
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sock, &read_fds); // 将套接字加入读描述符集合
struct timeval timeout = {TIMEOUT_SEC, 0}; // 超时时间(秒)
// select等待套接字可读或超时
if (select(sock + 1, &read_fds, nullptr, nullptr, &timeout) > 0) {
// 套接字可读,表示收到应答
char recv_buf[256];
struct sockaddr_in src_addr;
socklen_t src_len = sizeof(src_addr);
// 接收数据报文
ssize_t n = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr*)&src_addr, &src_len);
if (n > 0) {
// 解析IP头部(获取IP头长度,定位ICMP数据位置)
struct iphdr* ip_hdr = (struct iphdr*)recv_buf;
// ihl字段表示IP头长度(32位字的数量),需乘以4得到字节数
struct icmphdr* icmp = (struct icmphdr*)(recv_buf + ip_hdr->ihl * 4);
// 验证:是Echo应答且ID匹配(防止收到其他进程的应答)
if (icmp->type == ICMP_ECHOREPLY &&
icmp->un.echo.id == (getpid() & 0xFFFF)) {
// --- 6.5 计算RTT ---
struct timeval recv_time;
gettimeofday(&recv_time, nullptr);
long rtt = calculateRTT(send_time, recv_time);
rtt_times.push_back(rtt); // 保存RTT值
// 将源IP从二进制转换为字符串格式
char src_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &src_addr.sin_addr, src_ip, INET_ADDRSTRLEN);
// --- 6.6 输出应答信息 ---
std::cout << "Reply from " << src_ip << ": bytes="
<< (n - ip_hdr->ihl * 4 - ICMP_HDR_SIZE) // 有效载荷长度
<< " time=" << rtt << "ms TTL="
<< (int)ip_hdr->ttl << std::endl;
received++; // 接收计数+1
}
}
} else {
// select返回0(超时)或-1(错误)
std::cout << "Request timed out." << std::endl;
}
}
// ===== 7. 关闭套接字 =====
close(sock);
// ===== 8. 统计信息输出 =====
std::cout << "\nPing statistics for " << ip << ":" << std::endl;
int lost = sent - received;
double loss_rate = sent > 0 ? (double)lost / sent * 100 : 0;
std::cout << " Packets: Sent = " << sent << ", Received = " << received
<< ", Lost = " << lost << " (" << static_cast<int>(loss_rate) << "% loss)," << std::endl;
// --- 8.1 RTT统计计算 ---
if (!rtt_times.empty()) {
long min_rtt = *min_element(rtt_times.begin(), rtt_times.end());
long max_rtt = *max_element(rtt_times.begin(), rtt_times.end());
long avg_rtt = 0;
for (long t : rtt_times) avg_rtt += t;
avg_rtt /= rtt_times.size();
std::cout << "Approximate round trip times in milli-seconds:\n";
std::cout << " Minimum = " << min_rtt << "ms, Maximum = " << max_rtt
<< "ms, Average = " << avg_rtt << "ms" << std::endl;
}
}
作用:向目标主机发送ICMP Echo请求并统计结果。
完整工作流程:
- 初始化:解析主机名 → 创建RAW ICMP套接字 → 构建目标地址
- 发送循环(执行PING_COUNT次):
- 构建ICMP Echo Request包(填充ID、序列号、校验和)
- 记录发送时间戳
- 调用
sendto()发送数据包 - 使用
select()等待可读事件(3秒超时) - 接收成功:验证ID匹配 → 记录RTT → 输出回复信息
- 超时:输出"Request timed out"
- 统计输出:
- 计算丢包率:
((发送-接收)/发送) * 100% - 计算RTT统计:最小值、最大值、平均值
- 计算丢包率:
- 清理:关闭套接字
输出:

6.5 tracert 功能函数
/**
* @brief 追踪到目标主机的路由路径
* @param hostname 目标主机名或IP地址
*
* 工作原理:
* 1. 从TTL=1开始发送ICMP Echo请求
* 2. 每经过一个路由器,TTL减1,TTL=0时路由器返回ICMP超时报文
* 3. 逐步增加TTL值,直到到达目标或达到最大跳数
* 4. 每个TTL值发送3个探测包,获取平均延迟和多个路径(如果存在负载均衡)
*/
void tracert(const std::string& hostname) {
// ===== 1. 主机名解析 =====
std::string ip = resolveHostname(hostname);
if (ip.empty()) return;
// ===== 2. 特殊处理回环地址 =====
// 回环地址不经过任何路由器,直接返回本地结果
if (ip == "127.0.0.1") {
std::cout << "Tracing route to " << hostname << " [" << ip << "]" << std::endl;
std::cout << "over a maximum of 30 hops:" << std::endl;
std::cout << " 1 0ms 0ms 0ms localhost [127.0.0.1]" << std::endl;
std::cout << "Trace complete." << std::endl;
return;
}
std::cout << "Tracing route to " << hostname << " [" << ip << "]" << std::endl;
std::cout << "over a maximum of " << MAX_HOPS << " hops:" << std::endl;
// ===== 3. 主循环:递增TTL值 =====
// TTL从1开始递增,每个TTL值代表经过的路由器跳数
for (int ttl = 1; ttl <= MAX_HOPS; ++ttl) {
std::cout << std::setw(2) << ttl << " "; // 打印跳数序号
std::vector<std::string> hop_ips; // 存储该跳返回的IP地址(可能有多个)
std::vector<long> rtt_list; // 存储往返时间
// ===== 4. 每个TTL发送3个探测包 =====
// 发送多个包用于:1)检测丢包率 2)发现负载均衡路径 3)获取更准确的平均延迟
for (int probe = 0; probe < 3; ++probe) {
// --- 4.1 创建原始套接字 ---
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock < 0) {
perror("socket");
std::cout << "* "; // 套接字创建失败,输出*
continue;
}
// --- 4.2 设置当前TTL值(核心操作) ---
// 这是实现traceroute的关键:控制IP数据包的生存时间
setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
// --- 4.3 构建ICMP Echo请求报文 ---
char packet[PACKET_SIZE] = {0};
struct icmphdr* icmp_hdr = (struct icmphdr*)packet;
icmp_hdr->type = ICMP_ECHO; // 类型:8 (Echo请求)
icmp_hdr->code = 0;
icmp_hdr->un.echo.id = getpid() & 0xFFFF; // 进程ID标识
icmp_hdr->un.echo.sequence = ttl * 100 + probe; // 唯一序列号,便于识别
icmp_hdr->checksum = 0;
icmp_hdr->checksum = checksum((unsigned short*)packet, PACKET_SIZE);
// --- 4.4 设置目标地址 ---
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip.c_str(), &dest_addr.sin_addr);
// --- 4.5 发送并记录时间 ---
struct timeval send_time;
gettimeofday(&send_time, nullptr);
sendto(sock, packet, PACKET_SIZE, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
// --- 4.6 接收应答报文 ---
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sock, &read_fds);
struct timeval timeout = {TIMEOUT_SEC, 0}; // 超时设置
bool got_reply = false;
long rtt = -1;
if (select(sock + 1, &read_fds, nullptr, nullptr, &timeout) > 0) {
char recv_buf[256];
struct sockaddr_in src_addr;
socklen_t src_len = sizeof(src_addr);
ssize_t n = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr*)&src_addr, &src_len);
if (n > 0) {
// 解析IP头部,跳过IP头找到ICMP数据
struct iphdr* ip_hdr = (struct iphdr*)recv_buf;
struct icmphdr* icmp = (struct icmphdr*)(recv_buf + ip_hdr->ihl * 4);
// --- 4.7 验证应答报文 ---
// 接受两种类型:ICMP_TIMXCEED(超时)和ICMP_ECHOREPLY(到达目标)
if (icmp->type == ICMP_TIMXCEED || icmp->type == ICMP_ECHOREPLY) {
struct timeval recv_time;
gettimeofday(&recv_time, nullptr);
rtt = calculateRTT(send_time, recv_time);
rtt_list.push_back(rtt);
char src_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &src_addr.sin_addr, src_ip, INET_ADDRSTRLEN);
hop_ips.push_back(std::string(src_ip));
got_reply = true;
}
}
}
// --- 4.8 输出单个探测结果 ---
if (got_reply) {
std::cout << rtt << "ms ";
} else {
std::cout << "* "; // 超时或无应答
}
close(sock); // 每个探测包使用独立套接字
}
// ===== 5. 输出该跳的路由器信息 =====
if (!hop_ips.empty()) {
// 去重:如果多个探测返回相同IP,只显示一次
std::string first_ip = hop_ips[0];
std::string hostname_str = getHostnameByIP(first_ip); // 反向DNS查询
if (!hostname_str.empty()) {
std::cout << hostname_str << " [" << first_ip << "]";
} else {
std::cout << first_ip;
}
}
std::cout << std::endl;
// ===== 6. 终止条件检查 =====
// 如果收到ICMP_ECHOREPLY且源IP匹配目标IP,说明已到达目标
if (!hop_ips.empty()) {
// 简易判断:只要第一个应答IP是目标IP就停止
// 注意:实际应该检查icmp->type == ICMP_ECHOREPLY
if (hop_ips[0] == ip) {
std::cout << "Trace complete." << std::endl;
break; // 跳出TTL循环
}
}
}
}
作用:追踪从本机到目标主机的路径,显示每一跳的路由信息。
完整工作流程:
- 初始化:解析主机名 → 特殊处理127.0.0.1回环地址
- TTL循环(从1到MAX_HOPS):
- 输出当前跳数(1, 2, 3…)
- 每个TTL发送3个探测包:
- 创建RAW套接字并设置
IP_TTL选项 - 构建ICMP包(序列号=ttl*100+probe)
- 发送并记录时间戳
- 等待回复(3秒超时)
- 收到回复:提取源IP → 计算RTT → 存储结果
- 超时:输出"*"
- 创建RAW套接字并设置
- 输出本跳结果:
- 显示3个探测包的RTT(或"*")
- 解析第一跳IP的主机名(反向DNS)
- 格式:
IP或主机名 [IP]
- 终止条件:如果收到
ICMP_ECHOREPLY且IP匹配目标,输出"Trace complete"并退出
关键机制:
- TTL超时原理:每经过一个路由器,TTL减1,当TTL=0时路由器返回
ICMP_TIMXCEED(超时) - 逐跳探测:通过递增TTL值,可以获取路径上每个路由器的地址
输出:

【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【网络】的内容,请持续关注我 !!

更多推荐


所有评论(0)