day 3前缀树路由

本文介绍了如何使用Trie树实现动态路由,包括参数匹配和路由规则的存储与查找。通过Trie树结构,系统能高效地匹配URL路径,确保请求被正确路由到相应的处理函数。
摘要由CSDN通过智能技术生成

如果我们想支持类似于 /hello/:name

Trie 树简介

先来介绍一下动态路由,感觉类似于分组路由

所谓动态路由,就是一条路由规则可以匹配某一类型而非固定的路由,例如:hello/:name,可以匹配 hello/geektutu 。 hello/jack

前缀树就是所有的子节点都有一个共同的祖先节点

识记内容:

  • 参数匹配 eg:/p/:lang/doc ->/p/c/doc or /p/xzc/doc

  • 通配 * static/*filepath -> /static/fav.ico 这样的一般用于静态服务器,能够递归的匹配子路径

Trie树实现

package gee
​
import "strings"
​
type node struct {
    pattern  string  // 待匹配路由,例如 /p/:lang
    part     string  //路由中的一部分  :lang
    children []*node //子节点,例如[doc,intro]
    isWild   bool    //是否精确匹配 part含有:或 *时为true
    /*
        isWild成功保障动态路由的实现
    */
}
​
// 第一个匹配成功的节点,用于插入
// 这是用来在trie树上插入新的节点位置的函数
// 匹配单个节点在整个字符串的位置
func (n *node) matchChild(part string) *node {
    for _, child := range n.children {
        if child.part == part || child.isWild {
            return child
        }
    }
    return nil
}
​
// 所有匹配成功的节点,用于查找
// 客户端输入的URL匹配响应的路由规则
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
}
​
func (n *node) insert(pattern string, parts []string, height int) {
    if len(parts) == height { //height是用来判断递归的深度
        n.pattern = pattern //如果匹配到结尾还没有匹配成功的话,就直接加到后面
        return
    }
​
    part := parts[height]
    child := n.matchChild(part)
    if child == nil {
        //这说明也没有匹配成功,然后需要准备添加一个节点了
        //添加节点主要是判断最后面是不是通配符也就是判断isWild的值
        child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
        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, "*") {
        /*
            如果当前搜索的URL到达末尾或者说当前的节点是通配符
        */
​
        if n.pattern == "" {
            //如果如果我当前匹配的字符串为空的话
            return nil
        }
        return n
    }
​
    part := parts[height]
    //获取当前路径部分
    children := n.matchChildren(part)
​
    for _, child := range children {
        result := child.search(parts, height+1)
        if result != nil {
            return result //返回对应找到了的节点
        }
    }
    return nil //如果一直没有找到
}
​
/*
在一个典型的路由系统中,会有很多的路由规则,
每个规则对应着一个 URL 模式以及对应的处理函数。当客户端发送一个请求时,
路由系统会根据请求的 URL,逐个匹配所有的路由规则,
直到找到与请求路径完全匹配的规则为止。一旦找到了匹配的路由规则,路由系统会立即返回对应的处理函数,以处理该请求。
​
这种按需匹配的方式非常高效,因为它避免了不必要的匹配过程,只在需要时才匹配规则。同时,它也保证了每个请求都会被正确地路由到对应的处理函数,确保了系统的正常运行。
*/
​

router

package gee
​
import (
    "net/http"
    "strings"
)
​
type router struct {
    handlers map[string]HandlerFunc
    roots    map[string]*node
}
​
func newRouter() *router {
​
    return &router{
        handlers: make(map[string]HandlerFunc),
        roots:    make(map[string]*node)}
}
​
// 将路径当中的/和空去掉
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) addRouter(method string, pattern string, handler HandlerFunc) {
    parts := parsePattern(pattern)
​
    key := method + "-" + pattern
    //查看是否有这种方法
    _, ok := r.roots[method] //如果没有这种HTTP方法
    //需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
    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) //将这个路径节点化
    params := make(map[string]string) //用于存储匹配到的参数
    root, ok := r.roots[method]
    //首先找这个方法是否存在
    if !ok {
        return nil, nil
    }
​
    n := root.search(searchParts, 0)
    //开始在此方法下面的路由器中搜索
    //n是对应的节点
    if n != nil {
        parts := parsePattern(n.pattern)
        for index, part := range parts {
            if part[0] == ':' {
                //如果是动态
                params[part[1:]] = searchParts[index] //searchParts是原来的节点
                //将参数名和对应的值存储到对应的params
            }
​
            if part[0] == '*' && len(part) > 1 {
                params[part[1:]] = strings.Join(searchParts[index:], "/")
                //
                break
            }
        }
        return n, params
        //返回对应的节点和与请求路径匹配的路由规则
    }
    return nil, nil
}
​
/*
也就是说我返回的是一个节点和
我匹配成功的那个节点的路由规则和
我传过来路径匹配成功时刻节点
所构成的键值对
*/
​
func (r *router) handle(c *Context) {
    key := c.Method + "-" + c.Path
    if handler, ok := r.handlers[key]; ok {
        handler(c) //后面的c应该改变了
    } else {
        c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
    }
}
​

对代码进行人工调试:

我们发现就第一条语句 gee.New():而言

func New() *Engine {
    return &Engine{router: newRouter()}
}
​
func newRouter() *router {
​
    return &router{
        handlers: make(map[string]HandlerFunc),
        roots:    make(map[string]*node)}
    //现在我们路由包括了一个处理程序还有一个用于查找/储存URL的节点
}
​
type HandlerFunc func(*Context)
​
type Context struct {
    //origin object 起始对象
    Writer http.ResponseWriter
    Req    *http.Request
    //request info
    Path   string
    Method string
    Params map[string]string
    //respnse info
    StatusCode int
}
​
type node struct {
    pattern  string  // 待匹配路由,例如 /p/:lang
    part     string  //路由中的一部分  :lang
    children []*node //子节点,例如[doc,intro]
    isWild   bool    //是否精确匹配 part含有:或 *时为true
    /*
        isWild成功保障动态路由的实现
    */
}

我们现在Engine还是主要包含路由器router,router里面现在又包含了一个处理程序和一个Tree的结构

r.GET("/", func(c *gee.Context) {
        c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
    })
    
    func (engine *Engine) GET(pattern string, handler HandlerFunc) {
    engine.addRoute("GET", pattern, handler)
}
​
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
    engine.router.addRoute(method, pattern, handler)
}
​
​
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
    parts := parsePattern(pattern)
​
    key := method + "-" + pattern
    //查看是否有这种方法
    _, ok := r.roots[method] //如果没有这种HTTP方法
    //需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
    if !ok {
        r.roots[method] = &node{} //创建一个根节点
    }
    r.roots[method].insert(pattern, parts, 0)
    //对于新路径的增加
    r.handlers[key] = handler
}
​
func (n *node) insert(pattern string, parts []string, height int) {
    //这个函数的主要需求是插入一个新的node
    if len(parts) == height { //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)
    //注意:我们循环的入口是child调用的,这意味着,我们每次进行matchChild是遍历循环当前节点的所有叶子节点,如果找到了,就会继续往下面推进
}
​
​
func (n *node) matchChild(part string) *node {
    for _, child := range n.children {
        if child.part == part || child.isWild {
            return child
        }
    }
    return nil
}
​
type node struct {
    pattern  string  // 待匹配路由,例如 /p/:lang
    part     string  //路由中的一部分  :lang
    children []*node //子节点,一般来说子节点其实就是一条路径
    isWild   bool    //是否精确匹配 part含有:或 *时为true
    /*
        isWild成功保障动态路由的实现
    */
}
r.GET("/hello", func(c *gee.Context) {
        // expect /hello?name=geektutu
        c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
        /*
        这里相当于回应一个响应消息,我想知道怎么回应的
        通过这个方法将Context准备完成
        ->handler准备完成
        ->writer准备完成,其实我感觉handler是辅助writer的
       现在开始复习第一节的内容,我们设置一个函数其实是为了反馈到
*/
    }
    /*
    第一个参数会保存到方法GET
    
    */)
​
func (c *Context) Query(key string) string {
    return c.Req.URL.Query().Get(key) //获取指定名称的查询参数的值
    /*
            这里的Query是url.Values类型的方法
            用于获取指定键的第一个值
        http://example.com/?name=John&age=30&name=Jane
        这个查询参数包含了两个键值对:name=John 和 age=30,其中键 name 对应了两个值:John 和 Jane。
​
        如果我们使用 url.Values 类型来表示这个查询参数,
        并调用 Get 方法来获取 name 键对应的值,
        那么 Get 方法会返回第一个值,也就是 John。
    */
}
​
​
func (c *Context) String(code int, format string, values ...interface{}) {
    c.SetHeader("Content-Type", "text/plain")
    c.Status(code) //这里设置状态码
    c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
    /*
            format是一个字符串参数,用于指定返回的纯文本格式
        value...interface{} 这个是用来传递多个值,用于填充format字符串中的字符串
    */
}
​
​
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
    parts := parsePattern(pattern)
​
    key := method + "-" + pattern
    //查看是否有这种方法
    _, ok := r.roots[method] //如果没有这种HTTP方法
    //需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
    if !ok {
        r.roots[method] = &node{} //创建
    }
    r.roots[method].insert(pattern, parts, 0) //我们现在主要是研究匿名函数的设置
    r.handlers[key] = handler//可以发现我们将匿名函数设置在了一个map当中
}
​
type router struct {
    handlers map[string]HandlerFunc
    roots    map[string]*node
}
​
​
type HandlerFunc func(*Context)
​
type Context struct {
    //origin object 起始对象
    Writer http.ResponseWriter
    Req    *http.Request
    //request info
    Path   string
    Method string
    Params map[string]string
    //respnse info
    StatusCode int
}
​
这个函数的目的最终是生成Context,通过函数的不断封装,转化,最终会成功生成一个Context
​

感觉自己消化的并不是很好,开始复习前面的内容

Handler是一个 接口,需要实现方法ServeHTTP,

我们的ListenAndServe方法的第二个参数类型就是Handler,也就是说只要实现了Handler接口,就可以作为第二个参数

所有的HTTP请求,都会交给该实例处理

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值