0.Overview
在本节实验中,我们将下降到网络栈的底层,实现一个 NetworkInterface
接口类,完成网络层和数据链路层之间的交互传输组件。
和前几次实验相比,这次实验的难度算得上是相当简单,就连给出的单元测试也只有 2 个。
但是因为课程提倡向仓库发起对于测试集的 PR,所以每个新 lab 的测试代码都会新增一些代码发布时没有的 unit test。
因此建议是每 merge 一个新的分支,都重新跑一次最完整的一个 unit test(一般都是指 checkpoint3 的 unit test),保证自己以前的代码是完全没问题的。
1.Getting started
这个 checkpoint 的实验很简单了,因为数据链路层的服务是“尽最大努力交付”,所以我们在实现时只需要不断往外丢数据帧即可。
然后因为数据链路层是主要负责把网络层传来的 IP 数据报包装为以太网帧,或者是把以太网帧解离为 IP 数据报,所以我们还需要实现数据报解析和转发过程中必不可少的地址解析协议 ARP。
难点也不是很多,接下来只介绍一下给定的 starter code。
顺便一提,本节实验可以使用 Docker 完成。
2.需求分析
前置知识:
在数据链路层中,ARP 协议实际上负责 IP 地址和 MAC 地址(代码中称其为ethernet_address_
,以太网地址)之间的映射,这个映射关系完全由抽象的网络接口NetworkInterface
自己学习。
当NetworkInterface
转发 IP 数据报时发现找不到下一跳 IP 地址所对应的以太网地址(查表),就会向网络中广播一个 ARP 请求,并缓存这个数据报直到 ARP 请求得到响应。
相对的,如果NetworkInterface
收到了一个 ARP 请求,并且该请求所请求的 IP 地址和自己的对上了,就会向该请求的发送方回传一个 ARP 响应。
为了学习网络中 IP-以太网地址的映射关系,NetworkInterface
需要维护一张映射表。而为了适应动态变化的网络拓扑结构,这张映射表肯定需要定时强制更新;文档要求:这张表中的每个映射关系最长只能存在 30 秒。
2.1 NetworkInterface::send_datagram
方法
在该方法中,我们要将给定的下一跳 IP 地址转换为对应的以太网地址,并把 IP 数据报封装为以太网帧的 payload。
当目的以太网地址已知时,使用 serialize()
函数将 dgram
序列化为 std::vector<std::string>
类型,并装入 EthernetFrame::payload
中;接着完成以太网帧头部 EthernetFrame::header
的变量设置,最后把组装好的数据帧转发出去。
如果目的以太网地址未知,这时就需要组装一个 ARPMessage
请求对应的以太网地址,再将这个 ARP 请求序列化后装载以太网帧中发出。
文档提到:为了避免频繁的 ARP 请求阻塞网络,我们需要保证五秒内,相同的 IP 地址的 ARP 请求只发出一次。
2.2 NetworkInterface::recv_frame
方法
这个方法需要过滤掉目的以太网地址既不是广播地址(ETHERNET_BROADCAST
)、也不是本接口的以太网地址(ehternet_address_
)的数据帧。
如果数据帧的目的地址是本接口,那么就需要按照数据帧头部指出的协议类型,将数据帧解析为对应的数据报类型(使用 parse()
)。
当数据帧的协议是 IPv4 时,且解析成功(parse()
返回值为 true
),那么就把解析得到的 InternetDatagram
数据报推入队列 datagrams_received_
中。
若数据帧协议为 ARP,且解析成功,那么按照得到的 ARPMessage
中的信息,分析是否是请求本接口的 IP 地址和以太网地址的映射关系、或者是否是响应之前本接口发出的 ARP 请求。如果是 ARP 请求,那么就组装对应的 ARPMessage
并发送给请求发送方;如果是 ARP 响应,那么就将先前缓存的、现在能发送的 IP 数据报全部发送出去。
注意这里无论是 ARP 请求还是 ARP 响应,只要数据帧解析成功,都要从中学习新的地址映射关系。
2.3 NetworkInterface::tick
方法
这个方法负责告知接口已经过去了多长时间,用于更新映射表和 ARP 请求限制(5 秒内不能发出对相同 IP 的 ARP 请求)。
2.4 Q&A
Q&A 中指出了几点细节:
- 100 到 150 行的代码实现是合理的;
- 调用
transmit()
方法发出数据帧; - 自行决定 IP 和以太网地址的映射关系的数据结构实现;
- 使用
Address::ipv4_numeric()
方法将一个 ipv4 地址转换为 32 位无符号整数; - 不用考虑 ARP 请求始终未得到响应的可能。
3.代码实现
从需求分析可以得知,本次实验有比较频繁的查表需求。
值得高兴的是,文档提到 IP 地址本身可以被转换为一个无符号整数,所以我们可以使用诸如 std::map
、std::unordered_map
等容器设计我们的映射表、数据报缓存等等。而我们主要要面对的工作是从 IP 地址出发去查找对应的以太网地址,因此插入和查询开销接近 O(1) 的 std::unordered_map
就是不二之选。
所以现在可以有这样一个思路:
- 映射表和 ARP 请求限制使用
std::unordered_map
实现,主键就是uint32_t
状态下的 IP 地址; - 数据报缓存使用
std::unordered_multimap
实现,主键定义同上,这是因为同一个目的 IP 地址可以对应很多个 IP 报文。
因为同一个 IP 地址可以对应多个报文,所以在收到一个 ARP 响应时,可以使用 std::unordered_multimap::equal_range
找出所有下一跳地址相同的 IP 报文,然后逐一转发即可。