深蓝黑背景封面。左侧局域网示意图:左下角主机 A(IP 192.168.1.10,想要网关的 MAC)向右发出标注①的「ARP 请求(广播):谁是 192.168.1.1?」,请求经中间的交换机广播域竖线扇出到右上的网关(IP 192.168.1.1,MAC BB:…:02)和右下的主机 C(IP 192.168.1.30,与己无关丢弃请求);网关沿顶部弧线单独向 A 回一条标注②的紫色「ARP 应答(单播):我是,MAC = BB:…:02」;竖线下方注明「同一广播域:请求人人可见,应答只回给提问者」;最底部一行青色小结「广播问一遍 → 单播答回来 → 结果写进 ARP 缓存」。右侧大标题「IP 怎么变成 MAC 地址」,副标题「ARP 协议的工作全过程」,小字「请求 · 应答 · 缓存 · 免费 ARP」。左上标注「面试经典 · 网络」,右上圆角胶囊标注「Day 02 · 11-09」 图 1:本文封面,广播请求与单播应答的一次完整往返。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「IP 地址和物理地址之间靠什么转换」是网络面试的入门题,但多数人只答得出「ARP」三个字母。面试官真正想听的是全过程:报文长什么样、缓存怎么工作、免费 ARP 是干什么的。

为什么需要 ARP:IP 管往哪去,MAC 管这一跳给谁。

IP 地址是逻辑地址,回答「数据最终要到哪台机器」;而以太网帧在链路上跑,需要的是 48 位的物理地址(MAC)。同一局域网里发包,网卡只认 MAC——所以发出之前,必须先把「下一跳的 IP」翻译成「下一跳的 MAC」。RFC 826 定义的这个翻译协议就是 ARP:地址解析协议。

报文长什么样。

ARP 报文直接装在以太网帧里(帧类型 0x0806),关键字段有:硬件类型(以太网 = 1)、协议类型(IPv4 = 0x0800)、两个地址长度、操作码(请求 = 1,应答 = 2),然后是发送方 MAC、发送方 IP、目标 MAC、目标 IP。请求报文里目标 MAC 填全 0——这正是要问的东西;而承载它的以太网帧,目的 MAC 填广播地址全 F,交换机会把它送进网内每一个端口。

一次解析的完整过程,四步。

第一步查缓存。每台主机维护一张 ARP 缓存表(IP → MAC 的映射),有记录且未老化就直接用,一个 ARP 包都不发。

第二步广播请求。缓存未命中,主机 A 向全网广播:「谁是 192.168.1.1?请告诉 192.168.1.10。」同一广播域里所有机器都能收到。

第三步目标单播应答。网关认出目标 IP 是自己,单独回 A 一条应答:「我是,MAC 是 BB:…:02」;其他机器看一眼发现与己无关,直接丢弃。

第四步双方写缓存。这里有 RFC 826 里一个容易被忽略的细节:收到 ARP 报文的主机,要先把发送方的 IP/MAC 映射并入自己的表,然后才看操作码决定要不要回复。也就是说 A 问网关的同时,网关也顺带记下了 A——协议假设通信是双向的,这次顺手能省掉将来的一次反向广播。

缓存与免费 ARP,两个常考补丁。

缓存条目带老化时间:MAC 理论上固定,但主机会换网卡、虚拟机会迁移,旧映射不能永存。Linux 上可以用 arp -a 或 ip neigh 查看这张表。

免费 ARP 是一个「自问自答」的请求:源 IP 和目标 IP 都填自己。它干两件事——一是开机自检,若有人应答说明 IP 冲突了;二是主动刷新,本机 MAC 变了(换网卡、高可用集群里虚拟 IP 漂移到备机)后广播一声,让所有邻居把缓存里的旧映射换掉。主备切换后业务能秒级恢复,靠的就是这一声广播。

跨网段时,ARP 解析的其实不是对端。

ARP 广播出不了路由器。跨网段通信时,主机查路由表发现目标不在本网段,于是改为解析默认网关的 MAC,把帧交给网关去转发。所以「能 ping 通网关却到不了同网段另一台机器」这类故障,最后往往落在 ARP 表上——容器网络与 VPC 排障时尤其常见。

把全过程跑一遍。

下面这段 JavaScript 模拟三台主机的局域网:A 两次访问网关,随后 A「换网卡」发免费 ARP。用 Node.js 22 实测:

const net = { hosts: [], broadcast: 0, unicast: 0 };
function host(name, ip, mac) {
  const h = { name, ip, mac, cache: new Map(),
    resolve(t) {
      if (h.cache.has(t)) return console.log(`${name}: 缓存命中 ${t}(0 个 ARP 包)`);
      net.broadcast++;
      console.log(`${name}: 广播请求「谁是 ${t}?」`);
      for (const o of net.hosts) {
        if (o === h) continue;
        o.cache.set(h.ip, h.mac);          // 收方先把发送方映射并入表
        if (o.ip === t) {
          net.unicast++;
          console.log(`${o.name}: 单播应答「我是 ${t},MAC=${o.mac}」`);
          h.cache.set(t, o.mac);
        }
      }
    },
    gratuitous() {                          // 免费 ARP:源 IP 与目标 IP 都是自己
      net.broadcast++;
      for (const o of net.hosts) if (o !== h) o.cache.set(h.ip, h.mac);
      console.log(`${name}: 免费 ARP,全网刷新我的映射`);
    } };
  net.hosts.push(h); return h;
}
const A = host('A', '192.168.1.10', 'AA:01');
const GW = host('GW', '192.168.1.1', 'BB:02');
host('C', '192.168.1.30', 'CC:03');
A.resolve('192.168.1.1');
A.resolve('192.168.1.1');
A.mac = 'DD:04';                            // 模拟换网卡
A.gratuitous();
console.log('免费 ARP 后 GW 的缓存:', [...GW.cache]);

左侧「解析时序」面板以访问同网段网关为例给出四步:第一步先查 ARP 缓存,有映射且未老化就直接用、一个包都不发;第二步广播 ARP 请求「谁是 192.168.1.1?请告诉 192.168.1.10」;第三步目标单播应答,网关回「我是,MAC = BB:…:02」,其余主机丢弃请求;第四步双方写入 ARP 缓存,IP 与 MAC 的映射带老化时间,下次免广播。右上面板列出 ARP 报文关键字段(RFC 826):硬件类型以太网=1、协议类型 IPv4=0x0800、帧类型 0x0806、操作码请求 1 应答 2、发送方 MAC 与发送方 IP、目标 MAC(请求时全 0、帧的目的 MAC 为广播全 F)与目标 IP。右下面板讲两个常考补丁:ARP 缓存——每条映射带老化时间、收到应答即刷新、arp -a 可查看;免费 ARP——源 IP 与目标 IP 都填自己,用于开机自检 IP 冲突,同时让邻居刷新缓存里的旧 MAC 映射。底部总结条:同网段 ARP 直接解析对方 MAC,跨网段 ARP 解析的是网关 MAC、帧交给网关去转发 图 2:解析四步时序、报文字段与两个常考补丁。

实测输出:第一次访问产生 1 个广播加 1 个单播;第二次缓存命中,0 个 ARP 包;网关的表里也被顺手写入了 A 的映射;免费 ARP 之后,网关缓存里 A 的旧 MAC「AA:01」被新值「DD:04」刷新。四步机制与两个补丁全部兑现。

回到面试现场,建议的答题结构:一句话定位(ARP 把下一跳 IP 解析成 MAC,链路层发包需要)→ 四步过程(查缓存、广播请求、单播应答、写缓存,主动提「收方也顺手记发送方」这个细节)→ 两个补丁(缓存老化、免费 ARP)→ 一个边界(跨网段解析的是网关 MAC)。答到这里,这道题就从背诵题变成了展示题。你排障时遇到过 ARP 相关的坑吗,比如 IP 明明能通、二层却收不到包?评论区聊聊。