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

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

作者: Robin92 | 来源:发表于2022-03-26 23:30 被阅读0次

    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)(包含主要处理逻辑的函数)。

    • engine.handlerHTTPRequest() 会先设置处理一些配置项,如 UseRawPath、RemoveExtraSlash 等
    • 然后开始寻找路由,从 engine.trees 中寻找
    • 当找到后执行找到路由对应的处理链 .handlers

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

    RouterGroup

    Engine 组合了 RouterGroup。

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

    • IRoutes 接口定义了所有路由处理的实现方法。
    • IRouter 接口定义了所有路由处理的实现方法以及一个分组方法(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 // 标记位
    }
    
    • .Handlers 属性是一个切片,按序存储着处理函数(中间件方法和最终的处理函数)
    • .basePath 属性是定位此 Group 的地址路径
    • .engine 属性总是指向 Engine 实例,且父子 Group 都存有相同的引用
    • .root 属性标记了此 Group 是否为根组,即最祖先的结点

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

    函数实现

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

    • Group() 方法,RouterGroup 通过 Group() 方法创建子分组,子分组会继承下来父 Group 的 handlers 并追加自己独有的 handlers,计算出此 Group 的 path 地址,及记录 Engine 地址。
    • POST(relativePath string, handlers ...HandlerFunc) 调用了 handle() 方法,是在 Group 中加一个路由(相对地址)及处理函数链(很常用就不多说了,其他类似方法也略)。
    • Handle(method, relativePath string, handlers ...HandlerFunc) 方法相对于 POST()/GET() 等方法只是可以传入自定义的方法名,用于特殊的、不标准的、Gin 内置不存在的的请求方法(不常用)。
    • Any() 会将路由及函数处理链在所有的支持方法上都 copy 存储一份,以实现通过任何请求方法都会有同样的调用链。
    • StaticFile(relativePath, filePath) 会将路由映射到文件系统的某一文件上,此时的 relativePath 是不允许有变量存在的(不允许有 : 和 * )。内部通过 c.File() 响应此文件。
    • Static(relativePath, root string) 将路由映射到文件系统的某一个文件夹上,底层调用了 StaticFS(relativePath, Dir(root, false))
    • StaticFS() 类似 Static(),但自定义 http.FileSystem 了,FileSystem 就可以理解为一个目录,这个目录就是所谓的文件系统。gin 的实现为了安全禁用了目录中的 list 功能。

    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 并且是一个多叉树的根节点。

    • Engine.trees 是一个数组 []methodTree
    • methodTree{method string, root *node}
    • node{} 是个结点

    当 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 的一些信息。

    • Copy() ,复制一个 Context 用于 goroutine 使用
    • HanderName() 方法,通过反射实现的获取 handler 的名字(以 包路径.NAME 的形式)
    • HandlerNames() 方法,返回完整的 handlers 链(这个真是相见恨晚的方法,可以用来调试一些调试中间件的一些异常问题,尤其是中间件中的数据传递问题)
    • Handler() 方法,返回主处理方法(最后一个)
    • FullPath() 方法,返回路由的 URL 全路径

    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 中还提供了其他一些函数,当取消调用链的时候,可以设置请求返回的状态码和返回数据信息等。

    • Next() 方法,用于中间件中才有意义,用于在当前函数中开始执行下一个 handler
    • IsAborted() 方法,判断当前上下文是否已经取消
    • Abort() 方法,设置调用链为取消状态。
    • AbortWithError() 方法,= AbortWithStatus() + Error()
    • AbortWithStatus() 取消调用脸并设置 http 状态码
    • AbortWithStatusJSON() 方法,= Abort() + JSON()

    Context 中的 httpWriter 整理一下。

    错误处理

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

    • Error(err error) *Error 用于将 err 追加到错误信息列表中。

    元数据管理

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

    • Set(key string, value interface{}) 设置键值对,存储到 Keys 属性中。
    • gin.Context 提供了比较丰富的获取各种类型数据的方法,如 GetString、GetTime、GetStringSlice、GetStringMapStringSlice 等等。

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

    输入数据

    Param 类

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

    • Param(key) 用于获取单个 URL 内的参数。

    解析请求中 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

    • Query(key),按 key 获取字符串参数值
    • DefaultQuery(key, defaultVal),获取字符串,当没有传时返回默认值。值得注意的是,这里“当没有传时”不包含传了但值为空的情况。即当传 ?a=&b=时,会取到空值而不是默认值。
    • QueryTYPE(),获取指定类型的数据,TYPE是个占位符,若无则返回空值。Type 可以是 Array、Map
    • GetQueryTYPE(key),比起 QueryType,这类函数会返回第二个参数表明参数中有没有设置

    Form 类

    包含 PostForm、FormFile、MultipartForm 等。
    先略

    • FormFile() 获取用户提交的表单中的文件。
    • SaveUploadedFile() 将用户表单提交的文件保存到服务器文件系统中。
    • MultipartForm()

    绑定引擎

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

    • Bind() 按 Content-Type 自动绑定结构数据。支持类型 JSON / XML / YAML / Form-Data / ProtoBuf / MsgPack(见binding 包的 Default() 函数)。
    • BindJSON/BindXML/BindYAML/... 指定各自类型的绑定。
    • BindHeader() 绑定 header
    • BindUri 绑定 URL path 内的参数到结构体中

    响应渲染

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

    只说一些值得注意的

    • Header(k, v string) 设置响应的 header,当值为空字符串时,相当于删除此 header
    • IndentedJSON() 一般不要在正式环境中用,因为输出格式化的 JSON 是很耗 CPU 的事。
    • JSONP() xxx
    • DataFromReader() 由 gin 从 io 中读数据并写到响应
    • File() 高效率地写一个文件到响应。调用的是 http.ServeFile(),会禁用包含 .. 的路径
    • FileFromFS() 指定 FileSystem 响应文件内容
    • FileAttachment() 用指定的文件名响应客户端下载附件。
    • SSEvent() 写一个 Server-Send 事件到信息流中。
    • Stream() 流式响应

    内容协商

    • Negotiate() 根据 Accept 的格式调用不同的 Render 方法
    • NegotiateFormat() 返回可接受到格式
    • SetAccepted() 设置 Accept header

    实现 context.Context 接口

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

    其他

    • ClientIP() 返回用户 IP 地址,先获取 RemoteIP 然后从 Header 中获取真实 IP
    • RemoteIP() 返回 RemoteAddr
    • IsWebsocket() 通过 Header 判断是否为 websocket 请求
    • GetRawData() 获取流数据(c.Request.Body)

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/nmdejrtx.html