面试官:ping 的详细流程是怎么样的?🙉🙉🙉

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

🧱 一、ICMP 协议基础

🔸 ICMP 是什么?

ICMP(Internet Control Message Protocol,因特网控制报文协议)是 IP 协议族中的一个核心协议,用于在网络中传递控制消息、错误信息以及进行诊断(例如 ping 命令)。

✅ 它不属于传输层协议(不像 TCP/UDP),而是紧贴 IP 层的"辅助协议"。

常见的用途主要有以下几个方面:

  1. ping 检测目标是否可达(ICMP Echo)

  2. traceroute 路由追踪(依赖 ICMP 超时响应)

  3. 通知主机目标不可达(Destination Unreachable)

  4. 报告 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 中最常见的一些类型,它们在网络排错(如 pingtraceroute)、路由控制和错误报告中都扮演着重要角色。

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 地址:

  • 查询优先级一般是:

    1. /etc/hosts

    2. 本地 DNS 缓存(如 systemd-resolved

    3. /etc/resolv.conf 中配置的 DNS 服务器

这一步会触发一个 UDP(或 TCP)协议的 DNS 请求,最终返回目标主机的 IP。

🔹 2️⃣ 构造 ICMP 报文(用户态 → 内核态)

DNS 成功后,ping 工具会开始构造 ICMP 报文并发送出去,过程如下:

  • 使用 原始套接字(Raw Socket) 创建一个 ICMP 类型的 socket:

    c 复制代码
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  • 构建 ICMP Echo Request 报文(Type = 8,Code = 0),填入如下字段:

    • Identifier(标识符,通常是当前进程 PID)

    • Sequence Number(序号)

    • Payload(一般为时间戳或任意填充数据)

    • Checksum(根据报文内容计算)

  • 报文完成后调用 sendto() 发出,进入内核网络栈,由网卡驱动发送出去。

🔹 3️⃣ 报文到达目标主机(协议栈处理流程)

目标主机收到 ICMP 报文后,其内核网络协议栈会进行以下处理:

  1. 网卡驱动层:收到网络中断,读取数据包,通知内核有数据可处理。

  2. 内核协议栈处理

    • 首先识别为 IP 包,查看协议字段,判断为 ICMP。

    • 调用内核中的 icmp_rcv() 函数处理 ICMP 报文。

  3. 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 中模拟系统级别的网络工具,比如 pingtraceroute,实现真正的底层网络调试功能。

在上面的代码中,有如下步骤解释:

  1. 首先,程序接收到一个主机地址(可以是域名或 IP)和要 ping 的次数。

  2. 调用 resolveHostname 函数,把域名解析成实际 IP 地址。

  3. 使用 raw-socket 创建一个原始 ICMP socket,用于发包和接收响应。

  4. 进入一个循环,每次构造一个 ICMP Echo Request 报文,并通过 socket 发送。

  5. 同时启动监听,如果目标主机返回 Echo Reply,就计算往返时间(RTT)。

  6. 每次发送设置一个 2 秒的超时,如果没回应就记为丢包。

  7. 重复上述操作 N 次后,关闭 socket,并统计:总共发了几包、收到了几包、丢包率、最小/最大/平均 RTT。

  8. 最后打印出一份类似系统 ping 的结果报告,供开发者参考。

最终结果如下图所示:

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

🔚 总结一句话

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

相关推荐
SurgeJS3 分钟前
我造了一个轮子:Norm Axios(约定式请求)
前端·vue.js
USER_A00120 分钟前
【VUE3】练习项目——大事件后台管理
前端·vue.js·axios·pinia·elementplus·husky·vuerouter4
fruge23 分钟前
Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
前端·css·sass
开心猴爷31 分钟前
Flutter 开发系列(八):Flutter 项目的自动化测试与调试
后端
开心就好202532 分钟前
将Flutter推向极限:你应该知道的44个性能提示
后端
00后程序员33 分钟前
【Flutter】自动测试探索
后端
aiopencode35 分钟前
Flutter快学快用24讲--09 单元测试:Flutter 应用单元测试,提升代码质量
后端
调试人生的显微镜36 分钟前
Nativefier——可以把网页打包成exe的工具
后端
疯狂的程序猴36 分钟前
android studio 运行flutter报错
后端
鸿蒙场景化示例代码技术工程师39 分钟前
基于AssetStoreKit实现免密登录鸿蒙示例代码
前端