第十节 ARP 协议

链路层概述

我们首先学习一些有用的术语。在本章中为方便讨论,将运行链路层协议(即第2 层)协议的任何设备均称为结点(node)。结点包括主机、路由器、交换机和WiFi 接入点。我们也把沿着通信路径连接相邻结点的通信信道称为链路(link)。为了将一个数据报从源主机传输到目标主机,数据报必须通过端到端路径上的各段链路才能到达目标主机。在通过特定的链路时,传输结点将数据报封装在链路层帧中,并将该帧传送到链路中。

链路层的主体部分是网卡中实现的,在发送的数据帧的时候,协议栈取得由高层传递过的数据报(注意,数据帧与数据报不是一个东西,数据帧一般形容链路层的数据,是一帧一帧的,也是链路层的服务——数据成帧),在链路层中封装该数据报,也就是填写数据帧的各个字段,然后遵循链路接入协议将该帧数据进行传输;在接收端,控制器接收了整个数据帧,抽取出网络层的数据报,往上层传递。

其实这些知识过于底层,我们暂时也无需了解太多,只需要知道所有不同主机的信息都是必须通过链路层才能传递数据,所有链路层的完整与稳定是传递信息的必要条件。

MAC 地址的基本概念

MAC Address(Media Access Control Address),亦称为EHA(Ethernet Hardware Address)、硬件地址、物理地址(Physical Address)。MAC 地址就是在媒体接入层上使用的地址,也叫物理地址、硬件地址或链路地址,其被固化在网卡的ROM 中。可见MAC 地址实际上就是网卡地址或网卡标识符。当某台计算机使用某块网卡后,网卡上的标识符就成为该计算机的MAC 地址。MAC 地址长度为6 字节(48 比特),其前3 个字节表示组织唯一标志符(Organizationally Unique Identifier,即OUI),由IEEE 的注册管理机构给不同厂家分配的代码,以区分不同的厂家,后3 个字节由厂家自行分配,称为扩展标识符。同一个厂家生产的网卡中MAC 地址后24 位是不同的。

初识ARP

地址解析协议(Address Resolution Protocol,ARP)是通过解析IP 地址得到数据链路层地址的,是一个在网络协议包中极其重要的网络传输协议,它与网卡有着极其密切的关系,在TCP/IP 分层结构中,把ARP 划分为网络层,为什么呢,因为在网络层看来,源主机与目标主机是通过IP 地址进行识别的,而所有的数据传输又依赖网卡底层硬件,即链路层,那么就需要将这些IP 地址转换为链路层可以识别的东西,在所有的链路中都有着自己的一套寻址机制,如在以太网中使用MAC 地址进行寻址,标识不同的主机,那么就需要有一个协议将IP 地址转换为MAC 地址,由此就出现了ARP 协议,ARP 协议在网络层被应用,它是网络层与链路层连接的重要枢纽。

在局域网中,网络中实际传输的是“帧”,帧里面是有目标主机的MAC 地址的。在以太网中,一个主机要和另一个主机进行直接通信,必须要知道目标主机的MAC 地址,那就需要ARP 进行地址解析,所谓“地址解析”就是主机在发送帧前将目标IP 地址转换成目标MAC 地址的过程。ARP 协议的基本功能就是通过目标设备的IP 地址,查询目标设备的MAC 地址,以保证通信的顺利进行。

以太网帧结构

既然谈到MAC 地址,那就不得不说一下以太网帧结构了,每个网卡都有唯一一个物理地址,在硬件中进行数据帧传输的时候就必须有正确的目的物理地址,例如以太网的48 位MAC 地址就是存储在网卡内部存储器中。

以太网帧结构如图10_1 所示:

在这里插入图片描述

图 以太网帧结构

一看这个以太网帧结构,就觉得它有7 个字段,但是事实上,前同步码与帧开始符字段不能算是真正的以太网帧数据结构,他们是在网卡发送的时候添加进去的,为了数据的准确传输。

以太网帧以一个7 字节的前同步码(Preamble)字段开始。该前同步码的值都是10101010(0x55,大端模式);而后紧接着一个字节的帧开始符,其值是10101011(0xD5,大端模式)。前同步码字段的作用是实现物理层帧输入输出的同步,而帧开始符表示着以太网帧的开始,剩下的5 个字段才是真正的以太网数据帧结构。

目标MAC 地址(6 字节):这个字段包含目标网卡的MAC 地址,当一个网卡收到一个以太网数据帧,如果该数据帧的目标地址是网卡自身的MAC 地址或者是MAC 广播地址,它都将该帧的数据字段的内容传递给网络层;如果它收到了具有任何其他MAC 地址的帧,则将该数据帧丢弃。

源MAC 地址(6 字节):这个字段包含了传输该帧到局域网上的适配器的MAC 地址。

类型字段(2 字节):类型字段允许以太网复用多种网络层协议。为了理解这点,我们需要记住主机能够使用除了IP 以外的其他网络层协议。事实上,一台给定的主机可以支持多种网络层协议,以对不同的应用采用不同的协议。因此,当以太网帧到达网卡中,网卡需要知道它应该将数据字段的内容传递给哪个网络层协议。如IP 协议、ARP 协议等。

注意了:当这个字段的值小于1518 时,它表示后面数据字段的数据长度,当大于1518 的时候才表示递交给哪个协议。

数据字段(46~1500 字节):这个字段承载了IP 数据报。以太网的最大传输单元(MTU)是1500字节。这意味着如果IP 数据报超过了1500 字节,则主机必须将该数据报分片(关于分片会在后续讲解)。数据字段的最小长度是46 字节,这意味着如果IP 数据报小于46 字节,数据报必须被填充到46 字节。当采用填充时,传递到网络层的数据包括IP 数据报和填充部分,网络层使用IP数据报首部中的长度字段来去除填充部分。

CRC(4 字节):CRC 字段包含了以太网的差错校验信息。

在以太网帧中,目标MAC 地址可以分成三类,单播地址、多播地址和广播地址。单播地址通常是与某个网卡的MAC 地址对应,它要求以太网第一个字节的bit0(最先发出去的位)必须是0;而多播地址则要求第一个字节的bit0 为1,这样子多播地址就不会与任何网卡的MAC 地址相同,可以被多个网卡同时接收;广播地址的48 位MAC 地址全为1,也就是FF-FF-FF-FF-FF-FF,同一局域网内的所有网卡都会收到广播的数据包。

所有的以太网计算都向网络层提供不可靠的无连接服务,也就是说在网卡发送数据的时候,不会向目标网卡进行事先的通知(握手),网卡只在以太网帧中封装好来自上层的数据报,然后把数据报发送到局域网上。同样的当一个网卡接收一个以太网数据帧的时候,它也不会进行回复确认,如果当网卡执行CRC 校验不通过的时候,它也不会发送否定确认,这样子当一些以太网帧不通过CRC 校验时,网卡只是将其丢弃,而发送的一方就不会知道它传输的数据是否达到并且通过校验。

IP 地址映射为物理地址

TCP/IP 协议有自己的IP 地址,IP 地址(IPv4)是一个32 位的IP 地址,网络层发送数据包只需要知道目标主机IP 地址即可,而以太网发送数据则必须知道对方的硬件MAC 地址,同时IP 地址的分配与硬件MAC 地址是没有关系的,为了让网络层只需要知道IP 地址就可以完成通信工作,那就需要有一个协议将IP 地址映射成为对应的MAC 地址,此外还需要考虑IP 地址可能是动态的,非常灵活,使用静态的映射方法是行不通的,所以ARP 协议就提供优质的地址动态解析的机制,让32 位的IP 地址能映射成为48 位的MAC 地址,让上层应用于底层完全分离开,这样子在上层应用就能灵活使用IP 地址作为标识,进行通信。

ARP 缓存表

既然已经解释了ARP 的用途,我们再来看看它是如何工作的。为了实现IP 地址与网卡MAC 地址的查询与转换,ARP 协议引入了ARP 缓存表的概念,每台主机或路由器在其内存中具有一个ARP 缓存表(ARP table),这张表包含IP 地址到MAC 地址的映射关系,表中记录了<IP 地址,MAC 地址> 对,它们是主机最近运行时获得关于其他主机的IP 地址到物理地址的映射,当需要发送IP 数据的时候,主机就会根据目标IP 地址到ARP 缓存表中进行查找对应的MAC 地址,然后通过网卡将数据发送出去。ARP 表也包含一个寿命(TTL)值,它指示了从表中删除每个映射的时间。从一个表项放置到某ARP 表中开始,一个表项通常的过期时间是10 分钟。

我们电脑也是有自己的ARP 缓存表的,可以在控制台中通过“arp -a”命令进行查看,具体见图。

在这里插入图片描述

图ARP 缓存表

从图中可以看到作者的电脑有很多这样子的缓存表,其中192.168.0.181 是我电脑的IP 地址,而192.168.0.xxx 这些IP 是公司的其他电脑与其对应的物理地址(MAC 地址),比如IP 地址为192.168.0.108 的电脑MAC 地址为dc-72-9b-cf-0c-e5,如果我想向该电脑发送一个数据包,那么我的电脑就会从已有的ARP 缓存表中寻找这个IP 地址对应的物理地址,然后直接将数据写入以太网数据帧中让网卡进行发送即可,而如果没有找到这个IP 地址,那么这个数据就没法立即发送,电脑会先在局域网上广播一个ARP 请求(目标MAC 地址为FF-FF-FF-FF-FF-FF),广播的ARP 请求发出后,处于同一局域网内的所有主机都会接收到这个请求,如果目标IP 地址与主机自身IP 地址吻合就会返回一个ARP 应答,告诉请求者自身的MAC 地址,当我的电脑收到这个ARP 应答后,就去更新ARP 缓存表,并且重新将数据发送出去。

ARP 协议的核心就是对缓存表的操作,发送数据包的时候,查找ARP 缓存表以得到对应的MAC地址,必要时进行ARP 缓存表的更新,此外ARP 还需要不断处理其他主机的ARP 请求,在ARP缓存表中的TTL 即将过期的时候更新缓存表以保证缓存表中的表项有效。

其运作过程大致可以理解为:

  1. 如果主机A 想发送数据给主机B,主机A 首先会检查自己的ARP 缓存表,查看是否有主机B的IP 地址和MAC 地址的对应关系,如果有,则会将主机B 的MAC 地址作为源MAC 地址封装到数据帧中。如果本地ARP 缓存中没有对应关系,主机A 就会向局域网中广播ARP 请求(包括发送方的IP 地址、MAC 地址、接收方的IP 地址),每台主机接收到ARP 请求后都检查自己的IP 地址是否与ARP 请求中的接收方IP 地址相同,若不相同则丢弃ARP 请求包。

  2. 当交换机接受到此数据帧之后,发现此数据帧是广播帧,因此,会将此数据帧从非接收的所有接口发送出去。

  3. 当主机B 接受到此数据帧后,会校对IP 地址是否是自己的,并将主机A 的IP 地址和MAC 地址的对应关系记录到自己的ARP 缓存表中,同时会发送一个ARP 响应,其中包括自己的MAC地址。

  4. 主机A 在收到这个回应的数据帧之后,在自己的ARP 缓存表中记录主机B 的IP 地址和MAC地址的对应关系。而此时交换机已经学习到了主机A 和主机B 的MAC 地址了。

那么在LwIP 中缓存表是如何实现的呢?下面我们就结合源码进行讲解。

ARP 协议的核心是ARP 缓存表,ARP 的实质就是对缓存表的建立、更新、查询等操作,ARP 缓存表的核心是表项(entry)。LwIP 使用一个arp_table 数组描述ARP 缓存表,数组的内容是表项的内容,具体见代码清单。每个表项都必须记录一对IP 地址与MAC 地址的映射关系,此外还有一些基本的信息,如表项的状态、生命周期(生存时间)以及对应网卡的基本信息,LwIP使用一个etharp_entry 结构体对表项进行描述,具体见代码清单。

代码清单 ARP 缓存表

static struct etharp_entry arp_table[ARP_TABLE_SIZE];

编译器预先定义了缓存表的大小,ARP_TABLE_SIZE 默认为10,也就是最大能存放10 个表项,由于这个表很小,LwIP 对表的操作直接采用遍历方式,遍历每个表项并且更改其中的内容。

代码清单 etharp_entry 结构体

struct etharp_entry
{
#if ARP_QUEUEING
	/** 指向此ARP 表项上挂起的数据包队列的指针。*/
	struct etharp_q_entry *q; //(1)-
#else
	/** 指向此ARP 表项上的单个挂起数据包的指针。*/
	struct pbuf *q; //(1)-
#endif
	ip4_addr_t ipaddr; (2)
	struct netif *netif; (3)
	struct eth_addr ethaddr; (4)
	u16_t ctime; (5)
	u8_t state; (6)
};

代码清单(1):这里使用了一个宏定义进行预编译,默认配置该宏定义是不打开的,其实都是一样,q 都是指向数据包,(1)-􀀀 中的etharp_q_entry 指向的是数据包缓存队列,etharp_q_entry 是一个结构体,具体见代码清单,如果将ARP_QUEUEING 宏定义打开,系统会为etharp_q_entry结构体开辟一些MEMP_ARP_QUEUEING 类型的内存池,以便快速申请内存。而(1)-􀀀 中的q 直接指向单个数据包。具体见图。

在这里插入图片描述
ARP 表项上的单个挂起数据包

在这里插入图片描述
ARP 表项上挂起的数据包队列

代码清单 etharp_q_entry 结构体

struct etharp_q_entry
{
	struct etharp_q_entry *next;
	struct pbuf *p;
};
  • 代码清单(2):记录目标IP 地址。
  • 代码清单(3):对应网卡信息。
  • 代码清单(4):记录与目标IP 地址对应的MAC 地址。
  • 代码清单(5):生存时间。
  • 代码清单(6):表项的状态,LwIP 中用枚举类型定义了不同的状态,具体见代码清单。

代码清单 表项的状态

/** ARP states */
enum etharp_state
{
	ETHARP_STATE_EMPTY = 0,
	ETHARP_STATE_PENDING,
	ETHARP_STATE_STABLE,
	ETHARP_STATE_STABLE_REREQUESTING_1,
	ETHARP_STATE_STABLE_REREQUESTING_2
#if ETHARP_SUPPORT_STATIC_ENTRIES
	, ETHARP_STATE_STATIC
#endif /* ETHARP_SUPPORT_STATIC_ENTRIES */
};

ARP 缓存表在初始化的时候,所有的表项都会被初始化为ETHARP_STATE_EMPTY,也就是空状态,表示这些表项能被使用,在需要添加表项的时候,LwIP 内核就会遍历ARP 缓存表,找到合适的表项,进行添加。如果ARP 表项处于ETHARP_STATE_PENDING 状态,表示ARP 已经发出了一个ARP 请求包,但是还未收到目标IP 地址主机的应答,处于这个状态的缓存表项是有等待时间的,它通过宏定义ARP_MAXPENDING 指定,默认为5 秒钟,如果从发出ARP 请求包后的5 秒内还没收到应答,那么该表项又会被删除;而如果收到应答后,ARP 就会更新缓存表的信息,记录目标IP 地址与目标MAC 地址的映射关系并且开始记录表项的生存时间,同时该表项的状态会变成ETHARP_STATE_STABLE 状态。当要发送数据包的时候,而此时表项为ETHARP_STATE_PENDING 状态,那么这些数据包就会暂时被挂载到表项的数据包缓冲队列上,直到表项的状态为ETHARP_STATE_STABLE,才进行发送数据包。对于状态为ETHARP_STATE_STABLE 的表项,这些表项代表着ARP 记录了IP 地址与MAC 地址的映射关系,能随意通过IP 地址进行数据的发送,但是这些表项是具有生存时间的,通过宏定义ARP_MAXAGE 指定,默认为5 分钟,在这些时间,LwIP 会不断维护这些缓存表以保持缓存表的有效。当表项是ETHARP_STATE_STABLE 的时候又发送一个ARP 请求包,那么表项状态会暂时被设置为ETHARP_STATE_STABLE_REREQUESTING_1,然后被设置为ETHARP_STATE_STABLE_REREQUESTING_2 状态,这些是一个过渡状态,当收到ARP 应答后,表项又会被设置为ETHARP_STATE_STABLE,这样子能保持表项的有效。

所以ARP 缓存表是一个动态更新的过程,为什么要动态更新呢?因为以太网的物理性质并不能保证数据传输的是可靠的。以太网发送数据并不会知道对方是否已经介绍成功,而两台主机的物理线路不可能一直保持有效畅通,那么如果不是动态更新的话,主机就不会知道另一台主机是否在工作中,这样子发出去的数据是没有意义的。比如两台主机A 和B,一开始两台主机都是处于连接状态,能正常进行通信,但是某个时刻主机B 断开了,但是主机A 不会知道主机B 是否正常运行,因为以太网不会提示主机B 已经断开,那么主机A 会一直按照MAC 地址发送数据,而此时在物理链路层就已经是不通的,那么这些数据是没有意义的,而如果ARP 动态更新的话,主机A 就会发出ARP 请求包,如果得不到主机B 的回应,则说明无法与主机B 进行通信,那么就会删除ARP 表项,就无法进行通信。

ARP 缓存表的超时处理

从前面的章节也知道,ARP 是动态处理的,现在总结一下:ARP 表项的生存时间是5 分钟,而ARP 请求的等待时间是5 秒钟,当这些时间到达后,就会更新ARP 表项,如果在物理链路层无法连通则会删除表项。这就需要ARP 层有一个超时处理函数对ARP 进行管理,这些操作都是根据ARP 表项的ctime 字段进行的,它记录着对应表项的生存时间,而超时处理函数是etharp_tmr(),它是一个周期性的超时处理函数,每隔1 秒就调用一次,当ctime 的值大于指定的时间,就会删除对应的表项,具体见代码清单。

代码清单 etharp_tmr() 源码

void
etharp_tmr(void)
{
	int i;
	
	LWIP_DEBUGF(ETHARP_DEBUG, ("etharp_timer\n"));
	/* 遍历ARP 表,从ARP 表中删除过期的表项*/
	for (i = 0; i < ARP_TABLE_SIZE; ++i) (1)
	{
		u8_t state = arp_table[i].state;
		if (state != ETHARP_STATE_EMPTY
#if ETHARP_SUPPORT_STATIC_ENTRIES
				&& (state != ETHARP_STATE_STATIC)
#endif /* ETHARP_SUPPORT_STATIC_ENTRIES */
		)
		{
			arp_table[i].ctime++; (2)
			if ((arp_table[i].ctime >= ARP_MAXAGE) ||
					((arp_table[i].state == ETHARP_STATE_PENDING) &&
					(arp_table[i].ctime >= ARP_MAXPENDING))) (3)
			{
				/* 等待表项稳定或者表项已经过期*/
			LWIP_DEBUGF(ETHARP_DEBUG,("etharp_timer: expired %s entry %d.\n",
				arp_table[i].state >= ETHARP_STATE_STABLE ? "stable" : "pending", i));
					/* 从ARP 表中删除过期的表项*/
					etharp_free_entry(i); (4)
			}
			else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_1)
			{
				/* 过渡*/
				arp_table[i].state = ETHARP_STATE_STABLE_REREQUESTING_2;
			}
			else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_2)
			{
				/* 进入ETHARP_STATE_STABLE 状态*/
				arp_table[i].state = ETHARP_STATE_STABLE;
			}
			else if (arp_table[i].state == ETHARP_STATE_PENDING)
			{
				/* 仍然挂起,重新发送ARP 请求*/
				etharp_request(arp_table[i].netif, &arp_table[i].ipaddr);
			}
		}
	}
}
  • 代码清单(1):由于LwIP 的ARP 表是比较小的,直接遍历表即可,更新ARP 表的内容。
  • 代码清单(2):如果ARP 表项不是空的,那么就记录表项的时间。
  • 代码清单(3)(4): 当表项的时间大于表项的生存时间(5 分钟) , 或者表项状态是ETHARP_STATE_PENDING 处于等待目标主机回应ARP 请求包,并且等待的时间超过ARP_MAXPENDING(5 秒),那么LwIP 就认为这些表项是无效了,就调用etharp_free_entry() 函数删除表项。

ARP 报文

ARP 的请求与应答都是依赖ARP 报文结构进行的,ARP 报文是放在以太网数据帧中进行发的,所以下图会将以太网首部一同画出来,具体见图。
在这里插入图片描述
ARP 报文

在ARP 表建立前,主机并不知道目标MAC 地址,所以在一开始的时候只能通过广播的方式将ARP 请求包发送出去,处于同一局域网的主机都能接收到广播的数据包。所以一开始目标MAC地址是FF-FF-FF-FF-FF-FF,而以太网首部的帧类型是有多种,对于ARP 数据包来说,其值为0x0806,对于IP 数据报来说,其值为0x0800,此处我们只需简单了解一下即可,无需记住。

接下来就是ARP 报文部分,ARP 也是一种协议,也有ARP 首部,在ARP 首部一开始的2 个字节存储的是硬件类型,表示要知道目标网卡的硬件类型,其中,值为1 表示以太网地址,其他还可能表示令牌环地址;接下来还有2 字节的协议类型,表示硬件地址要映射的协议地址类型,其中,0x0800 表示IP 地址,其他还可能是ICMP/IGMP 等;接下来有1 个字节表示硬件地址长度,指出该报文中硬件地址的长度,对于以太网,它的值为6;还有1 字节的协议地址长度,对于ARP请求或应答来说,该值为4;ARP 首部最后的op 字段用于记录ARP 操作的类型,分别是:

  • ARP 请求,其值为1。
  • ARP 应答,其值为2。
  • RARP 请求,其值为3。
  • RARP 应答,其值为4。

我们只关心ARP 的请求与应答即可,RARP 是逆地址解析协议,在这里我们就不用去了解,它在网络中基本已经被淘汰,用于主机在启动的时候获得自己的IP 地址。

对于ARP 首部后面的四个字段分别是源MAC 地址、源IP 地址、目标MAC 地址、目标IP 地址,这些就是比较简单的了。

在ARP 请求包中,除了目标MAC 地址是未知以外,其他地址3 个字段都应该填写正确,然后通过广播的形式将该ARP 请求包发送出去,目标主机接收到该请求包后判断目标IP 地址与自身IP 地址是否一致,如果一致则返回ARP 应答;对应ARP 应答包,只需要把自己的MAC 地址填充进去,并且请求包的源主机信息与目标主机信息进行交换位置,然后把op 字段设置为2,就返回ARP 应答包即可。

注意,在发送ARP 请求包的时候,以太网首部的目标MAC 地址是FF-FF-FF-FF-FF-FF,而ARP首部目标MAC 地址为00-00-00-00-00-00-00,这里千万不要混淆。

在LwIP 中,使用了大量的数据结构对ARP 进行描述,比较麻烦,我们暂时不用去学它,只要知道原理是这样子的即可,关于这些数据结构的定义位于etharp.h 、ethernet.h 等头文件中,具体见代码清单。

代码清单 与ARP 报文相关的数据结构(已删减)

#define ETH_HWADDR_LEN 6 //以太网地址长度
truct eth_addr //以太网地址结构体
{
	PACK_STRUCT_FLD_8(u8_t addr[ETH_HWADDR_LEN]);
} PACK_STRUCT_STRUCT;

struct eth_hdr //以太网首部
{
	PACK_STRUCT_FLD_S(struct eth_addr dest); //以太网目标MAC 地址
	PACK_STRUCT_FLD_S(struct eth_addr src); //以太网源MAC 地址
	PACK_STRUCT_FIELD(u16_t type); //帧类型
} PACK_STRUCT_STRUCT;

struct etharp_hdr //ARP 报文
{
	PACK_STRUCT_FIELD(u16_t hwtype); //硬件类型
	PACK_STRUCT_FIELD(u16_t proto); //协议类型
	PACK_STRUCT_FLD_8(u8_t hwlen); //硬件地址长度
	PACK_STRUCT_FLD_8(u8_t protolen); //协议地址长度
	PACK_STRUCT_FIELD(u16_t opcode); //op 字段
	/* 以上是ARP 报文首部*/
	
	PACK_STRUCT_FLD_S(struct eth_addr shwaddr); //源MAC 地址
	PACK_STRUCT_FLD_S(struct ip4_addr_wordaligned sipaddr);//源ip 地址
	PACK_STRUCT_FLD_S(struct eth_addr dhwaddr); //目标MAC 地址
	PACK_STRUCT_FLD_S(struct ip4_addr_wordaligned dipaddr);//目标ip 地址
} PACK_STRUCT_STRUCT;

enum etharp_opcode //op 字段操作
{
	ARP_REQUEST = 1, //请求包
	ARP_REPLY = 2 //应答包
};

为了加深理解,我们使用wireshark 网络抓包工具形象地讲解报文格式与内容,关于wireshark 网络抓包工具的使用方式我们就不做过多讲解,打开工具,然后抓取电脑网络中的数据包,具体见图。

在这里插入图片描述
图 抓包界面

然后我们找到ARP 协议,双击进行查看ARP 协议中的数据包,然后我们可以看到第一个ARP协议是一个请求包,而第二个ARP 协议是一个应答包,具体见图 。

ARP 请求包(op 字段为1)
ARP 请求包(op 字段为1)

在这里插入图片描述

ARP 应答包(op 字段为2)

发送ARP 请求包

发送ARP 请求包的时候,需要填充已知的目标IP 地址、源MAC 地址、源IP 地址等,并且需要该ARP 包进行广播出去,所以以太网首部的目标MAC 地址为FF-FF-FF-FF-FF-FF,源码具体见代码清单。

代码清单 发送ARP 请求包相关源码

/* 发送原始ARP 数据包(操作码和所有地址都可以修改)
* @param netif 用于发送ARP 数据包的lwip 网络接口
* @param ethsrc_addr 以太网头的源MAC 地址
* @param ethdst_addr 以太网头的目标MAC 地址
* @param hwsrc_addr ARP 协议头的源MAC 地址
* @param ipsrc_addr ARP 协议头的源IP 地址
* @param hwdst_addr ARP 协议头的目标MAC 地址
* @param ipdst_addr ARP 协议头的目标IP 地址
* @param 操作编码ARP 数据包的类型
* @return ERR_OK 如果已发送ARP 数据包
* 如果无法分配ARP 数据包,则为ERR_MEM
*/
static err_t
etharp_raw(struct netif *netif, //用于发送ARP 数据包的lwip 网络接口
		const struct eth_addr *ethsrc_addr,//以太网头的源MAC 地址
		const struct eth_addr *ethdst_addr,//以太网头的目标MAC 地址
		const struct eth_addr *hwsrc_addr,//ARP 协议头的源MAC 地址
		const ip4_addr_t *ipsrc_addr,//ARP 协议头的源IP 地址
		const struct eth_addr *hwdst_addr, //ARP 协议头的目标MAC 地址
		const ip4_addr_t *ipdst_addr,// ARP 协议头的目标IP 地址
		const u16_t opcode)//操作编码ARP 数据包的类型(op 字段)
{
	struct pbuf *p;
	err_t result = ERR_OK;
	struct etharp_hdr *hdr;
	
	//申请ARP 报文的内存空间
	p = pbuf_alloc(PBUF_LINK, SIZEOF_ETHARP_HDR, PBUF_RAM);
	
	if (p == NULL)
	{
		ETHARP_STATS_INC(etharp.memerr);//内存申请失败,返回错误代码
		return ERR_MEM;
	}
	
	//ARP 报文的数据区域,并且强制将起始地址转化成ARP 报文首部
	hdr = (struct etharp_hdr *)p->payload;

	hdr->opcode = lwip_htons(opcode); //填写ARP 数据包的op 字段
	
	//填写源MAC 地址
	SMEMCPY(&hdr->shwaddr, hwsrc_addr, ETH_HWADDR_LEN);
	//填写目标MAC 地址
	SMEMCPY(&hdr->dhwaddr, hwdst_addr, ETH_HWADDR_LEN);
	
	//以太网首部源MAC 地址
	IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(&hdr->sipaddr, ipsrc_addr);
/
	//以太网首部目标MAC 地址
	IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(&hdr->dipaddr, ipdst_addr);
	
	//填写ARP 首部硬件类型
	hdr->hwtype = PP_HTONS(LWIP_IANA_HWTYPE_ETHERNET);
	//填写ARP 首部协议类型
	hdr->proto = PP_HTONS(ETHTYPE_IP);
	
	//填写ARP 数据包硬件地址长度
	hdr->hwlen = ETH_HWADDR_LEN;
	//填写ARP 数据包协议地址长度
	hdr->protolen = sizeof(ip4_addr_t);
	
	//调用底层发送函数将以太网数据帧发送出去
	ethernet_output(netif, p, ethsrc_addr, ethdst_addr, ETHTYPE_ARP);
	
	ETHARP_STATS_INC(etharp.xmit);
	
	pbuf_free(p);//发送完成释放内存
	p = NULL;
	return result; //返回结果
}

//FF-FF-FF-FF-FF-FF
const struct eth_addr ethbroadcast =
			{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}};
//00-00-00-00-00-00
const struct eth_addr ethzero = {{0, 0, 0, 0, 0, 0}};

//发送ARP 请求包,指定目标MAC 地址
static err_t
etharp_request_dst(struct netif *netif,
				const ip4_addr_t *ipaddr,
				const struct eth_addr *hw_dst_addr)
{
	return etharp_raw(netif,
					(struct eth_addr *)netif->hwaddr,
					hw_dst_addr,
					(struct eth_addr *)netif->hwaddr,
					netif_ip4_addr(netif), &ethzero,
					ipaddr, ARP_REQUEST);
}

//发送ARP 请求包,目标MAC 地址为ethbroadcast
err_t
etharp_request(struct netif *netif, const ip4_addr_t *ipaddr)
{
	return etharp_request_dst(netif, ipaddr, &ethbroadcast);
}

总的来说就是先调用etharp_request() 函数进行发送ARP 请求包,在etharp_request() 函数中会调用etharp_request_dst() 函数进行发送,此时指定的目标MAC 地址是ethbroadcast,而在etharp_request_dst() 函数中会调用etharp_raw() 进行发送ARP 请求包,层层调用,并且每层的参数都是越来越多的,这样子封装对于上层程序来说更加好处理,在etharp_raw() 函数中,会对ARP 数据包进行封装,然后再封装到以太网数据帧中,最终调用以太网底层发送函数进行将以太网数据帧发送出去。

数据包接收流程

以太网之数据包接收

以太网是有自己独立的寻址方式(MAC 地址),而对于TCP/IP 的上层协议(如TCP 协议、IP 协议),它们是以IP 地址作为网络的标识,如果没有IP 地址则无法进行收发数据。当数据通过网卡中接收回来的时候,LwIP 内核就需要将数据进行分解,如果是IP 数据报则递交给IP 协议去处理,如果是ARP 数据包则交由ARP 协议去处理。LwIP 中数据包从网卡接收的函数是ethernetif_input(),从第9 章我们可以知道,真正让LwIP 内核去处理接收到的数据包是ethernet_input() 函数,这两个函数是不一样的,名字稍微有点区别,LwIP 就是在这个函数中处理不同的数据包类型,其源码具体见代码清单。

代码清单 ethernet_input() 函数

err_t
ethernet_input(struct pbuf *p, struct netif *netif)
{
	struct eth_hdr *ethhdr;
	u16_t type;
	
#if LWIP_ARP || ETHARP_SUPPORT_VLAN || LWIP_IPV6

	u16_t next_hdr_offset = SIZEOF_ETH_HDR;
	
#endif

	LWIP_ASSERT_CORE_LOCKED();
	
	//校验数据长度
	if (p->len <= SIZEOF_ETH_HDR)
	{
		ETHARP_STATS_INC(etharp.proterr);
		ETHARP_STATS_INC(etharp.drop);
		MIB2_STATS_NETIF_INC(netif, ifinerrors);
		goto free_and_return;
	}
	
	if (p->if_idx == NETIF_NO_INDEX)
	{
		p->if_idx = netif_get_index(netif);
	}
	
	/* ethhdr 指针指向以太网帧头部,并且强制转换成eth_hdr 结构*/
	ethhdr = (struct eth_hdr *)p->payload; (1)
	
	type = ethhdr->type;
	
	if (ethhdr->dest.addr[0] & 1)
	{
		/* 这可能是多播或广播数据包*/
		if (ethhdr->dest.addr[0] == LL_IP4_MULTICAST_ADDR_0)
		{
			if ((ethhdr->dest.addr[1] == LL_IP4_MULTICAST_ADDR_1) &&
					(ethhdr->dest.addr[2] == LL_IP4_MULTICAST_ADDR_2))
			{
				/* 将pbuf 标记为链路层多播*/
				p->flags |= PBUF_FLAG_LLMCAST; (2)
			}
		}
		
		else if (eth_addr_cmp(&ethhdr->dest, &ethbroadcast))
		{
			/* 将pbuf 标记为链路层广播*/
			p->flags |= PBUF_FLAG_LLBCAST; (3)
		}
	}
	
	switch (type)
	{
		/* 如果是IP 数据报*/
		case PP_HTONS(ETHTYPE_IP):
			if (!(netif->flags & NETIF_FLAG_ETHARP))
			{
				goto free_and_return;
			}
			/* 跳过以太网首部*/
			if (pbuf_remove_header(p, next_hdr_offset)) (4)
			{
				goto free_and_return;
			}
			else
			{
				/* 传递到IP 协议去处理*/
				ip4_input(p, netif); (5)
			}
			break;
			
		//对于是ARP 包
		case PP_HTONS(ETHTYPE_ARP):
			if (!(netif->flags & NETIF_FLAG_ETHARP))
			{
				goto free_and_return;
			}
			/* 跳过以太网首部*/
			if (pbuf_remove_header(p, next_hdr_offset)) (6)
			{
				ETHARP_STATS_INC(etharp.lenerr);
				ETHARP_STATS_INC(etharp.drop);
				goto free_and_return;
			}
			else
			{
				/* 传递到ARP 协议处理*/
				etharp_input(p, netif); (7)
			}
			break;
			
//如果支持PPPOE
#if PPPOE_SUPPORT
	case PP_HTONS(ETHTYPE_PPPOEDISC):
		pppoe_disc_input(netif, p);
		break;
		
	case PP_HTONS(ETHTYPE_PPPOE):
		pppoe_data_input(netif, p);
		break;
#endif /* PPPOE_SUPPORT */

//如果支持ipv6
#if LWIP_IPV6
	case PP_HTONS(ETHTYPE_IPV6): /* IPv6 */
		/* skip Ethernet header */
	if ((p->len < next_hdr_offset) ||
			pbuf_remove_header(p, next_hdr_offset))
	{
		goto free_and_return;
	}
	else
	{
		/* pass to IPv6 layer */
		ip6_input(p, netif);
	}
	break;
#endif /* LWIP_IPV6 */

	default:
#ifdef LWIP_HOOK_UNKNOWN_ETH_PROTOCOL
	if (LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p, netif) == ERR_OK)
	{
		break;
	}
#endif
	ETHARP_STATS_INC(etharp.proterr);
	ETHARP_STATS_INC(etharp.drop);
	MIB2_STATS_NETIF_INC(netif, ifinunknownprotos);
	goto free_and_return;
}
	return ERR_OK;
	
free_and_return:
	pbuf_free(p);
	return ERR_OK;
}
  • 代码清单(1):ethhdr 指针指向以太网帧首部,并且强制转换成eth_hdr 结构,这是为了方便对以太网帧首部进行操作。
  • 代码清单(2):如果目标IP 地址的第一个字节的bit0 是1,那么有可能是多播或者是广播数据包,所以,还需要进行判断,如果是多播的,就将pbuf 标记为链路层多播。
  • 代码清单(3):如果是广播的,就将pbuf 标记为链路层广播。
  • 代码清单(4):如果数据包是ETHTYPE_IP 类型,则调用pbuf_remove_header() 函数跳过以太网帧首部,方便对数据进行操作。
  • 代码清单(5):除去以太网帧首部成功,调用ip4_input() 函数将数据包递交到IP 协议去处理,对于IP 层的处理,我们在后面的章节中讲解。
  • 代码清单(6):跳过以太网帧首部。
  • 代码清单(7):除去以太网帧首部成功,调用etharp_input () 函数将数据包递交到ARP 协议去处理。

ARP 数据包处理

ARP 数据包的处理函数为etharp _input(),在这里它完成两个任务:

  1. 如果收到的是ARP 应答包,说明本机之前发出的ARP 请求包有了回应,就根据应答包更新自身的ARP 缓存表;
  2. 如果收到的是ARP 请求包,如果包中的目标IP 地址与主机IP 地址匹配,除了记录原主机的IP 与MAC 地址,更新自身的ARP 表外,还要向源主机发送一个ARP 应答包。但是如果包中目IP 地址与主机IP 地址不匹配,则尽可能记录源主机的IP 与MAC 地址,更新自身的ARP 表,并丢弃该请求包,为什么说是尽可能呢,因为主机的ARP 缓存表是有限的,不可能记录太多的ARP 表项,所以在有空闲的表项时才记录,如果没有空闲的表项,ARP 觉得它自己已经尽力了,也记不住那么多表项。

etharp_input() 函数的源码具体见代码清单。

代码清单 etharp_input() 源码

void
etharp_input(struct pbuf *p, struct netif *netif)
{
	struct etharp_hdr *hdr;
	
	ip4_addr_t sipaddr, dipaddr;
	u8_t for_us;
	
	LWIP_ASSERT_CORE_LOCKED();
	
	LWIP_ERROR("netif != NULL", (netif != NULL), return;);
	
	hdr = (struct etharp_hdr *)p->payload;
	
	/* 判断ARP 包的合法性*/
	if ((hdr->hwtype != PP_HTONS(LWIP_IANA_HWTYPE_ETHERNET)) ||
			(hdr->hwlen != ETH_HWADDR_LEN) ||
			(hdr->protolen != sizeof(ip4_addr_t)) ||
			(hdr->proto != PP_HTONS(ETHTYPE_IP))) (1)
	{
		LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING,
		("etharp_input: packet dropped, wrong hw type, hwlen, proto,protolen or ethernet type(%"U16_F"/%"U16_F"/%"U16_F"/%"U16_F")\n",hdr->hwtype,(u16_t)hdr->hwlen, hdr->proto, (u16_t)hdr->protolen));
		
		ETHARP_STATS_INC(etharp.proterr);
		ETHARP_STATS_INC(etharp.drop);
		
		pbuf_free(p);
		return;
	}
	ETHARP_STATS_INC(etharp.recv);

	//拷贝源IP 地址与目标IP 地址
	IPADDR_WORDALIGNED_COPY_TO_IP4_ADDR_T(&sipaddr, &hdr->sipaddr); (2)
	IPADDR_WORDALIGNED_COPY_TO_IP4_ADDR_T(&dipaddr, &hdr->dipaddr); (3)
	
	/* 看看主机网卡是否配置了IP 地址*/
	if (ip4_addr_isany_val(*netif_ip4_addr(netif))) (4)
	{
		for_us = 0;
	}
	else
	{
		/* 判断目标IP 地址与主机IP 地址是否一样*/
		for_us = (u8_t)ip4_addr_cmp(&dipaddr, netif_ip4_addr(netif)); (5)
	}
	
	/* 更新ARP 缓存表*/
	etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr), (6)
				for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY);
	/* 更新完毕,根据包的类型处理*/
	switch (hdr->opcode) (7)
	{
	/* ARP request? */
	case PP_HTONS(ARP_REQUEST): (8)
		/* ARP 请求包*/
		LWIP_DEBUGF (ETHARP_DEBUG | LWIP_DBG_TRACE,
					("etharp_input: incoming ARP request\n"));
		/* 是请求自己的*/
		if (for_us)
		{
			/* 做出回应*/
			etharp_raw(netif,
					(struct eth_addr *)netif->hwaddr, &hdr->shwaddr,
				(struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif),
					&hdr->shwaddr, &sipaddr,
					ARP_REPLY); (9)
			/* 不是给自己的*/
		}
		else if (ip4_addr_isany_val(*netif_ip4_addr(netif))) (10)
		{
			LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
					("etharp_input: we are unconfigured, ARP request ignored.\n"));	
		}
		else (11)
		{
			LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
						("etharp_input: ARP request was not for us.\n"));
		}
		break;
	case PP_HTONS(ARP_REPLY): (12)
		/* 对于ARP 应答包*/
		LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
					("etharp_input: incoming ARP reply\n"));
		break;
default:
	LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
				("etharp_input: ARP unknown opcode type %"S16_F"\n",
				lwip_htons(hdr->opcode)));
	ETHARP_STATS_INC(etharp.err); (13)
	break;
	}
	/* 释放内存*/
	pbuf_free(p); (14)
}
  • 代码清单(1):判断ARP 包的合法性,已经类型是否为以太网、硬件地址长度是否为ETH_HWADDR_LEN、协议地址长度是否为sizeof(ip4_addr_t) 以及协议是否为ARP 协议,如果都满足则表示ARP 包合法。
  • 代码清单(2):拷贝源IP 地址到sipaddr 变量中,因为在ARP 包中的IP 地址字段并不是对齐的,不能直接使用,所以需要拷贝到临时变量,方便直接操作。
  • 代码清单(3):同理拷贝目标IP 地址到dipaddr 变量中。
  • 代码清单(4):看看主机网卡是否配置了IP 地址,如果没有配置,将for_us 变量设置为0,表示不是给主机自己的ARP 包。
  • 代码清单(5):调用ip4_addr_cmp() 函数判断目标IP 地址与主机IP 地址是否一样,如果一样则返回1,将for_us 变量设置为1,反之设置为0。
  • 代码清单(6):调用etharp_update_arp_entry() 函数更新ARP 缓存表,这个操作有点特殊,我们稍后讲解。
  • 代码清单(7):更新完毕,根据包的类型处理,即根据ARP 数据包的op 字段进行处理。
  • 代码清单(8):对于ARP 请求包,首先要判断一下是否是给主机自己的,如果是则要回应,否则就直接丢弃即可。
  • 代码清单(9):是请求自己的,调用etharp_raw() 函数作出应答。
  • 代码清单(10):如果不是给自己的,原因有两种,一种是网卡自身尚未配置IP 地址,这样子就只打印相关调试信息。
  • 代码清单(11):另一种是ARP 包中的目标IP 地址与主机IP 地址不符合,也不用做出回应,直接丢弃即可,并输出相关调试信息。
  • 代码清单1(12):对于ARP 应答包,理论上应该更新ARP 缓存表的,毕竟发出去的ARP 请求包得到回应,但是在前面已经更新了缓存表了,此处就不用重复更新了。
  • 代码清单(13):对于其他情况,直接返回错误代码。
  • 代码清单(14):释放内存。

更新ARP 缓存表

etharp_update_arp_entry() 函数是用于更新ARP 缓存表的,它会在收到一个ARP 数据包的时候被调用,它会先查找一个ARP 表项,如果没有找到这个ARP 表项的记录,就会去新建一个ARP 表项,然后重置ARP 表项的参数(状态、网卡。IP 地址与对应的MAC 地址以及生存时间等),然后检测ARP 表项中是否挂载数据包,如果有就将这些数据包发送出去,其源码具体见代码清单。

代码清单etharp_update_arp_entry() 函数源码

static err_t
etharp_update_arp_entry(struct netif *netif, (1)
						const ip4_addr_t *ipaddr, (2)
						struct eth_addr *ethaddr, (3)
						u8_t flags) (4)
{
	s16_t i;
	
	/* non-unicast address? */
	if (ip4_addr_isany(ipaddr) ||
			ip4_addr_isbroadcast(ipaddr, netif) ||
			ip4_addr_ismulticast(ipaddr)) (5)
	{
		return ERR_ARG;
	}
	/* 查找或者创建ARP 表项,并且返回索引值*/
	i = etharp_find_entry(ipaddr, flags, netif); (6)
	
	/* 如果索引值不合法,更新ARP 表项失败*/
	if (i < 0)
	{
		return (err_t)i;
	}
	
	/* 设置表项状态为ETHARP_STATE_STABLE */
	arp_table[i].state = ETHARP_STATE_STABLE; (7)
	
	/* 记录网卡*/
	arp_table[i].netif = netif; (8)	
	/* 插入ARP 索引树*/
	mib2_add_arp_entry(netif, &arp_table[i].ipaddr); (9)
	
	/* 更新缓存表中的MAC 地址*/
	SMEMCPY(&arp_table[i].ethaddr, ethaddr, ETH_HWADDR_LEN); (10)
	/* 重置生存时间*/
	arp_table[i].ctime = 0; (11)
	
	/* 如果表项上与未发送的数据包,那就将这些数据包发送出去*/
#if ARP_QUEUEING
	while (arp_table[i].q != NULL)
	{
		struct pbuf *p;
		/* 定义q 指向ARP 表项中的数据包缓存队列*/
		struct etharp_q_entry *q = arp_table[i].q; (12)
		/* 指向下一个数据包节点*/
		arp_table[i].q = q->next; (13)
		/* 获取pbuf 数据包*/
		p = q->p; (14)
		/* 释放MEMP_ARP_QUEUE 类型的内存块*/
		memp_free(MEMP_ARP_QUEUE, q); (15)
#else /* ARP_QUEUEING */
	if (arp_table[i].q != NULL)
	{
		struct pbuf *p = arp_table[i].q; (16)
		arp_table[i].q = NULL;
#endif /* ARP_QUEUEING */
		/* 发送缓存队列的数据包*/
ethernet_output(netif, p,
				(struct eth_addr *)(netif->hwaddr),
				ethaddr,
				ETHTYPE_IP); (17)
		/* 释放pbuf 数据包的内存空间*/
		pbuf_free(p); (18)
	}
	return ERR_OK;
}
  • 代码清单(1):对应表项中的网卡。
  • 代码清单(2):对应表项中的IP 地址。
  • 代码清单(3):对应表项中的MAC 地址。
  • 代码清单(4):表项的更新方式,动态表项有两种方式,分别为ETHARP_FLAG_TRY_HARD和ETHARP_FLAG_FIND_ONLY。前者表示无论如何都要创建一个表项,如果ARP 缓存表中没有空间了,那就需要回收较老的表项,将他们删除,然后建立新的表项。而如果是后者,就让内核尽量更新表项,如果ARP 缓存表中没有空间了,那么也无能为力,实在是添加不了新的表项。
  • 代码清单(5):IP 地址验证,ARP 数据包中的目标IP 地址不能是广播、多播地址。
  • 代码清单(6):调用etharp_find_entry() 函数查找或者创建ARP 表项,并且返回索引值,如果索引值不合法,表示更新ARP 表项失败,该函数比较复杂,此处就不做过多讲解,想要了解的可以在源码中查看该函数。
  • 代码清单(7):设置表项状态为ETHARP_STATE_STABLE。
  • 代码清单(8):记录网卡信息。
  • 代码清单(9):插入ARP 索引树。
  • 代码清单(10):更新表项中的MAC 地址。
  • 代码清单(11):重置表项的生存时间。
  • 代码清单(12):如果表项上与未发送的数据包,那就将这些数据包发送出去。这里通过宏定义ARP_QUEUEING 采用哪种方式发送数据包,如果定义了缓存数据包队列,那就需要将队列上的所有数据包发送出去,定义q 指向ARP 表项中的数据包缓存队列。
  • 代码清单(13):指向下一个数据包节点。
  • 代码清单(14):获取pbuf 数据包。
  • 代码清单(15):释放MEMP_ARP_QUEUE 类型的内存块。
  • 代码清单(16):此处是单个数据包挂载到表项上,无需太多操作,直接将数据包获取到,然后发送出去即可。
  • 代码清单(17):调用ethernet_output() 函数发送挂载在表项上的数据包。
  • 代码清单(18):释放pbuf 数据包的内存空间。

整个ARP 处理的流程示意图具体见图。

在这里插入图片描述

图ARP 协议处理的流程示意图

数据包发送流程

经过学习,我们知道一个数据包从底层传递进来的流程是怎么样的,如果是ARP 数据包就会给ARP 去处理,如果是IP 数据报就使用ip4_input() 函数传递到上层,这些处理在后面的章节讲解。那么如果上层协议想要发送数据,也肯定需要经过ARP 协议将IP 地址映射为MAC 地址才能完成发送操作,IP 数据报通过ip4_output() 函数将上层数据包传递到ARP 协议处理,关于IP 协议是怎么样传递的我们暂且不说,那么ARP 通过etharp_output() 函数接收到IP 数据报后,就会进行发送,ARP 会先从数据包中进行分析,看看这个IP 数据报是单播数据包还是多播或者是广播数据包,然后进行不同的处理:

  • 对于多播或者是广播数据包,这种处理就很简单,直接将数据包丢给网卡就行了(调用ethernet_output() 函数)。
  • 对于单播包的处理稍微麻烦一点,ARP 协议需要根据IP 地址找到对应的MAC 地址,然后才能正确发送,如果找不到MAC 地址的话,还要延迟发送数据包,ARP 协议首先会创建一个ARP 表项,然后将数据包挂到ARP 表项对应的缓存队列上,与此同时会发出一个ARP请求包,等待目标主机的回应后再发送IP 数据报。

此处需要注意的是,对于PBUFF_ERF、PBUF_POOL、PBUF_RAM 类型的数据包是不允许直接挂到ARP 表项对应的缓存队列上的,因为此时内核需要等待目标主机的ARP 应答,而这段时间里,这些数据有可能会被上层改动,这是不允许的,所以LwIP 需要将这些pbuf 数据包拷贝到新的空间,等待发送。

etharp_output() 函数

etharp_output() 函数被IP 层的ip4_output() 函数调用,IP 层传递一个数据包到ARP 中,etharp_output() 会根据数据包的目标IP 地址选择不同的处理,其源码具体见代码清单。

代码清单 etharp_output() 源码

const struct eth_addr ethbroadcast =
				{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}};
const struct eth_addr ethzero = {{0, 0, 0, 0, 0, 0}};

/** 24 位IANA IPv4 多播OUI 为01-00-5e: */
#define LL_IP4_MULTICAST_ADDR_0 0x01
#define LL_IP4_MULTICAST_ADDR_1 0x00
#define LL_IP4_MULTICAST_ADDR_2 0x5e

err_t etharp_output
	(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr)
{
	const struct eth_addr *dest;
	struct eth_addr mcastaddr;
	const ip4_addr_t *dst_addr = ipaddr;
	
	LWIP_ASSERT_CORE_LOCKED();
	LWIP_ASSERT("netif != NULL", netif != NULL);
	LWIP_ASSERT("q != NULL", q != NULL);
	LWIP_ASSERT("ipaddr != NULL", ipaddr != NULL);
	
	if (ip4_addr_isbroadcast(ipaddr, netif))
	{
		/* 如果是广播数据包,目标MAC 地址设置为FF-FF-FF-FF-FF-FF-FF */
		dest = (const struct eth_addr *)&ethbroadcast; (1)
		/* multicast destination IP address? */
	}
	
	else if (ip4_addr_ismulticast(ipaddr))
	{
		/* 如果是多播数据包,目标MAC 地址设置为多播地址:01-00-5E-XX-XX-XX*/
		mcastaddr.addr[0] = LL_IP4_MULTICAST_ADDR_0;
		mcastaddr.addr[1] = LL_IP4_MULTICAST_ADDR_1;
		mcastaddr.addr[2] = LL_IP4_MULTICAST_ADDR_2;
		mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f;
		mcastaddr.addr[4] = ip4_addr3(ipaddr);
		mcastaddr.addr[5] = ip4_addr4(ipaddr);
		
		dest = &mcastaddr; (2)
		
	}
	else
	{
		/* 如果是单播目标地IP 地址*/
		netif_addr_idx_t i;
		/* 判断目标IP 地址是否与主机处于同一子网上,
		如果不是,则修改IP 地址*/
		if (!ip4_addr_netcmp(ipaddr, netif_ip4_addr(netif),
							netif_ip4_netmask(netif)) &&
				!ip4_addr_islinklocal(ipaddr)) (3)
		{
#if LWIP_AUTOIP
			struct ip_hdr *iphdr =
				LWIP_ALIGNMENT_CAST(struct ip_hdr *, q->payload);
				
			if (!ip4_addr_islinklocal(&iphdr->src))
#endif
			{
#ifdef LWIP_HOOK_ETHARP_GET_GW
				dst_addr = LWIP_HOOK_ETHARP_GET_GW(netif, ipaddr);
				if (dst_addr == NULL)
#endif
				{
					/* 判断一下网关地址是否有效*/
					if (!ip4_addr_isany_val(*netif_ip4_gw(netif))) (4)
					{
						/* 发送到默认网关,让网关进行转发*/
						dst_addr = netif_ip4_gw(netif);
						/* 没有默认网关可用*/
					}
					else
					{
						/* 返回错误*/
						return ERR_RTE;
					}
				}
			}
		}
		/* 遍历ARP 缓存表*/
		for (i = 0; i < ARP_TABLE_SIZE; i++)
		{
			if ((arp_table[i].state >= ETHARP_STATE_STABLE) &&
					(arp_table[i].netif == netif) &&
					(ip4_addr_cmp(dst_addr, &arp_table[i].ipaddr)))
			{
				/* 如果找到目标IP 地址对应的表项,直接发送*/
				ETHARP_SET_ADDRHINT(netif, i);
				return etharp_output_to_arp_index(netif, q, i); (5)
			}
		}
		
		/* 如果没有找到与目标IP 地址对应的ARP 表项*/
		return etharp_query(netif, dst_addr, q); (6)
	}

	/* 对于多播、广播数据包,直接能得到对应的MAC 地址,可以进行发送*/
	return ethernet_output(netif, q,
				(struct eth_addr *)(netif->hwaddr), dest, ETHTYPE_IP); (7)
}
  • 代码清单(1):如果是广播数据包,目标MAC 地址设置为FF-FF-FF-FF-FF-FF-FF。

  • 代码清单(2):如果是多播数据包,目标MAC 地址设置为多播地址:01-00-5E-XX-XX-XX。此处简单补充一下单播包、广播包与多播包的相关知识:

单播包:顾名思义,就是一对一通信,发送的目标主机IP 地址是唯一的,就像是人们之间的对话一样,一个人对另外一个人说话。

多播包:“多播”可以理解为一个人向多个人(但不是在场的所有人)说话,比如在一个大餐厅中,一个人说话只能让一桌人知道,而其他桌上的人并不知道说了什么。同理的,主机发送的多播包只能让某些满足条件的目标主机接收到。

广播包:而广播就是类似于用大喇叭进行广播通知,在场的所有人都能知道。广播包是让所有处于同一子网的主机都能接收到数据包。

  • 代码清单(3):如果是单播目标地IP 地址,首先判断目标IP 地址是否与主机处于同一子网上,如果不是,则修改IP 地址,IP 地址为网关的IP 地址,目的是为了让网关进行转发。

  • 代码清单(4):判断一下网关地址是否有效,如果有效,则发送到默认网关,让网关进行转发,没有默认网关可用则返回错误代码。

  • 代码清单(5): 遍历ARP 缓存表, 如果找到目标IP 地址对应的表项, 调用etharp_output_to_arp_index() 函数直接发送,该函数源码具体见代码清单。

  • 代码清单(6):如果没有找到与目标IP 地址对应的ARP 表项,需要调用etharp_query() 函数进行发送,这个函数在稍后讲解,具体见下小节。

  • 代码清单(7):对于多播、广播数据包,直接能得到对应的MAC 地址,可以进行发送。

etharp_output_to_arp_index() 函数

这个函数是ARP 找到了IP 地址与MAC 地址对应的表项,从而能直接进行发送,除此之外,ARP还需要更新ARP 表项,我们知道,LwIP 中的ARP 表项生存时间是5 分钟(300 秒),那么在APP表项的生存时间即将到来的时候,ARP 需要更新表项,为什么要在发送数据的时候更新呢?因为如果不发送数据,那就没必要更新ARP 表项,这样子表项在生存时间到来的时候就会被系统删除,回收ARP 表项空间,而一直使用的ARP 表项需要是谁更新,更新的方式也有两种:

如果ARP 表项还差15 秒就过期了,LwIP 会通过广播的方式发送一个ARP 请求包,试图得到主机的回应。

而如果ARP 表项还差30 秒就过期了,那么LwIP 会通过单播的方式向目标主机发送一个请求包并试图得到回应。

在这种情况下发送ARP 请求包的时候,表项的状态会由ETHARP_STATE_STABLE 变成ETHARP_STATE_STABLE_REREQUESTING_1,如果目标主机回应了,那就更新ARP 缓存表中的表项。

当然,如果还没那么快到期的话,那就直接调用ethernet_output() 函数将数据包传递给网卡进行发送。函数源码具体见代码清单

代码清单 etharp_output_to_arp_index() 函数源码

#define ARP_MAXAGE 300

/* 即将到期的时间*/
#define ARP_AGE_REREQUEST_USED_UNICAST (ARP_MAXAGE - 30)
#define ARP_AGE_REREQUEST_USED_BROADCAST (ARP_MAXAGE - 15)

static err_t
etharp_output_to_arp_index(struct netif *netif,
						struct pbuf *q,
						netif_addr_idx_t arp_idx)
{
	LWIP_ASSERT("arp_table[arp_idx].state >= ETHARP_STATE_STABLE",
				arp_table[arp_idx].state >= ETHARP_STATE_STABLE);
	/* 如果arp 表项即将过期:LwIP 会重新请求它,
		但只有当它的状态是ETHARP_STATE_STABLE 才能请求*/
	if (arp_table[arp_idx].state == ETHARP_STATE_STABLE)
	{
		/* 还差15 秒到期*/
		if (arp_table[arp_idx].ctime >= ARP_AGE_REREQUEST_USED_BROADCAST)
		{
		/* 使用广播方式发出请求包*/
		if (etharp_request(netif, &arp_table[arp_idx].ipaddr) == ERR_OK)
		{
			arp_table[arp_idx].state = ETHARP_STATE_STABLE_REREQUESTING_1;
		}
	}
	/* 还差30 秒到期*/
	else if (arp_table[arp_idx].ctime >= ARP_AGE_REREQUEST_USED_UNICAST)
	{
			/* 发出单播请求(持续15 秒),以防止不必要的广播*/
			if (etharp_request_dst(netif,
								&arp_table[arp_idx].ipaddr,
								&arp_table[arp_idx].ethaddr) == ERR_OK)
			{
			arp_table[arp_idx].state = ETHARP_STATE_STABLE_REREQUESTING_1;
			}
		}
	}
	
	return ethernet_output(netif, q,
							(struct eth_addr *)(netif->hwaddr),
							&arp_table[arp_idx].ethaddr, ETHTYPE_IP);
}

etharp_query() 函数

如果在ARP 缓存表中没有找到目标IP 地址对应的表项,那么ARP 协议就会创建一个表项,这也是ARP 协议的核心处理,对于刚创建的表项,它在初始化网卡信息后会被设置为ETHARP_STATE_PENDING 状态,与此同时一个ARP 请求包将被广播出去,这个时候的表项是无法发送数据的,只有等待到目标主机回应了一个ARP 应答包才能发送数据,那么这些数据在这段时间中将被挂到表项的等待队列上,在ARP 表项处于ETHARP_STATE_STABLE 状态完成数据的发送,函数源码具体见代码清单。

代码清单 etharp_query() 函数

err_t
etharp_query(struct netif *netif,
			const ip4_addr_t *ipaddr,
			struct pbuf *q)
{
	struct eth_addr *srcaddr = (struct eth_addr *)netif->hwaddr;
	err_t result = ERR_MEM;
	int is_new_entry = 0;
	s16_t i_err;
	netif_addr_idx_t i;
	
	/* 检是否为单播地址*/
	if (ip4_addr_isbroadcast(ipaddr, netif) ||
			ip4_addr_ismulticast(ipaddr) ||
			ip4_addr_isany(ipaddr))
	{
		return ERR_ARG;
	}
	
	/* 在ARP 缓存中查找表项,如果没有则尝试创建表项*/
	i_err = etharp_find_entry(ipaddr, ETHARP_FLAG_TRY_HARD, netif);(1)
	
	/* 没有发现表项或者没有创建表项成功*/
	if (i_err < 0)
	{
		LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
					("etharp_query: could not create ARP entry\n"));
		if (q)
		{
			LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,
						("etharp_query: packet dropped\n"));
			ETHARP_STATS_INC(etharp.memerr);
		}
		return (err_t)i_err; //返回错误代码
	}
	LWIP_ASSERT("type overflow", (size_t)i_err < NETIF_ADDR_IDX_MAX);
	
	//找到对应的表项或者创建表项成功
	i = (netif_addr_idx_t)i_err;
	
	/* 将新表项标记为待处理*/
	if (arp_table[i].state == ETHARP_STATE_EMPTY)
	{
		is_new_entry = 1;
		arp_table[i].state = ETHARP_STATE_PENDING;
		/* 记录网络接口*/
		arp_table[i].netif = netif; (2)
	}
	/* 是否有新的表项*/
	if (is_new_entry || (q == NULL)) (3)
	{
		/* 发送ARP 请求包*/
		result = etharp_request(netif, ipaddr);
		if (result != ERR_OK)
		{
			/* 无法发送ARP 请求*/
		}
		if (q == NULL)
		{
			return result; (4)
		}
	}
	
	LWIP_ASSERT("q != NULL", q != NULL);
	/* 表项状态是否稳定*/
	if (arp_table[i].state >= ETHARP_STATE_STABLE)
	{
		ETHARP_SET_ADDRHINT(netif, i);
		/* 发送数据包*/
		result = ethernet_output(netif, q,
								srcaddr,
								&(arp_table[i].ethaddr),
								ETHTYPE_IP); (5)
	}
	/* 如果表项是ETHARP_STATE_PENDING 状态*/
	else if (arp_table[i].state == ETHARP_STATE_PENDING)
	{
		/* 将给数据包'q'排队*/
		struct pbuf *p;
		int copy_needed = 0;
		/* 如果q 包含必须拷贝的pbuf,请将整个链复制到一个新的PBUF_RAM */
		p = q;
		while (p)
		{
			LWIP_ASSERT("no packet queues allowed!",
						(p->len != p->tot_len) || (p->next == 0));
			if (PBUF_NEEDS_COPY(p)) (6)
			{
				//需要拷贝
				copy_needed = 1;
				break;
			}
		p = p->next;
		}
		if (copy_needed)
		{
			/* 将整个数据包复制到新的pbuf 中*/
			p = pbuf_clone(PBUF_LINK, PBUF_RAM, q); (7)
			}
			else
			{
				/* 引用旧的pbuf 就足够了*/
				p = q;
				pbuf_ref(p);
			}
			
		if (p != NULL)
		{
			/* 如果使用队列*/
#if ARP_QUEUEING
			struct etharp_q_entry *new_entry;
			/* 分配一个新的arp 队列表项*/ (8)
			new_entry = (struct etharp_q_entry *)memp_malloc(MEMP_ARP_QUEUE);
			if (new_entry != NULL)
			{
				unsigned int qlen = 0;
				new_entry->next = 0;
				new_entry->p = p;
				if (arp_table[i].q != NULL)
				{
					/* 队列已经存在,将新数据包插入队列后面*/
					struct etharp_q_entry *r;
					r = arp_table[i].q;
					qlen++;
					while (r->next != NULL)
					{
						r = r->next;
						qlen++;
					}
					r->next = new_entry; (9)
				}
				else
				{
					/* 队列不存在,数据包就是队列的第一个节点*/
					arp_table[i].q = new_entry; (10)
				}
#if ARP_QUEUE_LEN
				if (qlen >= ARP_QUEUE_LEN)
				{
					struct etharp_q_entry *old;
					old = arp_table[i].q;
					arp_table[i].q = arp_table[i].q->next;
					pbuf_free(old->p);
					memp_free(MEMP_ARP_QUEUE, old);
				}
#endif
				result = ERR_OK;
			}
			else
			{
				/* 申请内存失败*/
				pbuf_free(p);
				result = ERR_MEM;
			}
#else

			/* 如果只是挂载单个数据包,那么始终只为每个ARP 请求排队一个数据包,
			就需要释放先前排队的数据包*/
			if (arp_table[i].q != NULL)
			{			
				pbuf_free(arp_table[i].q); (11)
			}
			arp_table[i].q = p;
			result = ERR_OK;
			
#endif
			}
			else
			{
				ETHARP_STATS_INC(etharp.memerr);
				result = ERR_MEM;
			}
	}
	return result;
}
  • 代码清单(1)(2):函数的处理逻辑是很清晰的,首先调用etharp_find_entry() 函数在ARP 缓存表中查找表项,如果没有找到就尝试创建表项并且返回表项的索引,当然ARP 缓存表中可能存在表项,可能为新创建的表项(ETHARP_STATE_EMPTY),也可能为ETHARP_STATE_PENDING或者ETHARP_STATE_STABLE 状态。如果是新创建的表项,那么表项肯定没有其他信息,LwIP就会初始化一些信息,如网卡,然后就将表项设置为ETHARP_STATE_PENDING 状态。

  • 代码清单(3):如果表项是刚创建的或者数据包是空的,那么就会调用etharp_request() 函数发送一个ARP 请求包。

  • 代码清单(4):如果数据包是空的,直接返回结果

  • 代码清单(5):如果表项的状态大于等于
    ETHARP_STATE_STABLE,表示表项已经是稳定状态了,就调用ethernet_output() 函数发送数据包。

  • 代码清单(6):通过宏定义PBUF_NEEDS_COPY§ 对数据包的类型进行判断,如果需要拷贝则将变量copy_needed 设置为1,表示需要拷贝。

  • 代码清单10‑13(7):将整个数据包复制到新的pbuf 中。

  • 代码清单10‑13(8):如果ARP_QUEUEING 宏定义为1,则表示使用队列,那么LwIP 会分配一个新的ARP 数据包队列节点,然后插入队列中。

  • 代码清单10‑13(9):如果队列已经存在,将新数据包插入队列后面

  • 代码清单10‑13(10):如果队列不存在,数据包就是队列的第一个节点。

  • 代码清单10‑13(11):如果只是挂载单个数据包,就需要释放先前排队的数据包,然后再挂载新的数据包。

挂载的这些数据在等待到目标主机产生ARP 应答的时候会发送出去,此时的发送就是延时了,所以在没有ARP 表项的时候,发送数据会产生延时,在指定等待ARP 应答时间内如果等不到目标主机的应答,那么这个表项将被系统回收,同时数据也无法发送出去。

上层数据包通过ARP 协议进行发送数据的流程示意图具体见图。

在这里插入图片描述

图 数据包通过ARP 协议发送流程图

整个ARP 协议运作示意图具体见图。
在这里插入图片描述

图 整个ARP 协议运作示意图


参考资料:LwIP 应用开发实战指南—基于野火STM32

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值