go 进阶 gin底层原理相关: 三. gin接收请求匹配路由原理

一. 获取请求后的路由匹配

  1. gin中Engine实现了ServeHTTP()函数,是请求的入口函数, 这里可以去了解一下net/http的原理,查看ServeHTTP()源码,内部会调用handleHTTPRequest()
// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  // 这里使用了对象池,在pool中取得一个Context对象,并准备好Request和ResponseWriter的相关数据设置到Context中去,提供给方法链调用
	c := engine.pool.Get().(*Context)
  // 这里有一个细节就是Get对象后做初始化
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数

	engine.pool.Put(c)  // 处理完请求后将对象放回池子
}
  1. 查看handleHTTPRequest()源码(此处只摘取了其中关键部分),在该方法中会调用node下的getValue(rPath, c.Params, unescape)
// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
	// yangxiangrui.site...

	// 根据请求方法找到对应的路由树
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 在路由树中根据path查找
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()  // 执行函数链条
			c.writermem.WriteHeaderNow()
			return
		}
	
	// yangxiangrui.site...
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}
  1. getValue()该函数根据给定的路径(键)返回nodeValue值,nodeValue中保存注册的处理函数和匹配到的路径参数数据
// tree.go

type nodeValue struct {
	handlers HandlersChain
	params   Params  // []Param
	tsr      bool
	fullPath string
}

// 
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	for {
		prefix := n.path
		if path == prefix {
			// 我们应该已经到达包含处理函数的节点。
			// 检查该节点是否注册有处理函数
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
			}

			if path == "/" && n.wildChild && n.nType != root {
				value.tsr = true
				return
			}

			// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数
			indices := n.indices
			for i, max := 0, len(indices); i < max; i++ {
				if indices[i] == '/' {
					n = n.children[i]
					value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
						(n.nType == catchAll && n.children[0].handlers != nil)
					return
				}
			}

			return
		}

		if len(path) > len(prefix) && path[:len(prefix)] == prefix {
			path = path[len(prefix):]
			// 如果该节点没有通配符(param或catchAll)子节点
			// 我们可以继续查找下一个子节点
			if !n.wildChild {
				c := path[0]
				indices := n.indices
				for i, max := 0, len(indices); i < max; i++ {
					if c == indices[i] {
						n = n.children[i] // 遍历树
						continue walk
					}
				}

				// 没找到
				// 如果存在一个相同的URL但没有末尾/的叶子节点
				// 我们可以建议重定向到那里
				value.tsr = path == "/" && n.handlers != nil
				return
			}

			// 根据节点类型处理通配符子节点
			n = n.children[0]
			switch n.nType {
			case param:
				// find param end (either '/' or path end)
				end := 0
				for end < len(path) && path[end] != '/' {
					end++
				}

				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[1:]
				val := path[:end]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
						value.params[i].Value = val // fallback, in case of error
					}
				} else {
					value.params[i].Value = val
				}

				// 继续向下查询
				if end < len(path) {
					if len(n.children) > 0 {
						path = path[end:]
						n = n.children[0]
						continue walk
					}

					// ... but we can't
					value.tsr = len(path) == end+1
					return
				}

				if value.handlers = n.handlers; value.handlers != nil {
					value.fullPath = n.fullPath
					return
				}
				if len(n.children) == 1 {
					// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数
					// 用于 TSR 推荐
					n = n.children[0]
					value.tsr = n.path == "/" && n.handlers != nil
				}
				return

			case catchAll:
				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[2:]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
						value.params[i].Value = path // fallback, in case of error
					}
				} else {
					value.params[i].Value = path
				}

				value.handlers = n.handlers
				value.fullPath = n.fullPath
				return

			default:
				panic("invalid node type")
			}
		}

		// 找不到,如果存在一个在当前路径最后添加/的路由
		// 我们会建议重定向到那里
		value.tsr = (path == "/") ||
			(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
				path == prefix[:len(prefix)-1] && n.handlers != nil)
		return
	}
}

二. 总结

  1. 这里还是要说一下gin与net/http直接,在编写gin服务时,先初始化容器,创建Engine实例,然后调用Run()启动服务,在run方法中会执行net/http下的ListenAndServe(),请求数据就在net/http开始流转,所以 gin 建立 socket 的过程,accept 客户端请求的过程与 net/http 没有差别
  2. gin的路由注册,路由关系维护是基于httprouter实现的(有的博客说是直接调用httprouter底层,但是我再gin中没有找到,不知道是不是我看的不够仔细),这里就先说成gin路由注册是参考httprouter实现的
  3. 在编写gin服务时要先拿到Engine实例,了解一下Engine 的继承关系与内部结构
  1. Engine隐式的继承了RouterGroup,那Engine实际就可以看成一个RouterGroup,而RouterGroup实现了IRouter,IRoutes接口拥有GET(),POST()等路由注册方法
  2. Engine还实现了Handler也就是实现了ServeHTTP()函数,是请求的入口函数
  3. Engine中还存在一个trees属性也就是前缀树,
  4. Engine上还绑定了一个addRoute()方法,在通过RouterGroup的GET(),POST()等方法进行路由注册时,内部会调用到该方法,最终会将接口路径与接口处理器函数封装为methodTree保存到Engine的trees中
  1. 在执行ListenAndServe()启动http服务在指定端口监听接收请求时,内部会会调用
  1. " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
  2. “srv.Serve(ln)”: 开启循环调用Accept()基于多路复用初始化FD,pollDesc,封装epollevent,调用netpollopen(),将可读,可写,对端断开,边缘触发 的监听事件注册到epollevent中
  3. 当接收到连接请求Accept()方法返回会拿到一个net.Conn连接,可以理解为拿到了一个基于TCP的HTTP连接,并将conn连接的状态标志为StateNew,Conn是一个接口,内部提供了:Read读取连接数据,Write发生数据,Close关闭连接等方法,比较重要,底层就是通过这几个方法处理请求读写的
  4. 然后执行"go c.serve(ctx)" 开启一个goroutine执行conn的serve()方法
  1. 查看conn上的serve(),该方法内重点执行了:
  1. 首先调用newBufioReader() 封装了一个bufio.Reader
  2. 开启了一个无限for循环,循环内
  3. 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
  4. 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
  5. 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
  6. 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
  1. 会封装serverHandler调用serverHandler上的ServeHTTP(w, w.req),其中ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由,这里执行的就是gin中Engine实现的ServeHTTP()函数,查看该函数,重点关注调用了Engine上绑定的handleHTTPRequest()方法
  1. 解析请求,获取请求url,请求方法method, 获取到trees路由树,
  2. 然后基于当前的请求方法,比如GET,POST等,在trees中获取到root根节点(路由注册保存时会以请求方法作为root根节点存储到trees中)
  3. 获取到root根节点后,是一个node结构体变量,调用node上的getValue()获取子节点,在getValue()方法中会根据node的nType节点类型属性等执行不同的匹配规则判断是否满足条件,如果满足则获取,最终会获取到所有满足条件的node封装一个nodeValue结构体变量,nodeValue有一个handlers属性,存储了当前接口调用需要执行的处理器包括中间件
  4. 遍历handlers,也就是处理器包括中间件,按照顺序开始执行
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值