【计网实验——prj10】高效IP路由查找实验
实验要求
实验一
实现最基本的前缀树查找
实验二
调研并实现某种IP前缀查找方案
测试与验证
-
基于forwarding-table.txt数据集(Network, Prefix Length, Port)
-
本实验只考虑静态数据集,不考虑表的添加或更新
-
以前缀树查找结果为基准,检查所实现的IP前缀查找是否正确
- 可以将forwarding-table.txt中的IP地址作为查找的输入
-
对比基本前缀树和所实现IP前缀查找的性能
- 内存开销、平均单次查找时间
实现方案
最基本的前缀树查找
数据结构
在本实验的条件下,实现前缀树时需要用到以下两个数据结构:
- 网络号条目
NetEntry
,从forwarding-table.txt中的每一行提取出来,包括IP地址,前缀长度和端口号三个部分:
typedef struct NetEntry_t{
unsigned int net;
int prefix_len;
int port;
} NetEntry;
- 前缀树结点
treeNode
,包括前缀树中该结点处对应的网络号对应的端口号和父子结点指针:
typedef struct treeNode_t{
int port;
struct treeNode_t *left;
struct treeNode_t *right;
struct treeNode_t *parent;
} treeNode;
关键函数及其功能
NetEntry *create_entry(char *buf)
将从txt中读取的每一行生成net表项,用结构NetEntry存储:
NetEntry *create_entry(char *buf){
NetEntry *entry = (NetEntry *) malloc(sizeof(NetEntry));
const char *split = " ";
char *p, *net;
int count = 0;
p = strtok (buf, split);
while(p) {
if(count == 0){
net = p;
}
else if(count == 1){
entry->prefix_len = atoi(p);
}
else if(count == 2){
entry->port = atoi(p);
}
count++;
p = strtok(NULL, split);
}
entry->net = net_number(net);
return entry;
}
unsigned int net_number(char *buf)
将网络号转化为int类型,便于后续进行掩码计算:
unsigned int net_number(char *buf){
const char *split = ".";
char *p;
int count = 0;
unsigned int net_number = 0;
p = strtok(buf, split);
while(p) {
if(count == 0)
net_number |= atoi(p) << 24;
else if(count == 1)
net_number |= atoi(p) << 16;
else if(count == 2)
net_number |= atoi(p) << 8;
else if(count == 3)
net_number |= atoi(p);
count++;
p = strtok(NULL, split);
}
return net_number;
}
void init_tree()
初始化前缀树:
treeNode *init_tree() {
treeNode *root = (treeNode *) malloc(sizeof(treeNode));
total_mem += sizeof(treeNode);
if (!root) {
exit(0);
}
root->left = NULL;
root->right = NULL;
root->parent = root;
root->port = -1;
return root;
}
treeNode *createNode(NetEntry *entry, treeNode *p, int p_len)
为前缀树创建一个新的结点,该函数在构建网络号的查找路径时被调用,创建的新结点作为当前遍历结点的子结点,使得前缀搜索树从上至下生长:
treeNode *createNode(NetEntry *entry, treeNode *p, int p_len){
treeNode *tn = (treeNode *) malloc(sizeof(treeNode));
total_mem += sizeof(treeNode);
if (!tn) {
exit(0);
}
if (entry->prefix_len > p_len) {
tn->port = -1;
}else{
tn->port = entry->port;
}
tn->left = NULL;
tn->right = NULL;
tn->parent = p;
return tn;
}
当对某一网络号的前缀搜索还未到达匹配结点(match node)时,说明当前结点暂时不与任何网络号对应,更不存在对应的端口号,因此在以上代码实现中将中间结点(internal node)的对应端口号设为-1
加以区分。
void add_path(NetEntry *entry)
构建某一网络号的相关查找结构,即在当前树结构中从上至下遍历,补全在查找本网络号时需要经过的路径:
void add_path(NetEntry *entry){
treeNode *p = root;
unsigned int mask = ONE_HOT_MASK;
for(int i = 0; i < entry->prefix_len; i++){
if(entry->net & mask){
if(!p->right){
p->right = createNode(entry, p, i+1);
}
p = p->right;
}
else{
if(!p->left){
p->left = createNode(entry, p, i+1);
}
p = p->left;
}
mask = mask >> 1;
}
total_mem -= sizeof(NetEntry);
free(entry);
}
int lookup(int net_id)
根据网络号查找已经构建好的前缀树,获取网络号对应的端口号:
int lookup(int net_id){
treeNode *p = root;
unsigned int mask = ONE_HOT_MASK;
for(int i = 0; i < 32; i++){
if(net_id & mask){
if(p->right) p = p->right;
else break;
}
else{
if(p->left) p = p->left;
else break;
}
mask = mask >> 1;
}
return p->port == -1? lookback(p) : p->port;
}
根据输入的网络号,对前缀树从上至下搜索,若下一位为0则向左子树继续搜索,若下一位为1则向右子树继续搜索,每一层匹配一位,直到无法继续向下匹配,提取当前结点的端口值,若端口值为-1,则说明最终到了中间结点上,需要进行回溯查找,找到最近的最长前缀匹配的网络号,并返回该网络对应的转发端口。回溯过程由lookback
函数实现。
int lookback(treeNode *node)
回溯查找,利用结点的parent指针,找到最近的最长前缀匹配的网络号,并返回该网络对应的转发端口:
int lookback(treeNode *node){
treeNode *p = node;
while (p->port == -1) {
p = p->parent;
}
return p->port;
}
2-bit前缀树(multi-bit trie)
与简单的前缀树方法不同,多bit前缀树方法每次匹配不只匹配1bit,本设计中采用一次匹配2bit的方法。
数据结构改进
采用一次匹配2bit的方法,每次匹配将存在00
,01
,10
和11
四种匹配方法,即前缀树的每个结点存在四个子结点:
typedef struct treeNode_t{
//奇数位前缀匹配的port值
int port1;
//偶数位前缀匹配的port值
int port2;
struct treeNode_t *first;
struct treeNode_t *second;
struct treeNode_t *third;
struct treeNode_t *fourth;
} treeNode;
由于每次都是两位一起匹配,而前缀长度可能为奇数也可能为偶数,因此匹配结点(match node)存在两种网络号数值相同但前缀长度不同的匹配情况。需要注意的是,两种情况对应的端口号也可能不同,因此在结构体中用两个port
分量分别存储奇数位前缀和偶数位前缀匹配的端口值。
函数改进
2-bit前缀树作为基础前缀树的改进方法,实现思路大体相同,在细节上有以下几个函数存在一定的区别。
treeNode *createNode(NetEntry *entry, treeNode *p, int p_len)
treeNode *createNode(NetEntry *entry, treeNode *p, int p_len){
treeNode *tn = (treeNode *) malloc(sizeof(treeNode));
total_mem += sizeof(treeNode);
if (!tn) {
exit(0);
}
if (entry->prefix_len > p_len) {
//improve:将所有中间结点的port值设置为最近的最⻓前缀匹配的⽹络号的端口,免去lookback操作
tn->port1 = p->port2;
tn->port2 = tn->port1;
}else{
if(entry->prefix_len%2){
tn->port1 = entry->port;
tn->port2 = tn->port1;
}
else{
tn->port2 = entry->port;
tn->port1 = p->port2;
}
}
tn->first = NULL;
tn->second = NULL;
tn->third = NULL;
tn->fourth = NULL;
return tn;
}
将所有中间结点的port
值设置为最近的最长前缀匹配的网络号的端口,即让新建的中间结点继承父节点的端口号(**注意:**使用这种方法需要利用提供的txt文件中网络号是有序排列的这个性质),可以有效地避免回溯操作,这是基础前缀树搜索的一种优化方法,直接将这种方法应用在2-bit前缀树中,可以简化新建结点在两个port
分量的赋值。
在对新建结点的port1
和port2
两个分量赋值的过程中,需要考虑前缀长度的奇偶性和该结点的性质:若为中间结点,则与上述类似地,结点的两个port
值分别继承其前一位匹配对应的端口值(即代码中新结点tn
的port1
对应的前一位的port
值为当前结点p
(新结点tn
将称为p
的子结点)的port2
,tn
的port2
对应的前一位的port
值为tn
的port1
);若为匹配结点,匹配的前缀长度为奇数时tn->port1
即为当前匹配的网络号对应的端口号,tn->port2
仍然向上继承tn->port1
的值,同理,匹配的前缀长度为偶数时tn->port2
即为当前匹配的网络号对应的端口号,而tn->port1
则向上继承p->port2
的值。
void add_path(NetEntry *entry)
void add_path(NetEntry *entry){
treeNode *p = root;
unsigned int mask0 = MASK_0;
unsigned int mask1 = MASK_1;
unsigned int mask2 = MASK_2;
for(int i = 0; i < entry->prefix_len; i += 2){
if((entry->net & mask0) == mask0){
if(!p->first){
p->first = createNode(entry, p, i+2);
}
p = p->first;
}
else if((entry->net & mask1) == mask1){
if(!p->second){
p->second = createNode(entry, p, i+2);
}
p = p->second;
}
else if((entry->net & mask2) == mask2){
if(!p->third){
p->third = createNode(entry, p, i+2);
}
p = p->third;
}
else{
if(!p->fourth){
p->fourth = createNode(entry, p, i+2);
}
p = p->fourth;
}
//更新结点中的port2值
if(i + 2 == entry->prefix_len){
p->port2 = entry->port;
}
mask0 = mask0 >> 2;
mask1 = mask1 >> 2;
mask2 = mask2 >> 2;
}
total_mem -= sizeof(NetEntry);
free(entry);
}
最基本的逐位前缀查找方法中,路径创建过程只在原有的前缀树结构中补充查找当前网络号需要经过的路径中尚未建立的结点,对于原本已经存在的结点,不作改动,继续向下遍历,这是因为每个结点都与唯一的网络号对应(网络号相同但前缀长度不同视为不同的网络号)。然而,对于2-bit查找方法,由于一个结点与两种网络号存在匹配关系,即两前缀长度不同但数值相同的网络号可能查找路径完全相同,但最终需要匹配到同一结点中所存的不同的端口值。在为某一网络号的一种前缀长度(2k-1)建立路径并新建了匹配结点后,在为该网络号的另一种前缀长度(2k)建立查找路径时如果因为匹配结点已经存在而不对此结点作任何改动,会导致其丢失其中一种匹配方式的端口信息,从而无法在查找时得到正确的端口号。所以,2-bit前缀树方法需要新增的操作即在为前缀长度为2k的网络号建立查找路径时,即使匹配结点已经存在仍然需要更新该结点的port2
值。
int lookup(int net_id)
int lookup(int net_id){
treeNode *p = root;
unsigned int mask0 = MASK_0;
unsigned int mask1 = MASK_1;
unsigned int mask2 = MASK_2;
for(int i = 0; i < 32; i += 2){
if((net_id & mask0) == mask0){
if(p->first) p = p->first;
else break;
}
else if((net_id & mask1) == mask1){
if(p->second) p = p->second;
else break;
}
else if((net_id & mask2) == mask2){
if(p->third) p = p->third;
else break;
}
else{
if(p->fourth) p = p->fourth;
else break;
}
mask0 = mask0 >> 2;
mask1 = mask1 >> 2;
mask2 = mask2 >> 2;
}
return p->port2;
}
2-bit查找过程与最基本的前缀树查找过程不同的是不需要回溯(在createNode
函数处优化),且直接返回最终结点的port2
值即可(匹配到偶数位时本来就对应port2
,匹配到奇数位时port2
继承了port1
的值,因此也可以直接返回port2
)。
运行结果
两种方法的运行结果如下:
通过测试所有的网络号条目都查找正确(总的网络号数目比txt中的总条目数稍微少一些,是因为txt中存在网络号相同前缀长度不同的条目,在测试时只需要匹配出某一网络号的最长前缀对应的端口值即可),且2-bit前缀树方法比简单的前缀树方法在内存和时间上开销都小很多。