CS144学习(4)IP路由
最后一个实验是要实现一个IP路由表,只需要实现添加路由表项和前缀匹配两个部分,不涉及路由协议。这个实验就很简单了,就20行代码就差不多了。
实验的关键在于如何存储路由表,最简单当然也是最慢的方法就是直接保存在一个数组中,然后一个个匹配过去,时间复杂度为O(n)
。
一种方法是通过哈希表来进行改进,但哈希表是无序的,无法进行前缀匹配,因此需要设置33个哈希表来根据前缀分开存储。当匹配时从最长前缀的哈希表依次向前匹配,当匹配到一个之后就匹配成功。但这种方法存在一个问题,就是最坏情况下要对33个哈希表都匹配一遍才能找到匹配项,这样的话耗时就非常多了。在我的代码里面就是使用的这种方法,比较早的Linux内核中也是用的这种方法。
其实,最长前缀匹配问题是非常适合用前缀树(Trie-Tree)来实现的。每一位是0或1,正好对应二叉树的左右孩子,查找时根据IP地址一直查找到叶结点就找到了匹配项。这样的话,树的最大高度为32,最坏情况下也要匹配32次,而树的结构对于缓存来说也是不友好的。
实际上,路由表所对应的树当中有很多路径是可以进行压缩的,比如某个路径上只有一个路由项,那么这条路径就可以被压缩掉,从而来减小树的高度,这样就构成了路径压缩前缀树。
另一方面,路由表所对应的树有一个性质就是在有的部分会比较稠密,那么对于稠密的部分,我们可以进一步进行压缩,在结点中使用长度为2^n
的数组来保存多个孩子,查找时直接查找对应下标的孩子即可,也就是将之前一次匹配一位变为了一次匹配多位,这样就可以更好地利用缓存以及减小树的高度。这种树就是LC-Trie-Tree,Linux内核中使用的就是这种结构。在实际的测试中,上万个路由项的路由表的高度一般也不会超过5,这样的话前缀匹配的效率就大大提升了。
这个实验的代码如下,内容很简单:
#include "router.hh"
#include <iostream>
using namespace std;
void Router::add_route(const uint32_t route_prefix,
const uint8_t prefix_length,
const optional<Address> next_hop,
const size_t interface_num) {
cerr << "DEBUG: adding route " << Address::from_ipv4_numeric(route_prefix).ip() << "/" << int(prefix_length)
<< " => " << (next_hop.has_value() ? next_hop->ip() : "(direct)") << " on interface " << interface_num << "\n";
uint32_t mask = 0xffffffff << prefix_length;
if ((route_prefix & mask) != route_prefix) {
cerr << "Bad router_prefix" << endl;
}
_router_table[prefix_length][route_prefix] = {next_hop, interface_num};
}
void Router::route_one_datagram(InternetDatagram &dgram) {
if (dgram.header().ttl == 1 || dgram.header().ttl == 0) return; // drop dgram
uint32_t mask = 0xffffffff;
uint32_t target = dgram.header().dst;
for(int i = 32; i >= 0; i--) {
if (_router_table[i].count(target & mask)) {
// match
dgram.header().ttl -= 1;
auto record = _router_table[i][target & mask];
auto next_hop = record.address;
Address targetAddr = Address::from_ipv4_numeric(target);
_interfaces[record.interface].send_datagram(dgram, next_hop.value_or(targetAddr));
return;
} else {
mask <<= 1;
}
}
}
void Router::route() {
// Go through all the interfaces, and route every incoming datagram to its proper outgoing interface.
for (auto &interface : _interfaces) {
auto &queue = interface.datagrams_out();
while (not queue.empty()) {
route_one_datagram(queue.front());
queue.pop();
}
}
}