目录
1.框架概览
快问快答
Q:首先,什么是网络框架?
A: 对于GO来说,网络框架就是介于开发者和网络库net/http之间抽象出来的一层或一个工具,来达成帮助我们快速开发应用的目的,而net/http又搭建了用户态到内核态(指网络调用方面)的桥梁
那么gin框架是如何抽象出它和net/http的关系呢?
1.1使用实例
func main() {
serve := gin.Default()
serve.Use(func(ctx *gin.Context) {
fmt.Println("hi")
})
serve.GET("home", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "hi"})
})
v1 := serve.Group("/v1")
{
v1.GET("/login", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "hi"})
})
}
// serve.Run(":8088")
http.ListenAndServe(":8088", serve)
}
gin.Default返回Engine的指针类型
gin既可以通过自身的Engine.Run方法启动,也可以将Engine注入到ListenAndServe中启动服务器,说明了gin本身的关键数据结构Engine实现了http.Handler接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
即gin框架是通过http.Handler来搭建其和net/http库的关系,再大胆点思考,是不是意味着我们实现了该接口,也能手撸出一个web框架出来?
image.png
2.核心数据结构Engine
func Default(opts ...OptionFunc) *Engine
核心成员如下
type Engine struct {
// 路由组
RouterGroup
// ...
// context 对象池
pool sync.Pool
// 方法路由树
trees methodTrees
// ...
}
2.1RouteGroup
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
var _ IRouter = (*RouterGroup)(nil)
RouteGroup作为内嵌字段被嵌入Engine,是路由组的概念,其中的配置将被从属于该路由组的所有路由所复用,同时也是实现了IRouter接口
image.png
从该接口规定的方法来看,实际上它就是真正的注册路由结构体
Engine.Get()
实际上就是
RouterGoup.Get()
- Handlers 切片类型,存放处理函数,最终组成一个函数处理链调用
type HandlersChain []HandlerFunc
- basePath 基础路由,Engine被初始化时,basePath=="/"
- root 标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false
2.2 Engine创建流程
即Default函数
方法调用:gin.Default -> gin.New
- 首先调用new方法生成engine
- 再调用Use将日志和恢复中间件加入
- 最后使用option模式配置engine
func Default(opts ...OptionFunc) *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
func New() *Engine {
// ...
// 创建 gin Engine 实例
engine := &Engine{
// 路由组实例
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
// ...
// 9 棵路由压缩前缀树,对应 9 种 http 方法
trees: make(methodTrees, 0, 9),
// ...
}
engine.RouterGroup.engine = engine
// gin.Context 对象池
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
2.3注册中间件
即Use方法,不断向RouterGroup尾部加入HandlerFunc
方法调用: Engine.Use ->RouterGroup.Use
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
//根据树是否为nil返回engine或自身
return group.returnObj()
}
2.4路由注册
以Get为例
方法调用: engine.Get -> RouterGroup.Get -> RouterGroup.handle
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()
}
- calculateAbsolutePath函数将路由和路由组的basePath拼接
- combineHandlers负责将用户的业务代码和路由组的handlers组装生成新的切片
- addRoute负责添加到压缩前缀树中
关于gin中的压缩前缀树看这里
2.5服务启动
底层也是调用listenandserver
func (engine *Engine) Run(addr ...string) (err error) {
// ...
err = http.ListenAndServe(address, engine.Handler())
return
}
2.6处理请求
即Engine.ServerHttp函数
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)
}
因为请求的创建和销毁是很频繁的,这也意味着context的创建和销毁也是很频繁的,因此从池中取,用完放回池中,懒加载模式重置context(这里的context实际上是对http.ResponseWriter 和http.Request的封装)
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// ...
t := engine.trees
- handleHttpRequest就是根据方法找路由树,再根据路径找对应节点,其中找到节点后,对应的执行函数方法为
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
通过调用context.Next方法执行之前注册的函数
3 核心数据结构Context
type Context struct {
// ...
// http 请求参数
Request *http.Request
// http 响应 writer
Writer ResponseWriter
// ...
// 处理函数链
handlers HandlersChain
// 当前处于处理函数链的索引
index int8
engine *Engine
// ...
// 读写锁,保证并发安全
mu sync.RWMutex
// key value 对存储 map
Keys map[string]any
// ..
}
这里重点分析它是如何对handlers的遍历,即Next方法
进入next前,index=-1
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
在我们的印象里,中间件通常都是洋葱模式的,通过index和for循环依次调用handlers,那这种遍历调用是不是就意味着它不是洋葱模式了?
我们写一个代码看一下
serve.Use(func(ctx *gin.Context) {
// handle1
fmt.Println(1)
ctx.Next()
fmt.Println(4)
}, func(ctx *gin.Context) {
// handle2
fmt.Println(2)
ctx.Next()
fmt.Println(3)
})
1
2
3
4
发现它还是洋葱模式,我们一步步分析来看一下它是如何用index来模拟压栈流程的(实际上就是利用全局变量写递归,熟悉递归的后面就跳过),我们分别定义上面两个为handl1和2,主业务为handlFunc(先不管logger和recover中间件)
- 首先index=-1 第一次进入 index++,index=0,此时执行handle1 image.png
- 此时handle1被压入栈,还没执行完不出栈,继续c.next,index=1,执行hanle2 image.png
- 最后index=3,执行handleFunc image.png
-
执行完出栈handleFunc,返回继续执行c.next的for循环,因为此时index==3==int8(len(context.handlers)),这一层handl2触发的c.next执行完毕,回去执行handle2剩余部分并出栈
image.png
同理handle1触发的c.next发现index==3也停止执行剩余循环,结束next,执行handle1并出栈 image.png
2.1利用Abort停止handle传递
// 127>>1 = 63
const abortIndex int8 = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = abortIndex
}
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
其实现原理是将 Context.index 设置为一个过载值 63,导致 Next 流程直接终止. 这是因为 handlers 链的长度必须小于 63,否则在注册时就会直接 panic. 因此在 Context.Next 方法中,一旦 index 被设为 63,则必然大于整条 handlers 链的长度,for 循环便会提前终止.
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
// 断言 handlers 链长度必须小于 63
assert1(finalSize < int(abortIndex), "too many handlers")
// ...
}
此外,用户还可以通过 Context.IsAbort 方法检测当前 handlerChain 是出于正常调用,还是已经被熔断.
4.总结与思考
image.pnggin通过http.Hanlder接口搭建了自己和http包的桥梁,其框架核心就是Server(Engine)、路由树(RouterGroup)和上下文(Context)
那也就意味着我们设计好这三方面也能实现一个最简单的路由框架
网友评论