gin框架路由源码分析

gin框架的路由结构剖析

  1. 路由是web框架的核心功能,在学习gin路由前,路由是这样的,比如定义了两个路由/user/get, /user/delete
    则会构造出拥有三个节点的路由树,根节点是user,两个子节点分别是:get、delete
    上述是一种实现路由的方式,且比较直观,容易理解,对url进行切分,比较,时间复杂度是O(2n)

  2. gin的路由使用了类似前缀树的数据结构,只需要遍历一遍字符串即可,时间复杂度是O(n)
    当然,对于一次http请求来说,这点路由寻址优化可以忽略不急

  3. engine的常用设置

func main() {
	r := gin.Default()

	// 下面两种方式2选1即可,推荐使用第二种
	//r.TrustedPlatform = "Client-IP"  // 设置客户端真实ip的请求头
	// 设置受信任的代理
	r.SetTrustedProxies([]string{"127.0.0.1"})

	// 设置url中的大写自动转小写,..和//自动移除,
	r.RedirectFixedPath = true

	// 开启请求方法不允许,并且返回状态码405
	r.HandleMethodNotAllowed = true

	// 设置允许从远程客户端的哪个header头中获取ip(需搭配设置受信任的代理一起使用)
	r.RemoteIPHeaders = append(r.RemoteIPHeaders, "Client-IP")

	// TrustedPlatform 设置可信任的平台,如果增加了此项配置,那么获取客户端真实ip的时候
	// 会优先从请求头中的Real-IP获取,获取到了直接返回,获取不到才会从RemoteIPHeaders中去获取
	// 一般不这样设置,推荐从RemoteIPHeaders中获取,当然前提需要设置受信任的代理, 如果不想设置受信任的代理,那么可以直接从TrustedPlatform中直接获取
	//r.TrustedPlatform = "Real-IP"

	r.GET("/user/:name", routeUse)
	r.Run(":8000")
}

路由的种类

  1. 静态路由:
    框架/用户提前生成一个路由表,一般是map结构,key为URL上的path,value为代码执行点(处理函数),
    优点:只需要读取map,没有任何开销,速度奇快
    缺点:无法正则匹配路由,只能一一对应,模糊匹配的场景无法使用

  2. 动态路由:
    用户定义好路由匹配规则,框架匹配路由时,根据规则动态去规划路由
    优点:适应性强,解决了静态路由的缺点
    缺点:相比静态路由有开销,具体视算法和路由匹配规则而定

gin框架路由实现原理

gin框架作为一个轻量级的快速框架,采用的是前缀树的方式实现的动态路由

我们以下面代码为例,看看gin是如何实现路由的

func main() {
	r := gin.New()
	r.GET("/user/:name", routeUse)
	r.Run(":8000")
}

func routeUse(ctx *gin.Context) {
	ctx.String(200, "ok")
}

上面我们定义了一个路由 /user/:name , 它会精确匹配/user/:name,而不会匹配/user、/user/、/user/abc/a
接下来我们跟着源码去看看gin是如何实现的

初始化框架

r := gin.New()生成了一个engine对象,engine对象是整个框架的核心,也包含对路由的操作和许多成员变量
其中包含了路由要执行的任务链Handlers HandlersChain, 方法树trees methodTrees

定义路由

r.GET("/user/:name", routeUser)定义了一个GET请求,模糊匹配/user/:name
步骤1:
源码 routergroup.go=>group.calculateAbsolutePath()
将相对路径/user/:name join为绝对路径,因为有可能是个路由组,前面还有一些路径,路由组稍后介绍

步骤2:
源码 routergroup.go=>group.combineHandlers()
判断HandlersChain的长度,不能超过大于等于math.MaxInt8 / 2,也就是不能超过31个,

步骤3:
源码:routergroup.go=>group.engine.addRoute()
装入路由存入 engine.trees 变量

步骤4:
返回当前对象,能达到链式操作的目的

访问路由

输入localhost:8080/user/abc, 首先进入net/http和核心库的走一圈,最终到engine的ServeHttp方法,
将ResponseWriter和Request写入到从pool中新申请的Context中,然后调用engine的handleHTTPRequest方法,
将Context传入,循环之前注册的路由树engine.trees,

匹配method部分源码,gin.go=>handleHTTPRequest():

t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
    if t[i].method != httpMethod {
        continue    //判断请求method是否相等如果不等则continue
    }
    root := t[i].root
    // Find route in tree
    value := root.getValue(rPath, c.params, unescape)
    //匹配路由的核心算法,如果匹配出来的value为空则直接退出。
    ...
    ...
}

匹配路由核心算法源码:tree.go=>getValue():

func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
walk: // Outer loop for walking the tree
    for {
        prefix := n.path
        if len(path) > len(prefix) {
            if path[:len(prefix)] == prefix {
                //前缀匹配
                ...

                switch n.nType {
                case param:
                ...
                (*value.params)[i] = Param{
                    Key:   n.path[1:],
                    Value: val,
                }// 匹配出参数
            }
        }
        if path == prefix {
            // 完全匹配,无需匹配参数
            ...
        }
    }
}

前缀树算法

前缀树的本质就是一颗查找树,有别于普通的查找树,它适用于一些特殊的场合,比如用于字符串的查找,
比如在一个路由的场景中,有1W个路由字符串,每个字符串长度不等,我们可以用数组来存储,查找的时间复杂度是O(N),
也可以用map来存储,查找的时间复杂度是O(1),但是没办法解决动态匹配的问题,如果使用前缀树的时间复杂度是O(logn),
同时也可以解决动态匹配的问题

下图展示了前缀树的原理,有一下6个字符串,如果要查找cat字符串,步骤如下,

  1. 先拿到c和root的第一个节点a比较,如果不等,在继续和父节点root的第二个节点比较,直到拿到c
  2. 然后再拿到a和父节点c的第一个节点a比较,结果相等,则继续往下,
  3. 在拿到字符t和父节点a的第一个节点t比较,结果相等,则完成

同理在路由中,前缀树可以规划成如下,

具体查找方法和上面一致。

参考文章

posted @ 2022-05-18 22:50  专职  阅读(902)  评论(0编辑  收藏  举报