gin框架(1)- 路由原理:trie和radix tree

1. 前言

本篇是对gin框架源码解析的第一篇,主要讲述gin的路由httprouter的原理:radix tree(压缩字典树)。

2. Trie(字典树)

  在讲述radix tree之前,不得不简单提到radix tree的基础版本trie,又叫字典树,前缀匹配树(prefix tree),适合存储key为string,且所有key含有大量公共前缀的map型数据结构,示例如下:

图3 插入操作的常见情况

可见插入需要找到最长公共前缀节点(需要该辅助方法),然后执行插入操作,具体的代码如下:
class RadixTree:
    def find_longest_prex_node(self, path):
        """返回值为node(最长公共前缀节点), prex(最长公共前缀)
        """
        if path == '':
            raise Exception('path cannot be empty')
        node = self.root
        index = 0
        common_prex = '' # 记录公共前缀
        while index < len(node.children):
            child = node.children[index]
            if child.path == path:
                common_prex = path
                return child, common_prex
            elif len(child.path) < len(path) and child.path == path[:len(child.path)]:
                common_prex += child.path
                node = child
                index = 0
                continue
            else:
                index += 1
        return node, common_prex
        
    def insert(self, path, value):
        common_prex_node, common_prex = self.find_longest_prex_node(path)
        if common_prex == path:
            # 已经存在该节点,直接更新值
            common_prex_node.has_value = True
            common_prex_node.value = value
        path = path[len(common_prex):] # 剩余path
        for idx, child in enumerate(common_prex_node.children):
            if child.path[0] == path[0]:
                # 有公共前缀,把公共前缀作为一个单独的子节点
                i = 0
                while i < len(child.path) and i < len(path) and child.path[i] == path[i]:
                    i += 1
                new_child = Node(child.path[:i]) # 新的child
                grandson = Node(child.path[i:], has_value=child.has_value, value=child.value, children=child.children) #继承现有child的所有成员
                new_child.children.append(grandson)
                new_child.children.append(Node(path[i:], has_value=True, value=value)) # 要插入的值的位置
                common_prex_node.children[idx] = new_child
                return
        # 遍历完还没有返回结果,说明没有跟common_prex_node的所有child没有公共前缀
        node = Node(path, has_value=True, value=value)
        common_prex_node.children.append(node)
        return

3.4 删除操作

删除操作首先是找到待删除的节点node,然后分三种情况:

  1. 如果该节点没有children,则直接移除该节点;此时如果该节点父节点只剩下一个孩子,需要将父节点和父节点剩下的一个孩子合并;
  1. 如果该节点只有一个children,则node.path = node.path+node.children[0].path(跟其孩子节点的path merge), 然后node.value = node.children[0].value
  1. 如果该节点有多个children,则直接将其变成路径节点即可(node.has_value=False, node.value=null)
所以要删除,首先要找到对应的节点,因此查找函数除了返回True or False之外;如果返回True,还需要返回该节点,方便删除操作使用。
对应的python代码如下:
class RadixTree:
    def delete(self, path):
        has_node, node = self.find_node(path)
        if not has_node or node.has_value == False:
            return
        if len(node.children) == 0:
            # 直接删除该节点,需要找到其父节点
            _, parent_node = self.find_node(path[:len(path)-len(node.path)])
            idx = 0
            while idx < len(parent_node.children):
                if parent_node.children[idx].path == node.path:
                    break
                idx += 1
            if len(parent_node.children) == 2:
                # 说明移除该节点之后只剩下一个节点了
                parent_node.path += parent_node.children[1-idx].path
                parent_node.has_value = parent_node.children[1-idx].has_value
                parent_node.value = parent_node.children[1-idx].value
                parent_node.children = parent_node.children[1-idx].children
                return
            else:
                parent_node.children = parent_node.children[:idx] + parent_node.children[idx+1:]
                return
        elif len(node.children) == 1:
            node.path = node.path + node.children[0].path
            node.has_value = node.children[0].has_value
            node.value = node.children[0].value
            node.children = node.children[0].children
            return
        else:
            node.has_value = False
            node.value = ''

3.5 遍历操作

为了检查以上插入,删除,查找的正确性,写了一个遍历RadixTree的辅助函数:

class RadixTree:
    def traverse(self):
        full_path = ''
        self.DFS(self.root, full_path)

    def DFS(self, node, full_path):
        if node == None or len(node.children) == 0:
            return
        for i in range(len(node.children)):
            child = node.children[i]
            full_path += child.path
            print('node full path: {}, node path: {}, has_value: {}, value: {}'.format(full_path, child.path, child.has_value, child.value))
            self.DFS(child, full_path)
            full_path = full_path[:len(full_path)-len(child.path)]

4. httprouter概述

gin的路由注册采用的是httprouter。httprouter是基于radix tree实现的前缀匹配。httprouter在实现radix tree时增加了一些针对url的优化:

(1)每一种http方法一个 radix tree,这样可以提高匹配效率;
(2)提供了通配符 ":" 和 "*",其中 ":"+参数名称 表示路径参数,比如 GET /template/:user_id,其中 ":user_id" 就是一个路径参数,访问时用户输入url: /template/12345,则user_id=12345。"*"+参数名称 为匹配所有参数,结构只能为 .../.../.../*param_name,"*" 只能作为结尾参数。
 

 

posted @ 2022-10-21 10:47  晨枫1  阅读(816)  评论(0编辑  收藏  举报