Go语言动手写Web框架 - Gee第三天 前缀树路由Router

51 阅读3分钟

Go语言动手写Web框架 - Gee第三天 前缀树路由Router

前置知识点

Tire树


class Trie {
    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        children = new Trie[26];
        isEnd = false;
    }
    
    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if (node.children[index] == null) {
                node.children[index] = new Trie();
            }
            node = node.children[index];
        }
        node.isEnd = true;
    }
    
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node != null && node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }

    private Trie searchPrefix(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            int index = ch - 'a';
            if (node.children[index] == null) {
                return null;
            }
            node = node.children[index];
        }
        return node;
    }
}

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode-ti500/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

B站: 【【数据结构 10】Trie|前缀树|字典树】 https://www.bilibili.com/video/BV1Az4y1S7c7/?share_source=copy_web&vd_source=69c030752627360d1102b31b7703ff1f
相关leetcode题 208、720、692

大局观看架构演进和代码结构

第一天:实现engine,并且用hashmap 一一映射关系来存储处理所有Http请求和处理函数,注册、存储、查找路由关系全存在engine对象上
一个http需要监听一个工作端口并且与之伴随的有一个engine来处理所有的请求


而这个engine要解决各类请求的路由关系于是有了
 type Engine struct {
	router map[string]HandlerFunc
}

第二天:把engine类进行拆解,封装了回应类context
engine类拆解成两大块:1、router主要用来保存路由关系
                   2、engine 对各类请求的注册Get(/url,fun),POST(/url,fun)
                   3. context主要是对http.request,http.responsewrite 相关的进行封装,因为每次回应都需要status,header等通用属性,加一层封装可以简化
                   
第三天:router继续细化,分成tire树和函数
type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}
                   

tire.go 源码

//定义存路由表的结构体trie树
type node struct {
	pattern  string // 待匹配路由,例如 /p/:lang
	part     string // 路由中的一部分,例如 :lang
	children []*node // 子节点,例如 [doc, tutorial, intro]
	isWild   bool // 是否精确匹配,part 含有 : 或 * 时为true
}



// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(part string) *node {
	for _, child := range n.children {
		if child.part == part || child.isWild {
			return child
		}
	}
	return nil
}
// 所有匹配成功的节点,用于查找
func (n *node) matchChildren(part string) []*node {
	nodes := make([]*node, 0)
	for _, child := range n.children {
		if child.part == part || child.isWild {
			nodes = append(nodes, child)
		}
	}
	return nodes
}

/*
pattern 举例: /users/list,/users/*nfo,/users/:name

insert是递归调用,条件是当height高度等价于parts的个数,height是随insert递调调用不断递增,同时height做为parts下标取不断取
pattern拆分过的下一个元素。

这里的parts是pattern.split('/')拆分的字符数组。

height是结束条件,是通过递归累加height,当满足了parts个数就结束递归,并且在最后一个节点保存这整段请求路径的路径到节点的pattern里
*/

/*
 关于插入逻辑,当完成整个pattern的match,最后一个节点存放了整串pattern的数据
 比如GET /p/:any/hello,那么在tire树里的节点链分别是四个树节点
  get -> p -> (any) / hello ,只有在最后一个hello节点里,才存放了"/p/:any/hello"这个也是用来当成完全匹配的条件
  
*/

func (n *node) insert(pattern string, parts []string, height int) {
    // height是结束条件,是通过递归累加height,当满足了parts个数就结束递归,并且在最后一个节点保存这整段请求路径的路径到节点的pattern里
	if len(parts) == height {
		n.pattern = pattern
		return
	}
	
	//取当前height的part元素
	part := parts[height]
	
	//查找匹配part的树节点
	child := n.matchChild(part)
	if child == nil {
	    //新建儿子节点child:isWild由或条件来决定,判断每个part元素的第0个字符。
		child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
		
		//新建的儿子节点插入n.children里
		n.children = append(n.children, child)
	}
	// 继续递归
	child.insert(pattern, parts, height+1)
}


func (n *node) search(parts []string, height int) *node {
    //结束条件
	if len(parts) == height || strings.HasPrefix(n.part, "*") {
	   /*
           参见addroute和insert,addroute调用	r.roots[method].insert(pattern, parts, 0)
           
         */
		if n.pattern == "" {
			return nil
		}
		//最终返回整个pattern在tire的最后一个match节点
		return n
	}

	part := parts[height]
	
	// 返回多个孩子节点
	children := n.matchChildren(part)

    //多路递归树rec tree
	for _, child := range children {
		result := child.search(parts, height+1)
		if result != nil {
			return result
		}
	}

	return nil
}

Router day3-router/gee/router.go

Trie 树的插入与查找都成功实现了,接下来我们将 Trie 树应用到路由中去吧。我们使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中,还解析了:*两种匹配符的参数,返回一个 map 。例如/p/go/doc匹配到/p/:lang/doc,解析结果为:{lang: "go"}/static/css/geektutu.css匹配到/static/*filepath,解析结果为{filepath: "css/geektutu.css"}

type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}

// roots key eg, roots['GET'] roots['POST']
// handlers key eg, handlers['GET-/p/:lang/doc'], handlers['POST-/p/book']
    
func newRouter() *router {
	return &router{
		roots:    make(map[string]*node),
		handlers: make(map[string]HandlerFunc),
	}
}

// Only one * is allowed
// 请求pattern以/拆分,遇到*就break,返回parts[]
func parsePattern(pattern string) []string {
	vs := strings.Split(pattern, "/")

	parts := make([]string, 0)
	for _, item := range vs {
		if item != "" {
			parts = append(parts, item)
			if item[0] == '*' {
				break
			}
		}
	}
	return parts
}

/*

*/
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
	parts := parsePattern(pattern)

	key := method + "-" + pattern
	_, ok := r.roots[method]
	if !ok {
		r.roots[method] = &node{}
	}
	r.roots[method].insert(pattern, parts, 0)
	r.handlers[key] = handler
}

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
	searchParts := parsePattern(path)
	//存key和值,比如:any 对应了a,b,c  或 * 对应 /x/y/z
	params := make(map[string]string)
	// 取得方法所对应的节点
	root, ok := r.roots[method]

	if !ok {
		return nil, nil
	}

	// 这里的root是method所对应的节点,找到相关节点
	n := root.search(searchParts, 0)

	if n != nil {
	    //因为pattern存放在最后一个节点的pattern所以这里n.pattern
	  
	    /*
        getRoute 函数中,还解析了:和*两种匹配符的参数,返回一个 map 。例如/p/go/doc匹配到/p/:lang/doc,解析结果为:{lang: "go"},/static/css/geektutu.css匹配到/static/*filepath,解析结果为{filepath: "css/geektutu.css"}。   
	    */
	    
	    
		parts := parsePattern(n.pattern)
		for index, part := range parts {
			if part[0] == ':' {
			    // 去掉:当key,同时用index取出对应的part存到params
				params[part[1:]] = searchParts[index]
			}
			if part[0] == '*' && len(part) > 1 {
			
			     // 去掉*开头字符当key,同时用index:切片取出所有元素,用'/'连接成一个字符串
				params[part[1:]] = strings.Join(searchParts[index:], "/")
				break
			}
		}
		return n, params
	}

	return nil, nil
}

Context与handle的变化

在 HandlerFunc 中,希望能够访问到解析的参数,因此,需要对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到Params中,通过c.Param("lang")的方式获取到对应的值。

day3-router/gee/context.go

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	// 新增这个参数
	Params map[string]string
	// response info
	StatusCode int
}

//取key
func (c *Context) Param(key string) string {
	value, _ := c.Params[key]
	return value
}

day3-router/gee/router.go

func (r *router) handle(c *Context) {
    //在tire树里取取节点和params
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
	    // params赋给c
		c.Params = params
		// 请求key
		key := c.Method + "-" + n.pattern
		
		/* 取得函数并执行,函数参数是由最早的 type HandlerFunc func(http.ResponseWriter, *http.Request)
			演变成 type HandlerFunc func(c *context))
		*/
		
		r.handlers[key](c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

router.go的变化比较小,比较重要的一点是,在调用匹配到的handler前,将解析出来的路由参数赋值给了c.Params。这样就能够在handler中,通过Context对象访问到具体的值了。