CS144学习(4)IP路由

556 阅读3分钟

最后一个实验是要实现一个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();
        }
    }
}