• GO正则匹配路由处理和gozero的请求处理源码浅析


    先简单实现一个 http 服务端,这个服务支持正则匹配路由处理:

    package main
    
    import (
        "fmt"
        "net/http"
        "regexp"
    )
    
    // https://www.jianshu.com/p/4e8cdf3b2f88
    
    // client -> Request -> Multiplexer(router)->handler ->Response -> client
    
    // 路由定义
    type routeItem struct {
        pattern string                                       // 正则表达式
        f       func(w http.ResponseWriter, r *http.Request) //Controller函数
    }
    
    // 路由添加
    var routePath = []routeItem{
        {"^/user.*$", UserHandler},
        {"^/company.*$", CompanyHandler},
    }
    
    func UserHandler(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("hello user"))
    }
    func CompanyHandler(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("hello company"))
    }
    
    func IndexHandler(w http.ResponseWriter, r *http.Request) {
        isFound := false
        for _, p := range routePath {
            // 这里循环匹配Path,先添加的先匹配
            reg, err := regexp.Compile(p.pattern)
            if err != nil {
                continue
            }
            if reg.MatchString(r.URL.Path) {
                isFound = true
                p.f(w, r)
            }
        }
        if !isFound {
            // 未匹配到路由
            fmt.Fprint(w, "404 Page Not Found!")
        }
    }
    
    func main() {
        http.HandleFunc("/", IndexHandler)
        err := http.ListenAndServe("127.0.0.1:8899", nil)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
    }

    上面的例子就得到一个结论:注册路由的方式可以是 HandleFunc ,其中直接将 /  匹配的请求全部交给 IndexHandler处理,在函数里面决定处理结果

    先是注册不同路由对应的handler,然后是 listen address port 

    开始阅读 go-zero的源代码:

    无非两行:

    // 注册路由
    handler.RegisterHandlers(server, ctx)
    
    // 启动服务
    server.Start()

    最终启动服务 go-zero 框架用到的也是 *http.Server.ListenAndServe  

    其中 http.Server 对象是

    &http.Server{
        Addr:      fmt.Sprintf("%s:%d", host, port),
        Handler: handler,
    }

    注册的 handler 也就是路由的处理函数

    github.com/zeromicro/go-zero/rest/engine.go 的 internal.StartHttp 

    传递的第三个参数 Router 是 http.Handler 的继承

    所以很明显,整个 go-zero的web系统的路由注册在这里

    那么,gozero究竟是否支持模糊匹配呢?是否支持正则匹配呢?

    寻找的思路是:go的net/http最终都会转给

    func(w http.ResponseWriter, r *http.Request)

    的实现处理,gozero的模块划分比较清晰很容易定位到:

    /github.com/zeromicro/go-zero/rest/router/patrouter.go   ->   ServeHTTP(w http.ResponseWriter, r *http.Request)

    这个函数是每次请求的最终处理函数,那么它是否支持了正则匹配呢?

     很明显,主要取决于工具包

    func (t *Tree) Search(route string) (Result, bool)

     Search的实现逻辑

    然后是按照分隔符 '/' 严格匹配查找路径对应的handler的,所以不支持正则路由匹配

    Search的代码

    // Search searches item that associates with given route.
    func (t *Tree) Search(route string) (Result, bool) {
        if len(route) == 0 || route[0] != slash {
            return NotFound, false
        }
    
        var result Result
        ok := t.next(t.root, route[1:], &result)
        return result, ok
    }
    
    func (t *Tree) next(n *node, route string, result *Result) bool {
        log.Println("当前的存储的路径解析的参数结果集",result)
        log.Println("next route = ",route)
        log.Println("n.item = ",n.item)
        if len(route) == 0 && n.item != nil {
            result.Item = n.item
            return true
        }
        for i := range route {
            log.Println("i",i)
            if route[i] != slash {
                continue
            }
            token := route[:i]
            return n.forEach(func(k string, v *node) bool {
                fmt.Println("token=",token,"k",k)
                // 判定k的第一个字符是不是:,如果是将作为一个请求参数
                // if pat[0] == colon { // colon是:
                r := match(k, token)
                // 截断已经读取过的
                log.Println("last route is",route[i+1:])
                if !r.found || !t.next(v, route[i+1:], result) {
                    return false
                }
                log.Println("r.named",r.named)
                if r.named {
                    addParam(result, r.key, r.value)
                }
                return true
            })
        }
    }

    写的有点冗余也挺绕的,就是循环每一个路径的字符,不断和常量 : / 比对,找到路径对应的 handler 的同时获取 :path 这种占位符的变量

    /github.com/zeromicro/go-zero/core/search/tree.go

    上面是比对的字符的两个常量;

     路由匹配的难点在于它的route和handler的存储结构类似

    {
       school :{
           handler,
           list:{
                    handler,
                    get:{
                            * :{
                                 }
                          }
                }
        }
    }

    它是一个以路由item( 路径 / 分割的字母 )为key的一个无限级递归查找 handler, 

    并且每次查找的时候都看看当前比如 /user/list/get/:id  看看当前的list有没有:开头,如果是:开头将认定为是参数变量添加到map参数集合之中

    支持前缀路由匹配后的next 逻辑

    const ( any = "*" )
    func (t *Tree) next(n *node, route string, result *Result) bool {
        if len(route) == 0 && n.item != nil {
            result.Item = n.item
            return true
        }
        for i := range route {
            // 将当前的路径,递归查找逐个/的对应的handler
            if route[i] != slash {
                continue
            }
            token := route[:i]
            return n.forEach(func(k string, v *node) bool {
                if k == any {
                    // any是字符串"*"直接使用当前*号所在的handler处理/支持前缀匹配
                    result.Item = v.item
                    return true
                }
                r := match(k, token)
                if !r.found || !t.next(v, route[i+1:], result) {
                    return false
                }
                if r.named {
                    addParam(result, r.key, r.value)
                }
                return true
            })
        }
        // 直到查找到最后的一个 / 后面的单词时候,将返回当前handler,将判定是否返回当前handler
        // 如果最后一个值不是 :name的这样的参数,但是在前面又没有找到handler - 将直接返回并且不给result.Item赋值
        // 那么最后会被认为route not found
        return n.forEach(func(k string, v *node) bool {
            // 检查当前route是否是参数,如果是参数那么直接赋值并且返回当前handler
            if r := match(k, route); r.found && v.item != nil {
                result.Item = v.item
                if r.named {
                    addParam(result, r.key, r.value)
                }
                return true
            }
            if k == any {
                // any是字符串"*"直接使用当前*号所在的handler处理/支持前缀匹配
                result.Item = v.item
                return true
            }
            return false
        })
    }

    所以在 return false之前,判定一下当前的路由配置,如果是*那么直接赋 handler 处理,就可以实现模糊匹配

    if k == "*" {
        // 直接使用当前*号所在的handler处理
        result.Item = v.item
        return true
    }

    答案如上;

     还有一种使用方式,是覆盖 routeNotFound Hander

    https://github.com/zeromicro/zero-examples/blob/main/http/wildcard/main.go

    代码如下

    func main() {
        flag.Parse()
    
        engine := rest.MustNewServer(rest.RestConf{
            ServiceConf: service.ServiceConf{
                Log: logx.LogConf{
                    Mode: "console",
                },
            },
            Host:     "localhost",
            Port:     *port,
            MaxConns: 500,
        }, rest.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if strings.HasPrefix(r.URL.Path, "/api/any/") {
                fmt.Fprintf(w, "wildcard: %s", r.URL.Path)
            } else {
                http.NotFound(w, r)
            }
        })))
        defer engine.Stop()
    
        engine.AddRoute(rest.Route{
            Method:  http.MethodGet,
            Path:    "/api/users",
            Handler: handle,
        })
        engine.Start()
    }
  • 相关阅读:
    微众银行面试小总结
    关于撑开父容器高度的小探讨
    2015年9月阿里校招前端工程师笔试题
    高性能JavaScript 重排与重绘
    高性能JavaScript DOM编程
    纯CSS3动画实现小黄人
    JS+css3实现图片画廊效果总结
    新游戏《机械险境》
    Twitter "fave"动画
    fragment 与 activity
  • 原文地址:https://www.cnblogs.com/xuweiqiang/p/16205123.html
Copyright © 2020-2023  润新知