gin和http包的使用

gin,beego等底层都用的是net/http模块,上篇文章中对一个基本的http请求作了分析,这篇文章就gin怎么用的http模块的流程进行梳理。
gin的github地址
来看一个基本的gin项目代码:

package main
import "github.com/gin-gonic/gin"
func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong",})
    })
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

可以看到一个基本的启动过程:
(1)首先生成一个gin.Default
(2)设置路由r.GET...
(3)启动listen and serve
例行先提出问题:
(q1)gin是怎么使用到net/http的模块的?
(q2)gin的路由处理流程?
通过每个流程的分析来解答:

一、gin.Default

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault()        // go版本的校验(>1.8.x),debugPrint,debug模式的时候会有相关打印
    engine := New()              // 生成一个Engine结构体
    engine.Use(Logger(), Recovery())        // 用了Logger和Recovery两个middleware,同时也可以自己定义middleware
    return engine        // 返回结构体
}
1.1 debugPrintWARNINGDefault:

关于版本的校验等信息,debug模式下的启动日志打印

func debugPrintWARNINGDefault() {
    if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {                          //  版本的校验,不能小于1.8
        debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.
`)}
    debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)
}
func getMinVer(v string) (uint64, error) {
    first := strings.IndexByte(v, '.')
    last := strings.LastIndexByte(v, '.')
    fmt.Println(first)
    fmt.Println(last)
    if first == last {
        return strconv.ParseUint(v[first+1:], 10, 64)
    }
    return strconv.ParseUint(v[first+1:last], 10, 64)
}
1.2 engine := New()

New函数用于生成一个新的Engine结构体,可以看到各种参数配置
首先我们来关注Engine结构体的几个参数:
(1)RouterGroup:RouterGroup 描述的一个路由组,其中包含了这个路由组的相关公共属性,比如后面可以通过(group *RouterGroup) Group方法修改basePath字段,用于加一些公共的路由前缀(eg:"api/v1"),。通过(group *RouterGroup) Use(middleware ...HandlerFunc)方法改变Handlers字段,用于添加公共的中间件。
贴一段项目代码:

// InsecureRouterGroup returns router group without auth
func InsecureRouterGroup() *gin.RouterGroup {
    lock.Lock()
    r := gEngine.Group(constants.APIVERSION)
    r.Use(RequestID)
    lock.Unlock()
    return r
}

(2)各种bool参数,用于对重定向(redirect),转发(ForwardedByClientIP)等一些属性的控制
(3)trees:类型为methodTrees,是每个不同方法的路由集合,原理参考go的http/router框架。大致就是一个路由查找树,用到了radix tree(前缀树)或者说是压缩检索树的数据结构,来进行路由匹配,可以大大的提高路由查找的效率。

type methodTrees []methodTree
type methodTree struct {
    method string
    root   *node
}
type node struct {
    path      string              // 当前节点的对应的子查找路径
    indices   string              // 子节点的首字母的组成的字符串
    children  []*node            // 子节点
    handlers  HandlersChain     // 这个node对应要处理的Handlers
    priority  uint32
    nType     nodeType      // 当前节点类型(eg: root(根节点)/default(普通的)/param(参数类型)/catchAll(通配符))
    maxParams uint8
    wildChild bool        // 当前节点的子节点是否是参数节点
}

(4)pool:sync.Pool类型,New方法定义为return engine.allocateContext(),返回的结构体是gin.Context结构体,便于对结构体的复用,减少gc。
抛出问题
(qa6):gin.Context结构体在哪里被用到的?具体的代码处理逻辑?

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,        // 这个路由组的公共hanlers,后面会给其加上middleware
            basePath: "/",      // 路由地址为"/"
            root:     true,         // 表明这个为根路由
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),              // 给methodTrees定义一个methodTree的数组
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

抛出问题:
(q3)路由查找的时候RouterGroup和trees都是在哪里被用到了?

1.3 engine.Use(Logger(), Recovery())

这里用了gin的中间件Logger和Recovery

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)        // use中间件,把加入的middleware加入到engine.RouterGroup.Handlers中,在后面加入具体的业务代码的时候会combine这个Handlers和具体的业务handler,解释了(qa3)的问题。
    engine.rebuild404Handlers()          // 用于重写noroute的handler,具体的方法实现在func (engine *Engine) NoRoute(handlers ...HandlerFunc)中,用于用户自己定义
    engine.rebuild405Handlers()      // 同上,用于重写nomethod的handler
    return engine
}
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

至此对于gin.Default的代码分析完毕

二、r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message":"pong",}) })

先贴源码:

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("GET", relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath) // 合并相对路径和routerGroup的basePath
    handlers = group.combineHandlers(handlers) // 把业务的handler和共用的handler(middleware)结合到一起,返回一个新的HandlersChain
    group.engine.addRoute(httpMethod, absolutePath, handlers) // 把path加入到相应的method的检索树中去,关联相应的handlers
    return group.returnObj()
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    if finalSize >= int(abortIndex) {
        panic("too many handlers")
    }
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")

    debugPrintRoute(method, path, handlers)
    root := engine.trees.get(method)      // 查看engine.trees是否已经有了此method,没有的话创建出一个新的method的node
    if root == nil {
        root = new(node)
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)        // 把handlers加入到method的node中
}

总结:r.GET方法工作(1)合并拿到绝对路径 (2)将业务handler和middle handler合并为mergeHandler(3)将路径,mergeHandler,method(httprouter的节点相关元素)加入到methodTrees中。
总的来说处理过程符合httprouter那一套逻辑。

三、r.Run()

现在我们已经有了一个类型为Engine的handler(实现了ServeHTTP的方法),以及一个Engine里的路由检索树已经建立好了。
抛出问题
(qa4):对于进来的请求怎么进行监听的?
(qa5):对于路由的匹配是在哪里进行的?

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
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
}

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

可以看到这里的Run即为将engine这个handler放入到http.ListenAndServe中去,然后启用监听。。。。,监听流程和net/http的处理流程一致。(解答了qa4)

对于qa5:
首选回顾一下,net/http中的路由查找是怎么执行的吗?看下面的代码,其实发现net/http用的是ServeMux这个handler,也是实现了ServeHTTP这个方法,具体的路由匹配在mux.Handler(r) 中

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)                  //  handler的路由处理
    h.ServeHTTP(w, r)                       //  ServeHTTP的实现,对应到用户的业务逻辑处理
}

而engine即类似于ServeMux,也实现了一个ServeHTTP方法,也是在方法里进行了路由的匹配以及具体逻辑的处理,详细见下面代码。

// ServeHTTP conforms to the http.Handler interface。
// 这个方法的实现是在一个goroutine中实现的,就是说每对一个新的HTTP请求,都会生成一个新的goroutine去处理(并发处理详见net/http的处理过程)。
//对于这个HTTP请求,会先触发ServeHTTP方法得到一个Context请求上下文,然后调用engine.handleHTTPRequest(C),处理这个请求,同时将处理的内容写入到http.ResponseWriter中。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)      // 从池子里拿出来,要是没有了的话自己new一个出来。有并发请求进来的时候,会产生多个context
    c.writermem.reset(w)      // 对context的ResponseWriter赋为w
    c.Request = req            // 对context的Request赋为req
    c.reset()        // 对拿到的context进行reset,类似于清空,然后才进行使用
    engine.handleHTTPRequest(c)      // 这里就是最后的逻辑处理,包括(1)通过context .Request.path找到对应的handler   (2)middleware的执行 (3)业务代码的执行 (4)写入结果到context. ResponseWriter中去
    engine.pool.Put(c)        // 用完了之后放回到engine.pool中去
}

注释解释了qa6。
对于engine.handleHTTPRequest(c)中的处理逻辑,涉及到了middleware的处理,下面拉出来分析一下:

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }
    rPath = cleanPath(rPath)
    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {         // 遍历methodTrees,找到当前方法的methodTree   
            continue
        }
        root := t[i].root                // 找到methodTree的root
        // Find route in tree
        handlers, params, tsr := root.getValue(rPath, c.Params, unescape)        // 拿到当前tree下的这个路径下的handler和参数等
        if handlers != nil {
            c.handlers = handlers
            c.Params = params
            c.Next()              // 这里是真正的进行handler的执行,包括middleware和业务handle
            c.writermem.WriteHeaderNow()        // 写入header
            return  
        }
        if httpMethod != "CONNECT" && rPath != "/" {          // 跨域等操作
            if tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }
    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)                        // 会进行递归执行middleware,直到执行到业务代码。一般的middleware会有个c.Next方法。像cors等没有写c.Next的,其实在for c.index < int8(len(c.handlers))遍历中也会被执行到。
        c.index++
    }
}

可以看到在handleHTTPRequest执行过程中,都是通过context结构体进行上下文传递的,传递的内容包括Request,Writer,handlers,Params等等。

总结:

至此可以看到gin作为一个web框架,其实就是重写了原生的ServeMux的实现。Engine就是一个类似于ServeMux的handler。
基本的web实现的时候,重新实现的逻辑主要有:
(1)对于路由的查找,用了http/router的路由框架,并在ServeHTTP方法中进行了路由的查找。
(2)用了中间件的处理逻辑,并在路由添加的时候进行了路由的合并等操作,并且在handleHTTPRequest的时候也会对每个中间件进行处理。
后面有新的发现进行补充。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343