Gin与HTTP
Gin是一个Web框架,他有几个特点:
- 支持中间件操作(handlersChain机制)
- 更方便的使用 (gin.Context 可以理解成为一个并发安全的map)
- 更强大的路有解析能力 (radix tree 压缩前缀树来做路由树) 较之net/http有更强的模糊匹配等功能
Gin是在net/http标准库之上的再次封装,他们的关系可以有如下图:
可以看出,在 net/http 的既定框架下,gin 所做的是提供了一个 gin.Engine 对象作为 Handler 注入其中,从而实现路由注册/匹配、请求处理链路的优化.
因为net/http的服务端中有个serverHandler接收客户端请求然后返回一个响应,他是有一个抽象的ServeHTTP方法,因此可以通过gin.Engine去实现这个ServeHTTP并且注册入serverHandler
Gin使用实例
下面提供一段接入 Gin 的示例代码,让大家预先感受一下 Gin 框架的使用风格:
- 构造 gin.Engine 实例:gin.Default()
- 路由组注册中间件:Engine.Use()
- 路由组注册 POST 方法下的 handler:Engine.POST()
- 启动 http server:Engine.Run()
import "github.com/gin-gonic/gin"
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 注册中间件
mux.Use(myMiddleWare)
// 注册一个 path 为 /ping 的处理函数
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// 运行 http 服务
if err := mux.Run(":8080"); err != nil {
panic(err)
}
}
gin.Engine数据结构
(1) gin.Engine
Engine 为 Gin 中构建的 HTTP Handler,其实现了 net/http 包下 Handler interface 的抽象方法: Handler.ServeHTTP,因此可以作为 Handler 注入到 net/http 的 Server 当中.
Engine包含的核心内容包括:
- 路由组 RouterGroup:下一部分展开
- Context 对象池 pool:基于 sync.Pool 实现,作为复用 gin.Context 实例的缓冲池. gin.Context 的内容后面详解
- 路由树数组 trees:共有 9 棵路由树,对应于 9 种 http 方法. 路由树基于压缩前缀树实现,后面详解.
9 种 http 方法展示如下:
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
(2) RouterGroup
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
RouterGroup 是路由组的概念,其中的配置将被从属于该路由组的所有路由复用:
- Handlers:路由组共同的 handler 处理函数链. 组下的节点将拼接 RouterGroup 的公用 handlers 和自己的 handlers,组成最终使用的 handlers 链
- basePath:路由组的基础路径. 组下的节点将拼接 RouterGroup 的 basePath 和自己的 path,组成最终使用的 absolutePath
- engine:指向路由组从属的 Engine
- root:标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false
(3) HandlerChain
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
// 这个Context入参包含了用于我们接收的Reader或者要写的Writer都可以通过context传递chen
HandlersChain 是由多个路由处理函数 HandlerFunc 构成的处理函数链. 在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc.
流程入口
下面以创建 gin.Engine 、注册 middleware 和注册 handler 作为主线,进行源码走读和原理解析:
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 注册中间件
mux.Use(myMiddleWare)
// 注册一个 path 为 /ping 的处理函数
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// ...
}
初始化engine
方法调用:gin.Default -> gin.New
- 创建一个 gin.Engine 实例
- 创建 Enging 的首个 RouterGroup,对应的处理函数链 Handlers 为 nil,基础路径 basePath 为 “/”,root 标识为 true
- 构造了 9 棵方法路由树,对应于 9 种 http 方法
- 创建了 gin.Context 的对象池
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 { // new 传一个 兜底的构造方法
return engine.allocateContext(engine.maxParams)
}
return engine
}
注册middleware
通过 Engine.Use 方法可以实现中间件的注册,会将注册的 middlewares 添加到 RouterGroup.Handlers 中. 后续 RouterGroup 下新注册的 handler 都会在前缀中拼上这部分 group 公共的 handlers
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
// ...
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
可以看到是用append去把多个midlleware注册到路由组的Handlers中
注册一个具体handler
以 http post 为例,注册 handler 方法调用顺序为 RouterGroup.POST-> RouterGroup.handle,接下来会完成三个步骤:
- 拼接出待注册方法的完整路径 absolutePath
- 拼接出代注册方法的完整处理函数链 handlers
- 以 absolutePath 和 handlers 组成 kv 对添加到路由树中
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return 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()
}
gin服务端启动
流程入口
下面通过 Gin 框架运行 http 服务为主线,进行源码走读:
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 一键启动 http 服务
if err := mux.Run(); err != nil{
panic(err)
}
}
服务启动
一键启动 Engine.Run 方法后,底层会将 gin.Engine 本身作为 net/http 包下 Handler interface 的实现类,并调用 http.ListenAndServe 方法启动服务. gin.Engine就是调的http的包
func (engine *Engine) Run(addr ...string) (err error) {
// ...
err = http.ListenAndServe(address, engine.Handler())
return
}
ListenerAndServe 方法本身会基于主动轮询 + IO 多路复用的方式运行,因此程序在正常运行时,会始终阻塞于 Engine.Run 方法,不会返回.
请求处理
在服务端接收到 http 请求时,会通过 Handler.ServeHTTP 方法进行处理. 而此处的 Handler 正是 gin.Engine,其处理请求的核心步骤如下:
- 对于每笔 http 请求,会为其分配一个 gin.Context,在 handlers 链路中持续向下传递
- 调用 Engine.handleHTTPRequest 方法,从路由树中获取 handlers 链,然后遍历调用
- 处理完 http 请求后,会将 gin.Context 进行回收. 整个回收复用的流程基于对象池管理
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取一个 context
// 可以复用缓存的context实例,如果没有才创建
c := engine.pool.Get().(*Context)
// 重置/初始化 context
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理 http 请求
engine.handleHTTPRequest(c)
// 把 context 放回对象池,给后续请求复用
engine.pool.Put(c)
}
handleHTTPRequest就是核心处理业务的逻辑方法了
Engine.handleHTTPRequest 方法核心步骤分为三步:
- 根据 http method 取得对应的 methodTree
- 根据 path 从 methodTree 中找到对应的 handlers 链
- 将 handlers 链注入到 gin.Context 中,通过 Context.Next 方法按照顺序遍历调用 handler
此处根据 path 从路由树寻找 handlers 的逻辑位于 root.getValue 方法中,和路由树数据结构有关,放在本文第 4 章详解;
根据 gin.Context.Next 方法遍历 handler 链的内容放在后面详解.
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method // 获取当前请求方法
rPath := c.Request.URL.Path // 获取当前请求路径
// ...
t := engine.trees
// 遍历九棵方法树
for i, tl := 0, len(t); i < tl; i++ {
// 获取对应的方法树
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 从路由树中寻找路由
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil { // 如果匹配获取成功
c.handlers = value.handlers // 将路由树中的handlers注入到gin.context里
c.fullPath = value.fullPath // 同理 fullpath注入
**c.Next() // 真正遍历执行handlersChain**
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
// ...
}