一、ARP协议简介
ARP,全称 Address Resolution Protocol,译作地址解析协议,ARP 协议与底层网络接口密切相关。TCP/IP 标准分层结构中,把 ARP 划分为了网络层的重要组成部分。 当一个主机上的应用程序要向目标主机发送数据时,它只知道目标主机的 IP 地址,而在协议栈底层接口发送数据包时,需要将该 IP 地址转换为目标主机对应的 MAC 地址,这样才能在数据链路上选择正确的通道将数据包传送出去,在整个转换过程中发挥关键作用的就是 ARP 协议了。 本次的学习内容有:
ARP 协议的原理;
ARP 缓存表及其创建、维护、查询;
ARP 报文结构。
1.1、物理地址与网络地址
网卡的 48 位 MAC 地址都保存在网卡的内部存储器中,TCP/IP 协议有32bit 的 IP 地址(网络地址),网络层发送数据包时只知道目的主机的 IP 地址,而底层接口(如以太网驱动程序)必须知道对方的硬件地址才能将数据发送出去。
为了解决地址映射的问题,ARP 协议提供了一种地址动态解析的机制,在32 bit的 IP 地址和采用不同网络技术的硬件地址之间提供动态映射,为上层将底层的物理地址差异屏蔽起来,这样上层的因特网协议便可以灵活的使用 IP 地址进行通信。
1.2、ARP协议的本质
ARP 协议使用目标主机的 IP 地址,查询其对应的 MAC 地址,保证底层链路上数据包通信的进行。
假如我们的主机(192.168.1.78)需要向开发板(192.168.1.37)发送一个 IP 数据包,当发送数据时,主机会在自己的 ARP 缓存表中寻找是否有目标IP地址。如果找到了,也就知道了目标 MAC 地址为(008048123456),此时主机直接把目标 MAC 地址写入以太网帧首部发送就可以了;如果在 ARP 缓存表中没有找到相对应的 IP 地址,此时比较不幸,我们的数据需要被延迟发送,随后主机会先在网络上发送一个广播(ARP 请求,以太网目的地址为 FFFFFFFFFFFF),广播的 ARP 请求表示同一网段内的所有主机将会收到这样一条信息:“192.168.1.37 的 MAC 地址是什么?请回答”。网络 IP 地址为 192.168.1.37(开发板)的主机接收到这个帧后,它有义务做出这样的回答(ARP 应答):“192.168.1.37 的 MAC 地址是(008048123456)”。 这样,主机就知道了开发板的 MAC 地址,先前被延迟的数据包就可以发送了,此外,主机会将这个地址对保存在缓存表中以便后续数据包发送时使用。 ARP 的实质就是对缓存表的建立、更新、查询等操作。
二、数据结构
头文件etharp.h 文件实现了以太网中 ARP 协议的全部数据结构,ARP 协议实现过程中有两个重要的数据结构,即 ARP 缓存表和 ARP 报文。
2.1、ARP表
ARP协议的实质就是对缓存表的建立、更新、查询等操作。ARP 缓存表由缓存表项(entry)组成,每个表项记录了一组 IP 地址和 MAC 地址绑定信息,还包含了与数据包发送控制、缓存表项管理相关的状态、控制信息。LwIP中描述缓存表项的数据结构叫 etharp_entry,如下所示:
structetharp_entry
{
struct etharp_q_entry *q; //数据包缓冲队列指针
struct ip_addr ipaddr; //目标 IP 地址
struct eth_addr ethaddr; //MAC 地址
enum etharp_state state; //描述该 entry 的状态
u8_t
ctime; //描述该 entry 的时间信息
struct netif *netif; //对应网络接口信息
};
描述缓冲队列的数据结构叫做 etharp_q_entry,该结构的定义如下:
structetharp_q_entry
{
structetharp_q_entry *next; //指向下一个缓冲数据包
struct pbuf
*p; //指向数据包 pbuf
};
用一个图来看看 etharp_q_entry 结构在缓存表数据队列中的作用,如图所示:
[if !vml]
[endif]
state 是个枚举类型,它描述该缓存表项的状态,LwIP 中定义一个缓存表项可能有三种不同的状态,用枚举型 etharp_state 进行描述。
enumetharp_state
{
ETHARP_STATE_EMPTY
= 0, //empty 状态
ETHARP_STATE_PENDING, //pending 状态
ETHARP_STATE_STABLE //stable 状态
};
编译器为ARP表预先定义了ARP_TABLE_SIZE(10)个表项空间,因此ARP缓存表内部最多只能存放ARP_TABLE_SIZE条IP 地址与MAC地址配对信息。
static struct etharp_entry
arp_table[ARP_TABLE_SIZE]; //定义 ARP 缓存表
ETHARP_STATE_EMPTY 状态:初始化的时候为empty状态。
ETHARP_STATE_PENDING状态:表示该表项处于不稳定状态,此时该表项只记录到了IP 地址,但是还未记录到对应的MAC地址。 很可能的情况是,LwIP 内核已经发出一个关于该 IP地址的 ARP 请求到数据链路上,但是还未收到 ARP应答。
ETHARP_STATE_STABLE 状态:当 ARP表项被更新后,它就记录了一对完整的IP 地址和MAC地址。
在ETHARP_STATE_PENDING 状态下会设定超时时间(10秒),当计数超时后,对应的表项将被删除;在ETHARP_STATE_STABLE状态下也会设定超时时间(20分钟),当计数超时后,对应的表项将被删除。
网络接口结构指针 netif,该结构中包含了网络接口的 MAC地址和IP地址等信息,在发送数据包的时候,这些信息都起着至关重要的作用。
ctime为每个表项的计数器,周期性的去调用一个etharp_tmr函数,这个函数以5秒为周期被调用,在这个函数中,它会将每个ARP 缓存表项的 ctime 字段值加 1,当相应表项的生存时间计数值 ctime 大于系统规定的某个值时,系统将删除对应的表项。
//稳定状态表项的最大生存时间计数值:240*5s=20min
#defineARP_MAXAGE 240
//PENDING状态表项的最大生存时间计数值:2*5s=10s
#defineARP_MAXPENDING 2
void etharp_tmr(void)
{
u8_t
i;
for (i = 0; i
< ARP_TABLE_SIZE; ++i) //对每个表项操作,包括空闲状态的表项{
arp_table[i].ctime++; //先将表项 ctime 值加1
//如果表项是 stable 状态,且生存值大于 ARP_MAXAGE,
//或者是 pending 状态且其生存值大于 ARP_MAXPENDING,则删除表项
if(((arp_table[i].state == ETHARP_STATE_STABLE) && //stable 状态
(arp_table[i].ctime >= ARP_MAXAGE))||
((arp_table[i].state == ETHARP_STATE_PENDING) && //pending 状态
(arp_table[i].ctime >= ARP_MAXPENDING)) )
{
if(arp_table[i].q != NULL) //如果表项上的数据队列中有数据{
free_etharp_q(arp_table[i].q); //则释放队列中的所有数据
arp_table[i].q = NULL; //队列设置为空
}
arp_table[i].state = ETHARP_STATE_EMPTY; //将表项状态改为未用
}//if
}//for
}
2.2、ARP报文
ARP 请求和 ARP 应答,它们都是被组装在一个 ARP 数据包中发送的,一个典型的 ARP 包的组成结构如图所示:
[if !vml]
[endif]
以太网目的地址和以太网源地址:分别表示以太网目的MAC地址和源MAC地址,目的地址全1时是特殊地址以太网广播地址。在 ARP 表项建立前,源主机只知道目的主机的 IP 地址,并不知道其 MAC 地址,所以在数据链路上,源主机只有通过广播的方式将 ARP请求数据包发送出去,同一网段上的所有以太网接口都会接收到广播的数据包。
桢类型:ARP-0x0806、IP-0x0800、PPPoE-0x8864。
硬件类型:表示发送方想要知道的硬件类型。
协议类型:表示要映射的协议地址类型,0x0800-表示要映射为IP地址 。
硬件地址长度和协议地址长度:以太网ARP请求和应答分别为6和4,代表MAC地址长度和IP地址长度。
op:指出ARP数据包的类型,ARP请求(1),ARP应答(2)。
在以太网的数据帧头部中和 ARP 数据包中都有发送端的以太网MAC 地址。对于一个 ARP 请求包来说,除接收方以太网地址外的所有字段都应该被填充相应的值。当接收方主机收到一份给自己的 ARP 请求报文后,它就把自己的硬件地址填进去,然后将该请求数据包的源主机信息和目的主机信息交换位置,并把操作字段 op 置为 2,最后把该新构建的数据包发送回去,这就是 ARP 应答。
在 ARP 中用了一大堆的数据结构和宏来描述上图的结构。
#ifndefETHARP_HWADDR_LEN
#define ETHARP_HWADDR_LEN 6 //以太网物理地址长度
#endif
PACK_STRUCT_BEGIN//我们移植时实现的结构体封装宏
structeth_addr
{
//定义以太网 MAC 地址结构体 eth_addr,禁止编译器自对齐
PACK_STRUCT_FIELD(u8_t
addr[ETHARP_HWADDR_LEN]);
}
PACK_STRUCT_STRUCT;
PACK_STRUCT_END
PACK_STRUCT_BEGIN//定义以太网数据帧首部结构体 eth_hdr,禁止编译器自对齐
structeth_hdr
{
PACK_STRUCT_FIELD(struct eth_addr dest); //以太网目的地址(6 字节)
PACK_STRUCT_FIELD(struct eth_addr src); //以太网源地址(6 字节)
PACK_STRUCT_FIELD(u16_t
type); //帧类型(2 字节)
}
PACK_STRUCT_STRUCT;
PACK_STRUCT_END
//定义以太网帧头部长度宏,其中 ETH_PAD_SIZE 已定义为 0
#defineSIZEOF_ETH_HDR (14 + ETH_PAD_SIZE)
PACK_STRUCT_BEGIN//定义 ARP 数据包结构体 etharp_hdr,禁止编译器自对齐
structetharp_hdr
{
PACK_STRUCT_FIELD(u16_t
hwtype); //硬件类型(2 字节)
PACK_STRUCT_FIELD(u16_t
proto); //协议类型(2 字节)
PACK_STRUCT_FIELD(u16_t
_hwlen_protolen); //硬件+协议地址长度(2 字节)
PACK_STRUCT_FIELD(u16_t
opcode); //操作字段 op(2 字节)
PACK_STRUCT_FIELD(struct eth_addr shwaddr); //发送方 MAC 地址(6 字节)
PACK_STRUCT_FIELD(struct ip_addr2 sipaddr); //发送方 IP 地址(4 字节)
PACK_STRUCT_FIELD(struct eth_addr dhwaddr); //接收方 MAC 地址(6 字节)
PACK_STRUCT_FIELD(struct ip_addr2 dipaddr); //接收方 IP 地址(4 字节)
}
PACK_STRUCT_STRUCT;
PACK_STRUCT_END
#define SIZEOF_ETHARP_HDR 28 //宏,ARP 数据包长度
//宏,包含 ARP 数据包的以太网帧长度
#defineSIZEOF_ETHARP_PACKET (SIZEOF_ETH_HDR +SIZEOF_ETHARP_HDR)
#define ARP_TMR_INTERVAL 5000 //定义 ARP 定时器周期为 5 秒,不同帧类型的宏定义
#defineETHTYPE_ARP 0x0806
#defineETHTYPE_IP 0x0800
//ARP 数据包中 OP 字段取值宏定义
#define ARP_REQUEST 1 //ARP 请求
#defineARP_REPLY 2 //ARP 应答
发送 ARP 请求数据包的函数叫 etharp_request,它通过调用 etharp_raw 函数来实现,调用后者时,需要为它提供 ARP数据包中各个字段的值,后者直接将各个字段的值填写到在一个 ARP 包中发送(该函数并不知道发送的是 ARP 请求还是 ARP 响应,它只管组装并发送,所以称之为 raw)
//函数功能:根据各个参数字段组织一个 ARP 数据包并发送
//参数 netif:发送 ARP 包的网络接口结构
//参数 ethsrc_addr:以太网帧首部中的以太网源地址值
//参数 ethdst_addr:以太网帧首部中的以太网目的地址值
//参数hwsrc_addr:ARP 数据包中的发送方 MAC 地址
//参数 ipsrc_addr:ARP 数据包中的发送方 IP 地址
//参数 hwdst_addr:ARP 数据包中的接收方 MAC 地址
//参数 ipdst_addr:ARP 数据包中的接收方 IP 地址
//参数 opcode:ARP 数据包中的 OP 字段值,请求ARP为1,应答ARP为2
//注:ARP 数据包中其他字段使用预定义值,例如硬件地址长度为 6,协议地址长度为 4
err_t
etharp_raw(struct netif *netif, const structeth_addr *ethsrc_addr,
const struct eth_addr *ethdst_addr, const structeth_addr *hwsrc_addr,
const struct ip_addr *ipsrc_addr, const structeth_addr *hwdst_addr,
const struct ip_addr *ipdst_addr, constu16_t opcode)
{
struct pbuf *p; //数据包指针
err_t
result = ERR_OK; //返回结果
u8_t
k;
struct eth_hdr *ethhdr; //以太网数据帧首部结构体指针
struct etharp_hdr *hdr; // ARP 数据包结构体指针
//先在内存堆中为 ARP 包分配空间,大小为包含 ARP 数据包的以太网帧总大小
p
= pbuf_alloc(PBUF_RAW, SIZEOF_ETHARP_PACKET, PBUF_RAM);
if(p == NULL) //若分配失败则返回内存错误{
return ERR_MEM;
}
//到这里,内存分配成功
ethhdr
= p>payload; // ethhdr 指向以太网帧首部区域
hdr
= (struct etharp_hdr *)((u8_t*)ethhdr +
SIZEOF_ETH_HDR);// hdr 指向 ARP 首部
hdr>opcode
= htons(opcode); //填写 ARP 包的 OP 字段,注意大小端转换
k
= ETHARP_HWADDR_LEN; //循环填写数据包中各个 MAC 地址字段
while(k > 0)
{
k--;
hdr>shwaddr.addr[k]
= hwsrc_addr>addr[k]; //ARP 头部的发送方 MAC 地址
hdr>dhwaddr.addr[k]
= hwdst_addr>addr[k]; //ARP 头部的接收方 MAC 地址
ethhdr>dest.addr[k]
= ethdst_addr>addr[k]; //以太网帧首部中的目的地址
ethhdr>src.addr[k]
= ethsrc_addr>addr[k]; //以太网帧首部中的以太网源地址
}
hdr>sipaddr
= *(struct ip_addr2 *)ipsrc_addr; //填写 ARP 头部发送方 IP 地址
hdr>dipaddr
= *(struct ip_addr2 *)ipdst_addr; //填写 ARP 头部接收方 IP 地址
//下面填充一些固定字段的值
hdr>hwtype
= htons(HWTYPE_ETHERNET); //ARP 头部的硬件类型为 1,即以太网
hdr>proto
= htons(ETHTYPE_IP); //ARP 头部的协议类型为0x0800
//设置两个长度字段
hdr>_hwlen_protolen=htons((ETHARP_HWADDR_LEN<<8)| sizeof(struct ip_addr));
ethhdr>type
= htons(ETHTYPE_ARP); //以太网帧首部中的帧类型字段,ARP 包
result
= netif>linkoutput(netif, p); //调用底层数据包发送函数
pbuf_free(p); //释放数据包
p
= NULL;
return result; //返回发送结果
}
//特殊 MAC 地址的定义,以太网广播地址
const struct eth_addr ethbroadcast = {{0xff,0xff,0xff,0xff,0xff,0xff}};
//该值用于填充 ARP 请求包的接收方MAC 字段,无实际意义
const struct eth_addr ethzero = {{0,0,0,0,0,0}};
//函数功能:发送 ARP 请求
//参数 netif:发送 ARP 请求包的接口结构
//参数 ipaddr:请求具有该 IP 地址主机的 MAC
err_t
etharp_request(struct netif *netif, structip_addr *ipaddr)
{
//该函数只是简单的调用函数etharp_raw,为函数提供所有相关参数
return etharp_raw(netif, (structeth_addr *)netif>hwaddr,ðbroadcast,
(struct eth_addr *)netif>hwaddr,&netif>ip_addr,ðzero,ipaddr,ARP_REQUEST);
}