面试官: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,所以非常轻量、简单、但很有用。

相关推荐
uzong2 小时前
技术故障复盘模版
后端
GetcharZp3 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
玩转以太网4 小时前
基于W55MH32Q-EVB 实现 HTTP 服务器配置 OLED 滚动显示信息
服务器·网络协议·http
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端