gin框架(3)- Engine与Context

前言

上一章,我们讲述了request请求是如何在gin中流转的,其中提到了两个比较重要的结构体Engine和Context。Engine在gin中充当server的角色,Context则负责对request的封装(类似net/http中的request),本章详细介绍一下这两个结构体及其作用。

1. Engine

  Engine即gin对应的服务端类(Server类),对应net/http中的ServeMux。编写gin的服务时,通常有两种初始化方法:

// 方法1,声明一个DefaultServer
func main() {
    r := gin.Default() // gin.Default返回一个*Engine实例
    r.POST("/", ...) // 添加路由
    r.Run("localhost:8080") // 启动服务
}

// 方法2: 声明一个Server,自主添加中间件
func main() {
    r := gin.New() 
    r.Use(...) // 添加中间件
    r.POST("/", ...) //添加路由
    r.Run("localhost:8080") 
}
以上两种方法都生成一个*Engine实例,只是gin.Default()生成的Engine带了Logger和Recovery两个中间件。Engine的核心成员如下:
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
    ...
    RouterGroup  // 路由信息,主要包含路由的路径和对应路径的所有中间件和处理函数
    trees methodTrees  // 存储所有注册的路由
    pool sync.Pool  // 缓存所有的context,减少context的频繁gc
    ...
}

type RouterGroup struct {
    Handlers HandlersChain // 存储该路由下的中间件函数和处理函数
    basePath string   // 路由路径
    engine   *Engin // 对Engine的引用
    root bool   // 该RouterGroup实例是否是根路由
}

type methodTrees []methodTree

type methodTree struct {
    method string  // 函数名称
    root   *node   // 对应的radix_tree节点
}
一个Engine的核心功能包括:
  • 注册路由
  • 给某个路由添加中间件
  • 接受新的连接
  • 当已有连接有数据来临时,调用对应路由下的处理函数(请求处理,此部分开启了单独的goroutine处理)
下面我们看一下对应的实现了以上功能的Engine函数。
注:Engine的路由和中间件注册对应在radix tree的添加节点,请求处理中的路由匹配部分对应在radix tree中查找节点。只要明白了radix tree的原理,这几个功能很好理解。而接受新的连接和连接到来时,调用请求处理函数则是复用了net/http的处理函数,参考上一章第一节部分。因此以下部分只介绍对应的函数和原理,不展示源码,以求提纲挈领。

路由注册和添加路由中间件

  注册路由等功能是通过Engine下的RouterGroup实现的,RouterGroup实现了POST, GET,PUT, Group等函数。POST, GET,PUT, DELETE等函数就是在路由树radix_tree上添加一个路由节点;Group则是添加一个路由组,本质上就是在radix_tree上添加了一个非叶节点的路由节点。理解gin的路由实现,以上函数的原理理解起来就非常容易。中间件通过RouterGroup实现的Use函数添加。中间件函数的签名和请求处理函数一致,Use函数就是在RouterGroup.Handlers(HandlerFunc的数组)中添加一个中间件HandlerFunc。

接受新的连接

  Engine的Run函数底层就是一个for循环,在循环内为每个新到来的连接创建一个goroutine(本质上是利用了net/http的ListenAndServe函数,实现对新连接的处理)

处理请求

Engine实现了ServeHTTP函数,从上一章1.3的分析中,我们知道ServeHTTP是统一的请求处理函数。ServeHTTP的主要作用就是在Engine.trees中找到跟请求路由对应的HandlersChain,并调用它们处理请求,回写结果(即路由匹配->请求处理->结果回写)。

2. Context 

  Context是gin的核心结构体之一,主要负责在同一个请求上下游之间传递request。我们都知道golang本身的Context,主要用来设置一次处理的deadline、同步信号,传递请求相关值,相关知识可以参考 《go语言设计与实现》Context。gin的Context就是golang原生Context的延续。官方的注释精确的描述了gin的Context的作用:

Context is the most important part of gin. It allows us to pass variables between middlewaremanage the flow, validate the JSON of a request and render a JSON response for example.
下面看一下Context的核心成员变量:
type Context struct {
    ...
    Request   *http.Request  // 保存request请求
    Writer    ResponseWriter // 回写response 
    handlers  HandlersChain  // 该次请求所有的中间件函数和处理函数
    index     int8           // HandlersChain的下标,用来调用某个具体的HandlerFunc
    fullPath  string         // 请求的url路径
    engine    *Engine        // 对server Engine的引用
    Keys      map[string]any // 用于上下游之间传递参数
    ...
}
每次新的请求到来时,都会从Engine.pool中申请一个Context实例,用来封装本次请求的所有参数信息。下面介绍一些跟Context核心功能相关的函数。

中间件函数

  一次请求的所有中间件函数和请求处理函数都在Context.handlers中。因此,当请求到来时,只需要依次调用Context.handlers中的所有HandlerFunc即可。这就是调用中间件中的一个最重要的函数Next(),定义如下:

func (c *Context) Next() {
    c.index++
  // 依次遍历所有的中间件函数,并调用他们 for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
除了顺序执行所有中间件,还要有在某个中间件函数中终止处理的能力。比如某个中间件负责权限校验,如果用户的校验没通过,直接返回Not Authorized,跳过后续的处理。这个是通过Abort函数实现的:
func (c *Context) Abort() {
    c.index = abortIndex // abortIndex是个常量=63
}
Abort()的原理非常简单,直接让c.Index等于最大值,这样剩余的中间件函数都没机会执行(从这个函数中可以看出,中间件的数量是有上限的,上限就是63个)。
  正常的执行流是依次调用各个中间件函数,但是如果在某个中间件函数中显式调用了Next(),会先执行后续的中间件,执行完成了再返回当前中间件继续执行,流程如下:
 

参数传递

  Context中有成员Keys,上游需要传递的变量可以放在里面,下游处理时再从里面取出来,实现上下游参数传递。对应Set()和Get()方法。

func (c *Context) Set(key string, value any) {
    c.mu.Lock() // 用来保护c.Keys并发安全
    if c.Keys == nil {
        c.Keys = make(map[string]any)
    }

    c.Keys[key] = value
    c.mu.Unlock()
}

func (c *Context) Get(key string) (value any, exists bool) {
    c.mu.RLock()
    value, exists = c.Keys[key]
    c.mu.RUnlock()
    return
}
Get()函数还有很多衍生函数,比如GetString,GetStringMapString,GetStringMapStringSlice等,都是在Get的基础上,将数据转换成我们需要的类型再返回。

参数绑定

请求放到Context.Request里,需要解析成结构体或map,才好在业务代码里使用。这一部分是通过Bind()系列函数实现的。Bind()系列函数的作用就是根据request的数据类型,将其解析到结构体或map里。简单的看一个参数绑定的实现:

func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
    return b.Bind(c.Request, obj)  // 底层调用的是binding相关的函数
}
binding相关的函数解析在 go的binding和validate 章节中进行详细讲述,此处不再赘述。

3. Context和Engine合作完成一次HTTP请求的过程

request的流转 中我们分析过,每个新来的连接由一个专门的goroutine去服务,这个goroutine执行主要代码如下:

// 每个新到来的连接会由Server goroutine调用Accept函数,生成一个conn, 然后调用go conn.serve()
func (c *conn) serve(ctx context.Context) {
    ...
    for {
        w, err := c.readRequest(ctx) // 读取req请求
        if err != nil {
            // 错误处理
        }
        req = w.req 
        ... // 对req的前置校验
        serverHandler{c.server}.ServeHTTP(w, w.req) // 本质上调用的是Engine.ServeHTTP函数
        ...
    }
    ...
}
可以看到处理请求的流程就是在循环中,读取每个conn的req,然后调用Engine.ServeHTTP。我们再看一下Engine.ServeHTTP的主要处理流程。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 每次从Context的pool里取出一个Context结构体,先reset,然后将本次req的信息绑定到该结构体上
    c := engine.pool.Get().(*Context) 
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c) // 处理完成,回收Context结构体
}

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method  
    rPath := c.Request.URL.Path // 获取URL路径
    ...  // 对URL path进行清洗
    t := engine.trees  // 读取Engine上注册的路由树,每个HTTP method对应一个radix_tree
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root // 获取到对应http method对应的radix tree 
        // 找到对应的路由上注册的节点
        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
            c.fullPath = value.fullPath
            c.Next()  // 调用该路由下的所有HandlerFunc
            c.writermem.WriteHeaderNow()
            return
        }
        // 没有找到路由,执行修复后的path重定向
        if httpMethod != http.MethodConnect && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }
    ...
}
一次请求的处理过程都在上面的函数中了。主要是初始化Context结构体(reset并绑定request),然后找到对应路由下注册的所有HandlerFunc,并调用他们。为了简洁,以上函数只引用了最核心的步骤,完整的函数可以参考源码。
 
 

 

posted @ 2022-10-23 13:39  晨枫1  阅读(585)  评论(0编辑  收藏  举报