面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。

🧱 一、ICMP 协议基础
🔸 ICMP 是什么?
ICMP(Internet Control Message Protocol,因特网控制报文协议)是 IP 协议族中的一个核心协议,用于在网络中传递控制消息、错误信息以及进行诊断(例如 ping 命令)。
✅ 它不属于传输层协议(不像 TCP/UDP),而是紧贴 IP 层的"辅助协议"。
常见的用途主要有以下几个方面:
-
ping 检测目标是否可达(ICMP Echo)
-
traceroute 路由追踪(依赖 ICMP 超时响应)
-
通知主机目标不可达(Destination Unreachable)
-
报告 TTL 过期(Time Exceeded)
ICMP 的结构依赖 Type
类型,下面是最常用的 Type 8(Echo Request) 报文结构:
字段名 | 大小(字节) | 描述 |
---|---|---|
Type | 1 | 消息类型:8 = Echo Request,0 = Echo Reply |
Code | 1 | 子类型,Echo 请求/响应中通常为 0 |
Checksum | 2 | 报文的校验和(校验 ICMP 报文完整性) |
Identifier | 2 | 标识符,一般是当前进程的 PID,用于配对 Request / Reply |
Sequence | 2 | 序列号,通常从 0 开始递增 |
Data | 可变 | 自定义内容(可为时间戳、固定填充值等) |
每个 ICMP
报文都有一个 Type
字段,用于标识报文的功能和类型,不同的 Code
值则细化了报文的具体含义。
下面是 ICMP 中最常见的一些类型,它们在网络排错(如 ping
、traceroute
)、路由控制和错误报告中都扮演着重要角色。
Type | 名称 | Code | 含义说明 | 常见场景 |
---|---|---|---|---|
0 |
Echo Reply | 0 | ping 的应答包 | ping 的返回响应 |
3 |
Destination Unreachable | 多个 | 无法到达目标,具体原因由 Code 决定 | 路由失败、端口无响应等 |
4 |
Source Quench | 0 | 拥塞控制:源端速率太快,建议降速(已废弃) | 老旧协议中的拥塞控制 |
5 |
Redirect Message | 多个 | 告诉源主机"你走错路了,应走某个路由" | 路由器通知客户端优化路径 |
8 |
Echo Request | 0 | ping 的请求包 | 发起 ping |
11 |
Time Exceeded | 0/1 | TTL 超时(0),或 IP 分片重组超时(1) | traceroute 工具中的核心机制 |
12 |
Parameter Problem | 0 | 报文有问题(比如头部字段错误) | 协议解析错误时的响应 |
📶 二、整个通信流程深入细化
执行 ping www.baidu.com
,表面是一次简单的连通性测试,背后却涉及了 DNS 查询、ICMP 报文构造与内核协议栈处理等多个网络流程。
🔹 1️⃣ DNS 解析(如有)
当目标是域名(如 www.baidu.com
)时,系统首先会通过 DNS 解析得到其对应的 IP 地址:
-
查询优先级一般是:
-
/etc/hosts
-
本地 DNS 缓存(如
systemd-resolved
) -
/etc/resolv.conf
中配置的 DNS 服务器
-
这一步会触发一个 UDP(或 TCP)协议的 DNS 请求,最终返回目标主机的 IP。
🔹 2️⃣ 构造 ICMP 报文(用户态 → 内核态)
DNS 成功后,ping
工具会开始构造 ICMP 报文并发送出去,过程如下:
-
使用 原始套接字(Raw Socket) 创建一个 ICMP 类型的 socket:
cint sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
-
构建 ICMP Echo Request 报文(Type = 8,Code = 0),填入如下字段:
-
Identifier(标识符,通常是当前进程 PID)
-
Sequence Number(序号)
-
Payload(一般为时间戳或任意填充数据)
-
Checksum(根据报文内容计算)
-
-
报文完成后调用
sendto()
发出,进入内核网络栈,由网卡驱动发送出去。
🔹 3️⃣ 报文到达目标主机(协议栈处理流程)
目标主机收到 ICMP 报文后,其内核网络协议栈会进行以下处理:
-
网卡驱动层:收到网络中断,读取数据包,通知内核有数据可处理。
-
内核协议栈处理:
-
首先识别为 IP 包,查看协议字段,判断为 ICMP。
-
调用内核中的
icmp_rcv()
函数处理 ICMP 报文。
-
-
ICMP 处理逻辑:
-
校验报文合法性(如 checksum)
-
若是
Type = 8
(Echo Request),则生成一个 Echo Reply 报文(Type = 0
) -
复用原有 Identifier 和 Sequence Number,并原样返回 Payload
-
调用
ip_send_reply()
封装 IP 报头并发送响应回源主机
-
🔹 4️⃣ 原主机接收回应并展示结果
收到目标主机返回的 Echo Reply 后,ping
工具通过 recvfrom()
接收报文,匹配标识符和序列号确认响应归属,并根据时间戳计算 RTT,最终输出延迟、TTL 等统计信息。
如下信息所示:
ini
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=15.4 ms
其中字段解释:
字段 | 说明 |
---|---|
64 bytes |
包含 ICMP 报文的总长度 |
from |
回复来源的 IP 地址 |
icmp_seq=1 |
报文序列号(从 0 开始递增) |
ttl=118 |
剩余的 TTL(跳数) |
time=15.4ms |
往返耗时,常用于网络延迟分析 |
✅ 小结流程图
text
[ping命令执行]
↓
[DNS 解析域名]
↓
[构造 ICMP Echo Request 报文]
↓
[套接字发送 → 内核协议栈 → 网卡发出]
↓
[目标主机接收 → 内核生成 Echo Reply]
↓
[原主机接收 Echo Reply → 输出统计信息]
🧠 三、校验和(Checksum)详解
ICMP 校验和是将整个报文按每 16 位(2 字节)为一组累加,若有进位则加回低位,最终对总和取反(按位取反)得到校验值;接收方再次计算校验和并与收到的值相加,结果应为 0xffff
,否则说明报文在传输中可能发生了损坏。
自己实现一个 ping 命令
接下来我们用 Node.js 手写一个"构造 ICMP 报文"的简易 ping 实现,不依赖系统命令、不用现成 ping 库,自己造包发包,测目标主机能不能回。
⚠️ 注意:这个方法需要管理员权限(因为用了 raw socket),推荐在 Linux 或 macOS 上跑,Windows 下支持比较差。
如下代码所示:
js
const raw = require("raw-socket");
const dns = require("dns");
function checksum(buffer) {
let sum = 0;
for (let i = 0; i < buffer.length; i += 2) {
sum += buffer.readUInt16BE(i);
}
sum = (sum >> 16) + (sum & 0xffff);
sum += sum >> 16;
return ~sum & 0xffff;
}
function createICMPPacket(identifier, sequence) {
const buffer = Buffer.alloc(8);
buffer.writeUInt8(8, 0); // ICMP type 8 = Echo Request
buffer.writeUInt8(0, 1); // Code
buffer.writeUInt16BE(0, 2); // Checksum (temp)
buffer.writeUInt16BE(identifier, 4);
buffer.writeUInt16BE(sequence, 6);
const cs = checksum(buffer);
buffer.writeUInt16BE(cs, 2);
return buffer;
}
function resolveHostname(host) {
return new Promise((resolve, reject) => {
dns.lookup(host, (err, address) => {
if (err) reject(err);
else resolve(address);
});
});
}
async function pingHost(host, count = 4) {
const ip = await resolveHostname(host);
console.log(`PING ${host} (${ip}): 56 data bytes`);
const id = process.pid & 0xffff;
const socket = raw.createSocket({ protocol: raw.Protocol.ICMP });
let sent = 0;
let received = 0;
let times = [];
for (let seq = 0; seq < count; seq++) {
await new Promise((resolvePing) => {
const packet = createICMPPacket(id, seq);
const start = process.hrtime.bigint();
const timeout = setTimeout(() => {
console.log(`Request timeout for icmp_seq ${seq}`);
resolvePing();
}, 2000); // 2s timeout
socket.send(packet, 0, packet.length, ip, (err) => {
if (err) {
console.error(`Send error: ${err}`);
clearTimeout(timeout);
resolvePing();
}
});
const onMessage = (buffer, source) => {
if (source !== ip) return;
const type = buffer.readUInt8(20);
const recvId = buffer.readUInt16BE(24);
const recvSeq = buffer.readUInt16BE(26);
if (type === 0 && recvId === id && recvSeq === seq) {
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1e6;
times.push(ms);
received++;
console.log(
`${
packet.length
} bytes from ${ip}: icmp_seq=${seq} time=${ms.toFixed(2)} ms`
);
clearTimeout(timeout);
socket.removeListener("message", onMessage);
resolvePing();
}
};
socket.on("message", onMessage);
sent++;
});
await new Promise((r) => setTimeout(r, 1000)); // 间隔 1s
}
socket.close();
console.log(`\n--- ${host} ping statistics ---`);
console.log(
`${sent} packets transmitted, ${received} received, ${(
100 -
(received / sent) * 100
).toFixed(0)}% packet loss`
);
if (received > 0) {
const min = Math.min(...times).toFixed(2);
const max = Math.max(...times).toFixed(2);
const avg = (times.reduce((a, b) => a + b, 0) / times.length).toFixed(2);
console.log(`rtt min/avg/max = ${min}/${avg}/${max} ms`);
}
}
// 用法:你可以传 IP 或域名
pingHost("google.com", 4); // 你也可以改成 '8.8.8.8'
raw-socket
是一个 Node.js 的原生扩展模块,用于创建原始网络套接字(raw socket),它允许开发者绕过 TCP 和 UDP 层,直接发送 IP 层协议的数据包,比如 ICMP(也就是 ping 使用的协议)。这个包是构造和发送 ICMP 报文的关键工具,但由于其操作的是系统底层网络,通常需要管理员权限才能使用。通过 raw-socket
,我们就能在 Node.js 中模拟系统级别的网络工具,比如 ping
和 traceroute
,实现真正的底层网络调试功能。
在上面的代码中,有如下步骤解释:
-
首先,程序接收到一个主机地址(可以是域名或 IP)和要 ping 的次数。
-
调用
resolveHostname
函数,把域名解析成实际 IP 地址。 -
使用
raw-socket
创建一个原始 ICMP socket,用于发包和接收响应。 -
进入一个循环,每次构造一个 ICMP Echo Request 报文,并通过 socket 发送。
-
同时启动监听,如果目标主机返回 Echo Reply,就计算往返时间(RTT)。
-
每次发送设置一个 2 秒的超时,如果没回应就记为丢包。
-
重复上述操作 N 次后,关闭 socket,并统计:总共发了几包、收到了几包、丢包率、最小/最大/平均 RTT。
-
最后打印出一份类似系统 ping 的结果报告,供开发者参考。
最终结果如下图所示:

最终使用 ping 命令来对比,发现差不多,成功实现。

🔚 总结一句话
ping
就是一个用来"问候"别的主机的小工具,它通过 ICMP 协议给目标发一个"你在吗?"的请求(Echo Request),对方如果在、网络也通,就会回一个"我在!"(Echo Reply)。我们通过这种来回的时间,既能知道对方在不在,也能测出网络延迟有多大。它用的是网络协议栈的底层,不走 TCP 也不走 UDP,直接用的是 ICMP,所以非常轻量、简单、但很有用。