Gin 的启动过程、路由及上下文源码解读

2022-03-26  本文已影响0人  Robin92

Engine

Engine 是 gin 框架的一个实例,它包含了多路复用器、中间件和配置中心。

Engine 的启动

gin 通过 Engine.Run(addr ...string) 来启动服务,最终调用的是 http.ListenAndServe(address, engine),其中第二个参数应当是一个 Handler 接口的实现,即 engine 实现了此接口:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Engine.ServeHTTP() 会先初始化一个空的上下文,然后挂上请求 c.Reuqest = req,随后执行 engine.handlerHTTPRequest(c)(包含主要处理逻辑的函数)。

以上就是正常处理一个请求的主要逻辑,其他的就现阶段来说先忽略了。

RouterGroup

Engine 组合了 RouterGroup。

RouterGroup 实现了 IRouter 接口,IRouter 接口是 IRoutes 接口和 Group 函数组合而成。

// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}

// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
  // ...

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

RouterGroup 的结构

RouterGroup 的结构体只有四个属性:

type RouterGroup struct {
    Handlers HandlersChain // 创建时候会从父亲那 copy 一份,然后 append 指定的 handlers
    basePath string // 创建时候会从父亲那得到前缀,然后拼接指定的相对地址
    engine   *Engine // 会永远引用引擎
    root     bool // 标记位
}

当新建 Engine 时,会初始化一个 RouterGroup 结构,RouterGroup 是组合在 Engine 中的(所以 Engine 可以调用 RouterGroup 的所有方法),同时 Engine 的引用也记录在了 RouterGroup 上。

函数实现

如上,RouterGroup 实现了 IRouter 接口,下面是一些方法的实现。

StaticX 方法都加了路径中不允许存在变量(:*)的判断,所以使用是安全的。
var _ IRouter = &RouterGroup{} 可以用来检查 RouterGroup 是否实现了 IRouter 接口。👍

扩展:从 gin 对于 FileSystem 的实现可以探索更底层的东西。
gin.Dir(root string, listDirectory bool) 实现了对 http.Dir(root string) 的封装。
http.Dir() 用了本地文件系统的目录树,直接对外暴露一个文件夹有时候是不安全。比如文件中有些关键的隐藏文件等情况。
gin.Dir() 的第二个参数控制是否可以显示文件系统下的文件列表,默认 false 不显示,相对比较安全。
通过看源码发现 gin 是通过 onlyFilesFS.Readdir() 函数重写了 Readdir() 函数实现关闭 list 文件的。

Route 的添加

gin 通过上方 RouterGroup 暴露的几个方法添加路由,底层使用的方法是 Engine.addRoute(method, path string, handlers HandlerChain)

Engine.trees 属性是存储所有路由信息的总入口。它是一个切片,其中每个元素对应一种 method 并且是一个多叉树的根节点。

当 addRoute 时,先根据 method 找到对应的 tree (Engine.trees[i])。然后会比较 加入者 的 path 和 node 中的 path 相似的部分,相似的部分 作为 父结点,不同的部分作为 子结点。以 多叉树 的方式存储下来。

这里会把 URL 中的路由变量也当作字符串存入树中,因为相同 URL 他们的变量也是一样的。

Route 的匹配

当请求进来时,因为 Engine 实现了 Handler 接口,所以最后会调用到 Engine.ServeHTTP 内。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

gin 的 ServeHTTP 源码中可以看到获取它的 gin.Context 是通过池实现的,获取之后重置 ctx 中的信息。

找路径在

func (engine *Engine) handleHTTPRequest(c *Context) {
    // ...
    value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
    // ...
}

root.getValue() 比较复杂,这里就不多解释了。

gin 用的是 julienschmidt/httprouter 库的支持,所以可以参考这里。

Context

gin@v1.7.7 context.go

Context 中定义了一些属性和方法,用于扩展一些功能。

创建类方法

可以看到,这些方法主要用来获取 gin 自身 Context 的一些信息。

HanderName() 的主要实现是通过反射方法获取到函数的名称:
runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()

reflect.ValueOf(f).Pointer() 返回 f 的 uintptr 值
runtime.FuncForPC() 将 PC(program counter,程序计数器地址)解释为 *Func 类型,即 Go 中函数在运行时的二进制表示
*Func 上有三个函数

  • Name() 返回函数全名(包地址+函数名)
  • Entry() 返回函数的 uintptr 地址
  • FileLine(pc uintptr) 返回 pc 指针的文件名所在行号

流程控制类方法

Context 中保存了所有 handlers 列表,存在 Context.handlers 数组中,并用下标 Context.index 标记当前执行的位置。
当主动取消调用链时,会将 index 设置成一个最大值 63(math.MaxInt8 / 2),也即调用链最大支持 64 个函数。
Context 中还提供了其他一些函数,当取消调用链的时候,可以设置请求返回的状态码和返回数据信息等。

Context 中的 httpWriter 整理一下。

错误处理

gin 在 Context 中定义了错误信息字段 Context.Errors 切片,可以链式存储错误信息。

元数据管理

Go 原生的 Context 是通过 ValueContext 来存储元数据信息的,每个 ValueContext 只能存储一对信息,存储多个信息对需要将许多 ValueContext 组成链条,读写很不高效。
gin 的 Context 中存的元数据数据是存在 Context.Keys map[string]interface{} 属性中的,比起原生的 Context 使用起来会更高效。

元数据的读和写是并发安全的。
重复设置某一个 key,会更新存储的 value。

输入数据

Param 类

是指用在 URL 路径中设置的参数,如 /user/:id 的 id 参数。
存储在 Context.Params 属性中,其本质是一个切片,每一个元素是一个 K/V 元组。
因此,在 URL 中是可以使用重复的变量名的(如 /test/:id/case/:id),但获取值就需要自己从属性中获取了(如:c.Params[0])。

解析请求中 URL Params 参数的位置是在 Engine.handleHTTPRequest()

Query 类

Query 类是用在 URL 后的参数部分(如:?id=1)。

gin 通过 Context.queryCache 属性存储 query 参数,在调用获取 Query 参数时以懒加载的方式初始化:c.queryCache = c.Request.URL.Query()

需要注意的是它也支持传入 map 和 array,map 的传入需要像这样 ?m[k1]=v1&m[k2]=v2,array 的传入像这样 ?a=1&a=2

Form 类

包含 PostForm、FormFile、MultipartForm 等。
先略

绑定引擎

gin 为方便使用,通过绑定引擎设置了自动绑定用户输入和结构数据的方法。

响应渲染

这里包含设置状态码、设置响应头以及等信息。

只说一些值得注意的

内容协商

实现 context.Context 接口

这些方法除了 .Value() 方法外,其他都是返回的默认空值,略。

其他

上一篇 下一篇

猜你喜欢

热点阅读