4. ARP协议详解:ARP请求/响应、ARP缓存表、免费ARP、在LwIP中的实现

各位同学,咱们今天来啃一块硬骨头——ARP协议。说实话,很多做嵌入式网络开发的朋友,对ARP的理解就停留在“IP转MAC”这个层面。但实际项目中,ARP搞不好,网络直接瘫痪。我当年调试一个物联网网关,就因为ARP缓存表没处理好,整栋楼的设备都连不上服务器,被老板骂得狗血淋头。今天咱们就把ARP彻底讲透。

4.1 为什么需要ARP?

你想想看,网络通信最终依赖的是MAC地址。IP地址是逻辑地址,它帮你找到目标设备所在的网段。但到了局域网内部,交换机只认MAC地址。所以,当你的设备知道目标IP(比如192.168.1.100),却不知道它的MAC地址时,就需要ARP来“搭桥”。

说白了,ARP就是一个“寻人启事”协议。它在局域网里广播:“谁是192.168.1.100?请把你的MAC地址告诉我!”

4.2 ARP请求与响应:一问一答

ARP的工作流程其实很简单,就两步:

  1. ARP请求:源设备广播一个ARP请求包。这个包的目标MAC地址填的是全F(FF:FF:FF:FF:FF:FF),表示“所有人注意”。包里面包含了源IP、源MAC,以及目标IP。
  2. ARP响应:目标设备收到请求后,发现目标IP是自己,就单播回复一个ARP响应包。里面带上自己的MAC地址。

这里有个细节要注意:ARP请求是广播,但ARP响应是单播。为什么?因为请求方已经在请求包里把自己的MAC地址告诉对方了,响应方可以直接回复。

ARP报文结构(以太网环境)

+----------------+----------------+----------------+----------------+
| 硬件类型 (2字节) | 协议类型 (2字节) | 硬件地址长度(1) | 协议地址长度(1) |
+----------------+----------------+----------------+----------------+
| 操作码 (2字节)  |  源MAC地址 (6字节)  |  源IP地址 (4字节)   |
+----------------+----------------+----------------+----------------+
|  目标MAC地址 (6字节)  |  目标IP地址 (4字节)   |
+----------------+----------------+----------------+----------------+

操作码:1表示请求,2表示响应。这个字段很重要,接收方靠它区分是请求还是响应。

4.3 ARP缓存表:别每次都广播

如果每次发数据都要广播一次ARP请求,那网络效率就太低了。所以每个设备都维护着一张ARP缓存表,记录着IP地址和MAC地址的对应关系。

我见过不少新手工程师,在嵌入式设备里把ARP缓存表设得特别大,结果内存爆了。其实嵌入式设备,缓存表一般就10~20条记录就够了。为什么?因为你的设备通常只跟少数几个设备通信(比如网关、服务器)。

IP地址MAC地址状态生存时间
192.168.1.100:11:22:33:44:55RESOLVED120秒
192.168.1.100AA:BB:CC:DD:EE:FFPENDING3秒

状态字段很关键:

  • RESOLVED:已经解析成功,可以直接用。
  • PENDING:正在等待响应,还没拿到MAC地址。
  • STALE:过期了,需要重新验证。

生存时间(TTL)一般建议设120秒左右。太短了频繁广播,太长了MAC地址变了都不知道。

避坑指南:我曾经在一个项目里,把ARP缓存TTL设成了600秒。结果网关换了网卡,MAC地址变了,我的设备还拿着旧MAC发了10分钟的数据包,全部丢包。从那以后,我习惯把TTL控制在120秒以内。

4.4 免费ARP:我来了,都给我更新

免费ARP(Gratuitous ARP)是个很有意思的东西。它跟普通的ARP请求长得一模一样,但目标IP填的是自己的IP。说白了,就是设备主动告诉所有人:“我的IP是XXX,我的MAC是YYY,你们都给我记好了!”

免费ARP的典型应用场景:

  1. IP地址冲突检测:设备上线时发一个免费ARP。如果收到响应,说明有人用了同样的IP,赶紧报错。
  2. MAC地址变更通知:网卡换了,或者虚拟机迁移了,发个免费ARP让交换机更新MAC表。
  3. 网关冗余切换:VRRP等协议里,主备切换后发免费ARP,让所有设备知道网关MAC变了。

注意:免费ARP不能滥用。我曾经见过一个设备每5秒发一次免费ARP,直接把局域网里其他设备的ARP缓存表刷爆了。正常情况,设备上线时发一次就够了。

4.5 在LwIP中的实现

LwIP(Lightweight IP)是嵌入式领域最常用的TCP/IP协议栈。它的ARP实现非常精简,咱们来看看核心代码。

4.5.1 ARP表结构

struct etharp_entry {
  struct etharp_entry *next;    // 链表指针
  struct etharp_entry *prev;
  ip_addr_t ipaddr;             // IP地址
  struct eth_addr ethaddr;      // MAC地址
  u8_t state;                   // 状态:ETHARP_STATE_EMPTY, ETHARP_STATE_PENDING, ETHARP_STATE_STABLE
  u8_t time;                    // 生存时间计数器
  u8_t ctime;                   // 创建时间
};

LwIP的ARP表是一个静态数组,默认大小是10个条目。为什么用静态数组?因为嵌入式设备不能动态分配内存,怕内存碎片。我个人觉得这个设计很务实。

4.5.2 ARP请求发送流程

err_t etharp_request(struct netif *netif, const ip_addr_t *ipaddr) {
  struct etharp_hdr *hdr;
  struct pbuf *p;

  // 1. 分配pbuf
  p = pbuf_alloc(PBUF_RAW, sizeof(struct etharp_hdr), PBUF_RAM);
  if (p == NULL) {
    return ERR_MEM;
  }

  // 2. 填充ARP头部
  hdr = (struct etharp_hdr *)p->payload;
  hdr->hwtype = htons(1);           // 以太网
  hdr->proto = htons(0x0800);       // IP协议
  hdr->hwlen = 6;
  hdr->protolen = 4;
  hdr->opcode = htons(ARP_REQUEST); // 操作码:请求

  // 3. 填充源MAC和源IP
  MEMCPY(&hdr->shwaddr, netif->hwaddr, ETH_HWADDR_LEN);
  SMEMCPY(&hdr->sipaddr, &netif->ip_addr, sizeof(ip_addr_t));

  // 4. 目标MAC填0,目标IP填目标地址
  memset(&hdr->dhwaddr, 0, ETH_HWADDR_LEN);
  SMEMCPY(&hdr->dipaddr, ipaddr, sizeof(ip_addr_t));

  // 5. 通过以太网层发送(广播)
  etharp_output(netif, p, (const ip_addr_t *)&ip_addr_broadcast);

  return ERR_OK;
}

这段代码逻辑很清晰。注意第4步,目标MAC填0,因为还不知道嘛。第5步用广播地址发送,让所有人都能收到。

4.5.3 ARP响应处理

static err_t etharp_raw(struct netif *netif, const struct eth_addr *ethaddr,
                        const struct eth_addr *dest, const ip_addr_t *ipaddr,
                        const ip_addr_t *destipaddr, u16_t opcode) {
  // ... 类似请求的填充过程,但opcode设为ARP_REPLY
  // 目标MAC填请求方的MAC,目标IP填请求方的IP
  // 单播发送
}

响应处理的关键点:

  • 操作码改成ARP_REPLY
  • 目标MAC和IP从请求包里提取出来
  • 用单播方式发送,不是广播

4.5.4 缓存表更新策略

LwIP的ARP缓存更新遵循一个原则:谁主动谁更新

  • 收到ARP响应时,把对方的IP-MAC映射加入缓存表。
  • 收到ARP请求时,如果请求的是自己的IP,也把对方的映射加入缓存表(这叫“偷听”)。
  • 缓存表满了怎么办?LwIP会踢掉最旧的那个条目。

我的经验:LwIP默认的ARP表只有10个条目。如果你的设备要跟很多设备通信,记得在lwipopts.h里把ARP_TABLE_SIZE改大。我一般设成20,再大就浪费内存了。

4.6 常见问题与避坑

  1. ARP请求超时:LwIP默认重试5次,每次间隔1秒。如果5次都没响应,就返回超时。我建议在应用层也加个超时处理,别死等。
  2. ARP欺骗:局域网里如果有人恶意发送伪造的ARP响应,你的设备就会把数据发给错误的地方。LwIP没有防ARP欺骗的机制,需要自己在应用层加校验。
  3. 代理ARP:有些路由器会替其他设备回复ARP请求。如果你的设备跟路由器不在同一个网段,可能会遇到这个问题。

我曾经踩过的坑:在一个项目里,设备A和设备B通过一个交换机直连。设备A发ARP请求找设备B,但设备B的MAC地址变了。设备A的ARP缓存里还留着旧MAC,结果发了半天数据都丢了。后来我加了个机制:如果连续3次发送失败,就主动清除ARP缓存,重新请求。

4.7 小结

ARP协议看着简单,但实际项目中坑不少。记住几个要点:

  • ARP请求是广播,响应是单播
  • 缓存表别太大,TTL别太长
  • 免费ARP用于IP冲突检测和MAC变更通知
  • LwIP的ARP实现很精简,但该有的功能都有

下一章咱们讲ICMP协议,就是那个ping命令用的协议。到时候你会发现,ICMP和ARP经常一起出现,配合着用才能把网络调通。