gin框架分析三:gin路由

对于gin框架,如何处理每个请求以及如何将每个请求正确发送到对应的处理方法上,这个过程将会设计到gin中的路由构建和路由寻找过程。

gin构建路由

路由数据结构

gin路由关键数据结构在Engine中定义如下:

// method用于区分不同种类请求
type methodTree struct {
   method string
   root   *node
}
 
// 不同种类请求构成数组结构
type methodTrees []methodTree
trees            methodTrees 

// 路由树节点
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
}

gin中路由的关键定义如上代码,methodTree中method变量指出路由的请求方法是GET还是POST等方式。 

有如下请求和处理函数:

	router.POST("/mypost", func(context *gin.Context) {
		...
	})

	router.POST("/mypost/p1", func(context *gin.Context) {
		...
	})

	router.POST("/mypost/p2", func(context *gin.Context) {
		...
	})

	router.POST("/mypost/a1", func(context *gin.Context) {
		...
	})

	router.POST("/post/a1", func(context *gin.Context) {
		...
	})

	router.POST("/post/my1", func(context *gin.Context) {
		...
	})

	router.POST("/post/query", func(context *gin.Context) {
		...
	})

请求和路由树的结构将构建如下图:

 engine.POST

对于post、get、put、delete等方式的请求,其调用的是group.handle(http.MethodPost, relativePath, handlers)函数,因此只需要分析此函数即可

弄明白对于路由如何组织构建的,代码如下:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

 绝对路径获取

首先 group.calculateAbsolutePath(relativePath)将会获取请求的绝对路径,例如“/mypost/../p3”其获取的绝对路径是“/p3”,

 合并中间件

group.combineHandlers(handlers)将会把默认中间件和url处理的handler合并:

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	assert1(finalSize < int(abortIndex), "too many handlers")
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
} 

  默认通过engine.Use()设置的中间件将会和url处理handler合并形成一个处理器链用于每一个请求的处理。

 构建路由树

路由的添加通过group.engine.addRoute(httpMethod, absolutePath, handlers)函数完成,如源码定义:

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	assert1(path[0] == '/', "path must begin with '/'")
	assert1(method != "", "HTTP method can not be empty")
	assert1(len(handlers) > 0, "there must be at least one handler")

	debugPrintRoute(method, path, handlers)

	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)

	// Update maxParams
	if paramsCount := countParams(path); paramsCount > engine.maxParams {
		engine.maxParams = paramsCount
	}

	if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
		engine.maxSections = sectionsCount
	}
}

  首先通过所请求的方式在engine.trees数组中获取是否存在对应请求方式的路由树,如果没有则新建路由树,否则取出已经存在的路由树通过root.addRoute(path, handlers)遍历树并构建路由,具体的路由树构建代码在tree.go文件中addRoute方法。

gin路由寻找

http服务端入口

net/http 包提供的 http.ListenAndServe() 方法,可以在指定的地址进行监听,并对监听的地址通过绑定的handler实例中调用ServeHTTP方法来进行处理。Gin实现了go原生的net/http中的Handler接口,因此通过gin的ServeHTTP方法作为框架入口并开始其后的流程。源码中实现如下:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

首先在pool中获取一个可重复利用的contex实例,并对go原生的request和response做封装。然后通过engine.handleHTTPRequest(c)函数进入路由寻址和处理过程。

路由寻找过程

handleHTTPRequest方法中揭示了如何寻找路由及路由找到后如何调用路由中处理器。在源码中:

t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
   if t[i].method != httpMethod {
      continue
   }
   root := t[i].root
   // Find route in tree
   value := root.getValue(rPath, c.params, c.skippedNodes, unescape)

...

  此代码封装了路由寻找的完整过程,首先通过调用方式(POST、GET等)在路由数组中匹配到对应的路由树,然后在root.getValue从树根节点入口开始寻找。

路由树的节点中有几个需要注意的变量:

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
}  

path: 当前路由树URL路径

indices: 下层节点路由首字母,例如“/mypost/a/a1”和"/mypost/b/b1"这两个路径,当path为mypost时,indices将会是"ab",字母在字符串中的下标数字标识children数组中path为a和b节点的位置

children: 下层路由树

handlers: 匹配到路由后的处理函数(中间件和处理函数集合)

 

以“/mypost/a/a1”为例,路由寻找规则如下:

 1、从路径中取出长度和路由树节点中path长度一致的字符串,比较是否相等,若取出的字符串和节点中的path相等,则匹配到路由。如路由根节点中path为"/",则取出“/mypost/a/a1”中长度为len("/")的子串"/";

 2、路径中剩余的部分"mypost/a/a1"的首字母"m"和节点indices中的字母比较,相等则获取下标号;

 3、或许到的下标号到children数组中取对应下标号的节点,作为新的入口节点;

 4、修改匹配路径为剩余部分即"mypost/a/a1",然后从步骤1重复,直到节点indices为""则匹配到最后路由树叶子节点;

 5、将叶子节点返回,寻找到路由结果;

路由处理方法调用过程

在获取到路由之后,节点中的handlers将会被封装到context实例之中,然后通过Next方法开始路由响应过程的调用。

if value.handlers != nil {
	c.handlers = value.handlers
	c.fullPath = value.fullPath
	c.Next()
	c.writermem.WriteHeaderNow()
	return
}
Context.Next方法对于handler的调用实现代码很简单,循环路由节点上的handler,每个都调用一次。
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

 结束

gin框架对于路由的构建和寻找过程很简单,从源码分析上来看,gin的整个核心其实都是围绕路由为重点,至此GIN框架大部分内容都已经分析完成。  

posted @ 2023-01-30 16:45  一朵野生菌  阅读(363)  评论(0编辑  收藏  举报