gin框架的路由

466 阅读5分钟

一、概览

Gin 是在 Golang HTTP 标准库 net/http 基础之上的再封装,两者的交互边界如下图:

gin.png

首先看一下gin的简单使用:

func main() {
	e := gin.Default()
	e.GET("/ping", func(context *gin.Context) {
		context.JSON(200, "pong")
	})
	e.Run("127.0.0.1:8080")
}

Run()方法底层实际调用的是还是 net/httpListenAndServe(addr, handler)

func (engine *Engine) Run(addr ...string) (err error) {
	// ... 省略代码
        
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)  《=====底层还是http.ListenAndServe()
	return
}

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

二、客户端的request数据如何流入gin

因为 Run() 方法调用的是 http.ListenAndServe ,所以 gin 建立 socket 的过程,accept 请求的过程与 net/http 没有差别,net/http 时如何建立socket和accept的,看我的上一篇文章 net/http 是如何处理请求的

唯一不同的就是在获取 ServeHTTP 的位置:

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	// Handler 通过 http.ListenAndServe() 进行赋值 
	// Gin框架通过给Handler赋值为engine (err = http.ListenAndServe(address, engine))
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req) 《=====此处开始和 net/http 中调用的ServeHTTP()有区别
}

gin 的 ServeHTTP:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	//1.从 临时对象池 取一块儿内存
	c := engine.pool.Get().(*Context)
	//2.初始化内存
	c.writermem.reset(w)
	//3.
	c.Request = req
	c.reset()

	//4.处理请求
	engine.handleHTTPRequest(c)

	//5.归还内存
	engine.pool.Put(c)
}

此方法 ServeHTTP 是 gin 的第一步,将请求转入 gin 的处理流程。

流程图:

httpReq.jpg

三. gin的路由是如何实现的

1. 路由的设计

  • 在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵「前缀树」;
  • 树的节点按照 URL 中的 / 符号进行层级划分;
  • URL 支持 :name 形式的名称匹配,还支持 *subpath 形式的路径通配符;
  • 每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。

9个方法分别对应一颗前缀树:

GET、POST、HEAD、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE

树的节点按照 URL 中的 / 符号进行层级划分,例如,路由的地址是:

  • /hi
  • /hello
  • /:name/:id

那么gin对应的树会是这个样子的:

Trie.jpg

节点的数据结构如下:

type node struct {
   path      string
   indices   string
   wildChild bool
   nType     nodeType
   priority  uint32
   children  []*node // child nodes, at most 1 :param style node at the end of the array
   handlers  HandlersChain
   fullPath  string
}

关于前缀树,参考这篇: Radix树 和这篇 gin 框架之路由前缀树初始化分析

路由匹配规则:

路由参数:
是指在路径中定义的参数,例如 /user/42

必选参数:
使用 :param 的语法完成必选参数,例如 /user/:ID ,可以匹配 /user/42 ,但不能匹配 > /user 或 /user/

可选参数:
使用 *param 的语法完成可选参数,例如 /user/*ID ,可匹配 /user/42 和 /user/

2. 路由的两种注册

  • 普通注册
e := gin.Default()
e.GET("/ping", func(context *gin.Context) {
        context.JSON(200, "pong")
})
  • 使用 RouteGroup注册 ,便于版本维护
e := gin.Default()
v1 := e.Group("v1")
{
	v1.POST("/v1/test", func(context *gin.Context) {
		context.JSON(200, "message")
	})
}
  • 使用中间件
e.Use(gin.Recovery())

3. 生成路由树&注册添加路由 的具体实现

从e.GET、e.POST方法入口,逐级调用到 group.engine.addRoute(httpMethod, absolutePath, handlers)

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
        return group.handle(http.MethodPost, relativePath, handlers)
}

统一调用 RouterGroup 的 handle() :

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// ... 省略代码
	group.engine.addRoute(httpMethod, absolutePath, handlers) 《=====
	// ... 省略代码
}

addRoute 添加路由,通过method 获取对应的路由树,然后注册添加路由

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   // ... 省略代码

   root := engine.trees.get(method)  《=====获取对应方法的前缀树
   if root == nil {
      root = new(node)
      root.fullPath = "/"
      engine.trees = append(engine.trees, methodTree{method: method, root: root})
   }
   root.addRoute(path, handlers) 《===== 添加路由处理函数

   // ... 省略代码
}

结构体 node 是路由树的节点:

type node struct {
	path      string          //当前叶子节点的最长的前缀
	indices   string          //通常维护了children列表的path的各首字符组成的string
	wildChild bool            //默认是false,当children时通配符类型时,wildChild为true
	nType     nodeType        //是节点的类型,默认是static类型,还包括root类型,对于path包含:通配符,类型是param,对于包含*通配符,类型是catchAll
	priority  uint32          //代表了有几条路会经过此节点,用于在节点进行排序时使用
	children  []*node         //就是一颗树的叶子结点。每个路由的去掉前缀后,都被分布在这些 children 数组里
	handlers  HandlersChain   //存放的是当前叶子节点对应的路由的处理函数
	fullPath  string          //是从root节点到当前节点的全部path部分,如果此节点为终节点,handlers为对硬的处理链,非则为nil。maxParams是当前节点到各个叶子节点的包含的通配符的最大数量
}

其实 gin 的实现不像一个真正的树, children []*node 所有的孩子都放在这个数组里面, 利用indices, priority变相实现一棵树。

###4. server 端收到客户端请求时,如何找到对应的路由的handler?

前面分析到,在Gin框架中,通过Run() 方法调用了 http.ListenAndServe 进行监听和创建新连接处理请求 ,最终会调用到 serveHTTP。
serveHTTP 中调用了 engine.handleHTTPRequest(c) ,这个方法第一件事就是去路由树里面去匹配对应的URL,找到相关路由,拿到相关的处理函数。

func (engine *Engine) handleHTTPRequest(c *Context) {
   // ... 省略代码

   //0.一棵前缀树
   t := engine.trees
   for i, tl := 0, len(t); i < tl; i++ {
      if t[i].method != httpMethod {
         continue
      }
      root := t[i].root
      //1.去路由树找匹配的url
      value := root.getValue(rPath, c.Params, unescape) <====从路由树中查找路由
      //2.拿到相关处理函数
      if value.handlers != nil {
         c.handlers = value.handlers          《======处理函数赋值给context
         c.Params = value.params
         c.fullPath = value.fullPath
         c.Next()                            《======调用处理函数
         c.writermem.WriteHeaderNow()
         return
      }
      if httpMethod != "CONNECT" && rPath != "/" {
         if value.tsr && engine.RedirectTrailingSlash {
            redirectTrailingSlash(c)
            return
         }
         if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
         }
      }
      break
   }
   
   // ... 省略代码
}

在路由树中查找路由 node.getValue:

func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
   // ... 代码省略
}

Next 这个方法里调用路由处理函数,如果有中间件,它会依次进行调用,最后在调用业务处理函数。

func (c *Context) Next() {
   c.index++
   for c.index < int8(len(c.handlers)) {
      c.handlers[c.index](c)
      c.index++
   }
}