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()
}

 

 

 

posted @ 2022-04-29 00:12  许伟强  阅读(423)  评论(0编辑  收藏  举报