GO的框架之gin(一)

Gin与HTTP Gin是一个Web框架,他有几个特点: 支持中间件操作(handlersChain机制) 更方便的使用 (gin.Context 可以理解成为一个并发安全的map) 更强大的路有解析能力 (radix tree 压缩前缀树来做路由树) 较之net/http有更强的模糊匹配等功能 G

Gin与HTTP

Gin是一个Web框架,他有几个特点:

  • 支持中间件操作(handlersChain机制)
  • 更方便的使用 (gin.Context 可以理解成为一个并发安全的map)
  • 更强大的路有解析能力 (radix tree 压缩前缀树来做路由树) 较之net/http有更强的模糊匹配等功能

Gin是在net/http标准库之上的再次封装,他们的关系可以有如下图:

Untitled.png

可以看出,在 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数据结构

Untitled.png

(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

Untitled.png

以 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就是核心处理业务的逻辑方法了

Untitled.png

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
    }
    // ...
}
LICENSED UNDER CC BY-NC-SA 4.0
Comment