21

gin 源码阅读(一)-- 启动

 4 years ago
source link: https://studygolang.com/articles/28068
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

文章首发于同名公众号,欢迎关注~

因为 gin 的安装教程已经到处都有了,所以这里省略如何安装, 建议直接去 github 官方地址的 README 中浏览安装步骤,顺便了解 gin 框架的功能。 https://github.com/gin-gonic/gin

最简单的代码

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    
    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

上面就是使用了 gin 框架的最简单的 demo ,接下来通过这个 demo 去一步步阅读分析 gin 是如何启动的。

gin.Run()

这里是服务器启动的地方,进去看看这个函数有什么奥秘:

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

除去第一行和第四行打印,是不是有点像 go 原生的 http 库?来看看 http 库的 web 服务简单 demo

package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", indexHandler)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

可以看到 gin 框架启动的时候,也是基于 http.ListenAndServe(addr string, handler Handler) 这个方法的,与原生的 http 库的区别就是, ListenAndServe(addr string, handler Handler) 这个方法的第二个参数,传入的是 engine ,我们继续来看看 ListenAndServe 方法的具体代码:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

server.ListenAndServe() 已经是服务监听并启动的方法,所以关键点在 Handler 这里:

// A Handler responds to an HTTP request.
// ...
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这是 http 包里的源码注释,其实写得很详细了,注释很多,我直接省略了,感兴趣的可以去阅读一下,不出意外,这个 Handler 是一个接口,从 gin.engine 的传入已经可以看出来了吧?注释的意思是这个接口是 处理程序响应HTTP请求 ,可以理解为,实现了这个接口,就可以接收所有的请求并进行处理。

因此 gin 框架的入口就是从这里开始, gin engine 实现了 ServeHTTP ,然后接管所有的请求走 gin 框架的处理方式。细心的读者应该能发现,原生的 http web 服务传入了 nil ,事实上是 http 库也有一个默认的请求处理器,感兴趣的读者可以去仔细阅读研究一下 go 官方团队的请求处理器实现哦~

那我们继续来看下 gin 框架是怎么实现这个 ServeHTTP 方法的:

// gin.go
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // 从池里获取资源处理请求
    c.writermem.reset(w) // 重置资源的 ResponseWriter
    c.Request = req // 赋值请求给 context
    c.reset()

    engine.handleHTTPRequest(c) // 将资源信息给到 handler 进一步处理

    engine.pool.Put(c) // 请求处理结束,将资源放回池子
}

可以看到实现并不难,代码的含义我已经写上了注释,这里的重点是 Context ,但不是这一篇文章的重点,我放在这一系列的后面进行讲解。

我们只需要知道, gin 也是实现了 Handler 接口,所以可以将请求按 gin 的处理方式进行处理,也就是我们可以使用 gin 来做 web 服务框架的起点。

gin.Default()

接下来进去 Default 函数去看具体实现,代码如下:

// gin.go
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault() // 忽略
    engine := New()
    engine.Use(Logger(), Recovery()) // 暂时忽略
    return engine
}

其实单单看函数名,已经知道这是构造默认的 gin engine ,可以看到 engine 是通过 New() 方法得到的,我们选忽略第一行和第三行。

// gin.go
func New() *Engine {
    debugPrintWARNINGNew() // 忽略
    engine := &Engine{
        RouterGroup: RouterGroup{ // 路由组,后面再分析
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine, // bool,是否为默认处理 engine
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine  // 重点,将 engine 重新赋值给路由组对象中的 engine
    engine.pool.New = func() interface{} { // 重点,资源池
        return engine.allocateContext()
    }
    return engine
}

到这里,我们不用看 engine 结构的具体定义,也已经看出来比较多的信息了,主要用路由组和资源池,这两个都可以分别展开写一篇文章,由于篇幅有限,这里就先不介绍了。

值得注意的是这里面有两个地方很巧妙:

engine.RouterGroup.engine = engine

很明显,路由组 RouterGroup 中还有个 engine 的指针对象,为什么要这么设计呢?读者们可以思考一下。

engine.pool.New = func() interface{} { // 对象池
        return engine.allocateContext()
    }

看下 engine.allocateContext() 方法:

func (engine *Engine) allocateContext() *Context {
    return &Context{engine: engine}
}

可以看到 engine 中包含了 pool 对象池,这个对象池是对 gin.Context 的重用,进一步减少开销,关于 sync.pool 对象池我就不在这里细说了, 后续再更新关于 sync.pool 的文章。

engine

最后再来看看 engine 的结构:

type Engine struct {
    // 路由组
    RouterGroup

    // 如果true,当前路由匹配失败但将路径最后的 / 去掉时匹配成功时自动匹配后者
    // 比如:请求是 /foo/ 但没有命中,而存在 /foo,
    // 对get method请求,客户端会被301重定向到 /foo
    // 对于其他method请求,客户端会被307重定向到 /foo
    RedirectTrailingSlash bool

    // 如果true,在没有处理者被注册来处理当前请求时router将尝试修复当前请求路径
    // 逻辑为:
    // - 移除前面的 ../ 或者 //
    // - 对新的路径进行大小写不敏感的查询
    // 如果找到了处理者,请求会被301或307重定向
    // 比如: /FOO 和 /..//FOO 会被重定向到 /foo
    // RedirectTrailingSlash 参数和这个参数独立
    RedirectFixedPath bool

    // 如果true,当路由没有被命中时,去检查是否有其他method命中
    //  如果命中,响应405 (Method Not Allowed)
    //  如果没有命中,请求将由 NotFound handler 来处理
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool

    // #726 #755 If enabled, it will thrust some headers starting with
    // 'X-AppEngine...' for better integration with that PaaS.
    AppEngine bool

    // 如果true, url.RawPath 会被用来查找参数
    UseRawPath bool

    // 如果true, path value 会被保留
    // 如果 UseRawPath是false(默认),UnescapePathValues为true
    // url.Path会被保留并使用
    UnescapePathValues bool

    // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
    // method call.
    MaxMultipartMemory int64

    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    pool             sync.Pool
    
    //每个http method对应一棵树
    trees            methodTrees
}

上面提到过 ServeHTTP 这个方法,其中 engine.handleHTTPRequest(c) 这行代码就是具体的处理操作。

可以看到 engine 的结构中有这么一个字段 RedirectTrailingSlash ,在 Default() 初始化方法中为 true ,我对此比较感兴趣,大家也可以根据注释的意思来测试一下,最终会走到下面的代码中:

func (engine *Engine) handleHTTPRequest(c *Context) {
    ……
    if httpMethod != "CONNECT" && rPath != "/" {
        if value.tsr && engine.RedirectTrailingSlash {
            // 这里就是尝试纠正请求路径的函数
            redirectTrailingSlash(c)
            return
        }
        if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
        }
    }
    ……
}

总结一下

R7bQbqa.png!web

上图就是本文的核心了,可以结合图来理解一下 gin 启动的过程及设计,下一篇会将 gin 的路由,敬请期待~

本系列 “拆轮子系列:gin 框架” 的第一篇就到这里了,这么通读下来,发现 gin 框架的设计和实现真的太棒了,简洁清晰,又不失巧妙,很适合大家也去阅读学习一下,墙裂推荐!!!

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK