GO七天开发挑战:7天实现Web框架-Gee(day 3 前缀树路由Router)

学习资料来自:GitHub - geektutu/7days-golang: 7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列https://github.com/geektutu/7days-golang

Go语言动手写Web框架 - Gee第三天 前缀树路由Router | 极客兔兔 (geektutu.com)icon-default.png?t=M85Bhttps://geektutu.com/post/gee-day3.html

Day3 前缀树路由Router

Trie树实现

       1. 数据结构

        其中pattern表示当前节点可以匹配的路由;part表示当前节点对应的路由某一部分的内容;children则存储当前节点的子节点,即后续可匹配路由;isWild 则表示当前节点是否是模糊匹配。(模糊匹配举例,/:lang/ 可匹配/go/或者/aaa/等内容;/*filepath/ 匹配后续文件路径)

type node struct {
	pattern  string  // 待匹配的路由
	part     string  // 节点内容,即树中的某一部分,例如:lang,about等
	children []*node // 当前节点的子节点
	isWild   bool    // 是否模糊匹配,当含有:或者*时为 true
}

       2. node对象对应的方法

        1)匹配

        (1)查找子树中第一个匹配:遍历当前节点对象的所有子节点,遇到第一个 part 部分可匹配的节点即返回。

func (n *node) matchChild(part string) *node {
	for _, child := range n.children { // 遍历当前节点的所有子节点
		if child.part == part || child.isWild {
			return child
		}
	}
	return nil
}

        (2) 查找子树中所有可能的匹配:遍历当前节点对象的所有子节点,将所有可匹配的节点放入 slice 中,遍历所有节点后,返回 slice

func (n *node) matchChilren(part string) []*node {
	matchedNodes := make([]*node, 0)
	for _, child := range n.children {
		if child.part == part || child.isWild {
			matchedNodes = append(matchedNodes, child)
		}
	}
	return matchedNodes
}

        2) 插入和查询

        (1)插入:对应路由中的注册过程,根据目标路由内容,在当前节点的子树中找第一个可匹配的节点。如果可匹配的节点不存在,根据目标路由内容,构建新的节点,并将该新节点插入到当前节点的子树中。然后再根据完整路由,插入下一个路由内容,递归完成整个路由内容的节点插入,即完成路由的注册。

func (n *node) insert(pattern string, parts []string, height int) {
	if len(parts) == height { //已经是最后一个节点,不需要再插入,更新已有待匹配字符即可
		n.pattern = pattern
		return
	}
	part := parts[height]       // 当前节点内容
	child := n.matchChild(part) // 找第一个匹配的树节点(当前节点准备插入的位置)
	if child == nil {           // 未找到可匹配的节点,需要进行插入操作,添加为当前节点的子节点
		child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
		n.children = append(n.children, child)
	}
	child.insert(pattern, parts, height+1) // 递归到下一层,使用下一个part继续插入
}

        (2)查询:对应路由中的注册过程,根据已有的trie树,查找所给的路由对应的树节点。当查找到最后一个节点或者当前节点包含*通配符,证明路由成功匹配,返回当前节点。否则查找子树中所有可以匹配的节点,遍历这些节点,递归查找下一层是否匹配,直至找到完全匹配路由的叶子节点,并将该叶子节点返回。

func (n *node) search(parts []string, height int) *node {
	if len(parts) == height || strings.HasPrefix(n.part, "*") { // 查找到最后一个节点,或者匹配到 *
		if n.pattern == "" {
			return nil
		}
		return n
	}
	part := parts[height]            // 获取当前节点内容
	children := n.matchChilren(part) // 查找n的子树中所有符合匹配的节点

	for _, child := range children {
		result := child.search(parts, height+1) // 递归查找下一层节点是否匹配
		if result != nil {                      // 匹配则返回叶子节点
			return result
		}
	}
	return nil // 未找到可匹配的,返回nil
}

路由Router实现

       1. 数据结构

        其中,roots 是请求的路由对应的树根节点,用于判断路由是否匹配,起始的树节点为 GET/POST等请求类型节点(key eg, roots['GET'] roots['POST']);handlers存储对应的响应处理函数,键值通常为 “请求类型-完整路由” 例如 handlers['GET-/p/:lang/doc'], handlers['POST-/p/book']。

type router struct {
	roots    map[string]*node       // 请求的路由对应的树根节点,用于判断路由是否匹配(key eg, roots['GET'] roots['POST'])
	handlers map[string]HandlerFunc // 对应的处理函数(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),
	}
}

        2. 路由解析

        根据”/“对路由进行划分,将其拆分成可作为node.part的各个部分,注意:理由中只能有一个*,*后的所有内容均被视为文件路径。

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
}

       3. Router对象对应的方法

        1)注册与匹配

        (1)注册:根据所给的请求类型以及路由,构建可匹配的 trie 树。将路由拆分后依次插入到请求类型对应的子树中,同时在handlers中存储对应的响应方式。

func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
	parts := parsePattern(pattern) // 完成对路由的分析,获取其中的各个部分
	key := method + "-" + pattern  // 构建router中handlers的注册路由
	_, ok := r.roots[method]
	if !ok { // 该方法还没有树根节点,添加一个空节点便于插入
		r.roots[method] = &node{}
	}
	r.roots[method].insert(pattern, parts, 0) // 像树中添加该路由的各个节点
	r.handlers[key] = handler
}

        (2)匹配: 根据请求类型以及对应的路由,返回匹配的节点以及对应的参数列表(模糊匹配部分的内容)

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
	searchParts := parsePattern(path) // 获取待查找路由的各个部分
	params := make(map[string]string) // 模糊匹配对应的匹配内容
	root, ok := r.roots[method]       // 获取该类型请求对应的树节点

	if !ok {
		return nil, nil
	} // 不存在该类型请求的路由,直接返回空

	n := root.search(searchParts, 0) // 查找是否存在匹配的路由节点

	if n != nil { //节点匹配
		parts := parsePattern(n.pattern) // 解析当前找到的节点的路由
		for index, part := range parts { // 遍历路由的各个部分
			if part[0] == ':' { // 遇到模糊匹配:
				params[part[1:]] = searchParts[index] // key:除匹配符(:)的其余字符,value:待匹配路由的对应位置内容
			}
			if part[0] == '*' && len(part) > 1 { // 遇到模糊匹配*
				params[part[1:]] = strings.Join(searchParts[index:], "/") // key:除匹配符(*)的其余字符,value:待匹配路由之后的内容
				break                                                     // 后续可不再匹配
			}
		}
		return n, params // 返回匹配的节点,以及模糊匹配对应的内容
	}
	return nil, nil // 没有匹配的节点,直接返回空
}

         2)根据路由给出响应

        根据请求的类型以及路由找到匹配的节点后,返回对应的响应内容。

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path) // 根据请求中的路由进行匹配
	if n != nil {                             // 查找到对应的路由,返回对应的响应
		c.Params = params
		key := c.Method + "-" + n.pattern
		r.handlers[key](c)
	} else { // 未查找到对应的路由,返回未找到路由的响应
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

Context 修改

        对context结构进行如下修改:

        1)结构体中增加  Params: make(map[string]string),Params存储模糊匹配对应的内容(例如 param[”:lang“]=”go“),需要时可通过Param获取相应的模糊匹配参数。

        2)构造函数增加对Params的初始化。

        3)新增方法,根据所给的模糊匹配的内容,返回对应路由中的匹配内容

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

运行测试

         测试的main函数

func main() {
	r := gee.New()
	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "

Hello Gee

") }) r.GET("/hello", func(c *gee.Context) { c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) }) r.GET("/hello/:name", func(c *gee.Context) { // expect /hello/geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) r.GET("/assets/*filepath", func(c *gee.Context) { c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")}) }) r.POST("/login", func(c *gee.Context) { c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), }) }) r.Run(":9999") }

        命令行使用 curl 进行路由测试,返回指定内容 

你可能感兴趣的:(Go,七天开发挑战,golang)