大家好,我是渔夫子。本号新推出「Go工具箱」系列,意在给大家分享应用go语言编写的、实用的、好玩的工具。同时理解其底层的实现原理,以便更深刻地理解Go语言。

在理论工作中,大家肯定会用到go的web框架。那么,你晓得各框架是如何解决http申请的吗?明天就支流的web框架ginbeego框架以及go规范库net/http来总结一下http申请的流程。

一、规范库 net/http 的申请流程

首先,咱们来看下http包是如何解决申请的。通过以下代码咱们就能启动一个http服务,并解决申请:

import (    "net/http")func main() {    // 指定路由    http.Handle("/", &HomeHandler{})    // 启动http服务    http.ListenAndServe(":8000", nil)}type HomeHandler struct {}// 实现ServeHTTPfunc (h *HomeHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {    response.Write([]byte("Hello World"))}

当咱们输出http://localhost:8000/的时候,就会执行到HomeHandlerServeHTTP办法,并返回Hello World

那这里为什么要给HomeHandler定义ServeHTTP办法,或者说为什么会执行到ServeHTTP办法中呢?

咱们顺着http.ListenAndServe办法的定义:

func ListenAndServe(addr string, handler Handler) error

发现第二个参数是个Handler类型,而Handler是一个定义了ServeHTTP办法的接口类型:

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

仿佛有了一点点关联,HomeHandler类型也实现了ServeHTTP办法。但咱们在main函数中调用http.ListenAndServe(":8000", nil)的时候第二个参数传递的是nil,那HomeHandler里的ServeHTTP办法又是如何被找到的呢?

咱们接着再顺着源码一层一层的找上来能够发现,在/src/net/http/server.go的第1930行有这么一段代码:

serverHandler{c.server}.ServeHTTP(w, w.req)

有个serverHandler构造体,包装了c.server。这里的c是建设的http连贯,而c.server就是在http.ListenAndServe(":8000", nil)函数中创立的server对象:

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

server中的Handler就是http.ListenAndServe(":8000", nil)传递进来的nil

好,咱们进入 serverHandler{c.server}.ServeHTTP(w, w.req)函数中再次查看,就能够发现如下代码:

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {    handler := sh.srv.Handler    if handler == nil {        handler = DefaultServeMux    }    ...    handler.ServeHTTP(rw, req)}

/src/net/http/server.go的第2859行到2862行,就是获取到server中的Handler,如果是nil,则应用默认的DefaultServeMux,而后调用了hander.ServeHTTP办法。

持续再看DefaultServeMux中的ServeHTTP办法,在/src/net/http/server.go中的第2416行,发现有一行h, _ := mux.Handler(r)h.ServeHTTP办法的调用。这就是通过申请的门路查找到对应的handler,而后调用该handlerServeHTTP办法。在开始的实例中,就是咱们的HomeHandlerServeHTTP办法。

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)    h.ServeHTTP(w, r)}

也就是说**ServeHTTP**办法是**net/http**包中规定好了要调用的,所以每一个页面处理函数都必须实现ServeHTTP办法

二、gin框架的http的申请流程

gin框架对http的解决流程实质上都是基于go规范包net/http的解决流程的。 上面咱们看下gin框架是如何基于net/http实现对一个申请解决的。
首先咱们看通过gin框架是如何启动http服务的:

import (    "github.com/gin-gonic/gin")func main() {    //  初始化gin中自定义的Engine构造体对象    engine := gin.New()    // 增加路由    engine.GET("/", HomeHandler)    // 启动http服务    engine.Run(":8000")}func HomeHandler(ctx *gin.Context) {    ctx.Writer.Write([]byte("Hi, this is gin Home page"))}

咱们查看engine.Run函数的源码,发现也是通过net/http包启动的http服务。如下:

func (engine *Engine) Run(addr ...string) (err error) {    defer func() { debugPrintError(err) }()    if engine.isUnsafeTrustedProxies() {        debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +            "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")    }    address := resolveAddress(addr)    debugPrint("Listening and serving HTTP on %s\n", address)    err = http.ListenAndServe(address, engine.Handler())    return}

函数较短,在第11行,通过http.ListenAndServe(address, engine.Handler())函数启动的http服务。和第一节中的通过go的规范库net/http启动的服务形式一样,只不过第二个参数不是nil,而是engine.Handler()

咱们持续查看engine.Handler()函数的源码,发现该函数返回的是一个http.Handler类型。在源代码中,返回的是engine对象。这里暂且不探讨应用http2的状况。也就是说engine实现了http.Handler接口,即实现了http.Handler接口中的ServeHTTP函数。

func (engine *Engine) Handler() http.Handler {    if !engine.UseH2C {        //  这里间接返回了engine对象        return engine    }    h2s := &http2.Server{}    return h2c.NewHandler(engine, h2s)}

咱们再查看Engine构造体中实现的办法,发现有ServeHTTP函数的实现,如下:

// 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)    c.Request = req    c.reset()    engine.handleHTTPRequest(c)    engine.pool.Put(c)}

这里咱们次要看第8行的engine.handleHTTPRequest(c)函数,代码如下:

func (engine *Engine) handleHTTPRequest(c *Context) {    httpMethod := c.Request.Method    rPath := c.Request.URL.Path    //省略代码...    // 依据申请的办法httpMethod和申请门路rPath查找对应的路由    t := engine.trees    for i, tl := 0, len(t); i < tl; i++ {        if t[i].method != httpMethod {            continue        }        root := t[i].root        // 在路由树中找到了该申请门路的路由        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()            c.writermem.WriteHeaderNow()            return        }        // 省略代码...    }    // 省略代码...    // 没有找到路由,则返回404    c.handlers = engine.allNoRoute    serveError(c, http.StatusNotFound, default404Body)}

次要看第14行的代码局部,依据申请的门路查找路由,找到了对应的路由,从路由中获取该门路对应的处理函数,赋值给该框架自定义的上下文对象c.handlers,而后执行c.Next()函数。

c.Next()函数实际上就是循环c.handlers,源码如下:

func (c *Context) Next() {    c.index++    for c.index < int8(len(c.handlers)) {        c.handlers[c.index](c)        c.index++    }}

c.handlers是一个HandlersChain类型,如下:

type HandlersChain []HandlerFunc

HandlersChain类型实质上是一个HandlerFunc数组,而HandlerFunc类型的定义如下:

type HandlerFunc func(*Context)

这个函数类型是不是就是在注册路由engine.GET("/", HomeHandler)HomeHandler的类型呢?如下是咱们注册路由以及定义HomeHandler的代码:

import (    "github.com/gin-gonic/gin")func main() {    //  初始化gin中自定义的Engine构造体对象    engine := gin.New()    // 增加路由    engine.GET("/", HomeHandler)    // 启动http服务    engine.Run(":8000")}func HomeHandler(ctx *gin.Context) {    ctx.Writer.Write([]byte("Hi, this is gin Home page"))}

这样就造成了一个解决流程的闭环。咱们总结下gin框架对http申请的解决流程。

  • 首先,通过gin.New()创立一个Engine构造体实例,该Engine构造体实现了net/http包中的http.Handler接口中的ServeHTTP办法。
  • 通过engine.Run函数启动服务。实质上也是通过net/http包中的http.ListenAndServe办法启动服务的,只不过是是将engine作为服务接管申请的默认handler。即Engine.ServeHTTP办法。
  • 在Engine构造体的ServeHTTP办法中,通过路由查找找到该次申请的对应路由,而后执行对应的路由执行函数。即func(ctx *gin.Context)类型的路由。

以下是gin框架解决http申请的全景图:

三、beego框架的http申请解决流程

beego框架启动http服务并监听解决http申请实质上也是应用了规范包net/http中的办法。和gin框架不同的是,beego间接应用net/http包中的Server对象进行启动,而并没有应用http.ListenAndServe办法。但实质上是一样的,http.ListenAndServe办法的底层是也调用了net/http包中的Server对象启动的服务。

首先咱们看下beego框架启动http服务的过程:

package mainimport (    "github.com/beego/beego/v2/server/web"    beecontext "github.com/beego/beego/v2/server/web/context")func main() {    web.Get("/home", HomeHandler)    web.Run(":8000")}func HomeHandler(ctx *beecontext.Context){        ctx.Output.Body([]byte("Hi, this is beego home"))}

在上述代码中,咱们注册了一个 /home 路由,而后再8000端口上启动了http服务。接下来咱们看下web.Run(":8000")的外部实现:

func Run(params ...string) {    if len(params) > 0 && params[0] != "" {        BeeApp.Run(params[0])    }    BeeApp.Run("")}

在该函数中,调用了BeeAppRun办法。 这里你会发现有两次BeeApp.Run调用,为什么要调用两次呢?这里其实不是一个bug。咱们进BeeApp.Run函数就能够晓得,其实Run办法运行后就阻塞了,不会进行最初的BeeApp.Run("")调用,所以不会呈现两次调用。如下在第34行时,实际上是通过通道的输入形式进行了阻塞(这里为进行阐明,只列出了相干的代码):

func (app *HttpServer) Run(addr string, mws ...MiddleWare) {    // init...    app.initAddr(addr)    app.Handlers.Init()    addr = app.Cfg.Listen.HTTPAddr    var (        err        error        l          net.Listener        endRunning = make(chan bool, 1)    )    app.Server.Handler = app.Handlers        if app.Cfg.Listen.EnableHTTP {        go func() {            app.Server.Addr = addr                        if app.Cfg.Listen.ListenTCP4 {                // 省略...            } else {                if err := app.Server.ListenAndServe(); err != nil {                    logs.Critical("ListenAndServe: ", err)                    // 100毫秒 让所有的协程运行实现                    time.Sleep(100 * time.Microsecond)                    endRunning <- true                }            }        }()    }    // 通过通道进行阻塞    <-endRunning

咱们再具体看下BeeApp实例。BeeApp*HttpServer类型的实例,在导入包时,通过init函数进行的初始化。其定义如下:

var BeeApp *HttpServer

咱们看下HttpServer的构造体蕴含的次要字段如下:

有两个要害的字段,一个是http.Server类型的Server,这个就是用来启动并监听服务。看吧,万变不离其宗,最终启动和监听服务还是应用go规范包中的net/http。

另外一个就是ControllerRegister类型的Handlers。这个字段就是用来治理路由和http申请的入口。咱们看下ControllerRegister构造体的关键字段:

ControllerRegister中要害的字段也有两个,一个是路由表routers,一个是进行路由匹配的FilterRouter类型。

咱们再来看ControllerRegister构造体实现的办法中有一个是ServeHTTP办法,阐明是实现了标准表net/http中的http.Handler接口,源码如下:

func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) {    ctx := p.GetContext()    ctx.Reset(rw, r)    defer p.GiveBackContext(ctx)    var preFilterParams map[string]string    p.chainRoot.filter(ctx, p.getUrlPath(ctx), preFilterParams)}

其中第8行的 p.chainRoot.filter(ctx, p.getUrlPath(ctx), preFilterParams)就是路由匹配的过程。理论的路由匹配和执行过程实际上是在ControllerRegisterserveHttp办法中,这里留神和http.Handler接口的ServerHTTP办法的首字母的大小写的区别。 serveHttp办法是在初始化chainRoot对象时指定的过滤函数,在第13行的newFilterRouter的第二个参数就是具体的路由匹配函数,如下:

func NewControllerRegisterWithCfg(cfg *Config) *ControllerRegister {    res := &ControllerRegister{        routers:  make(map[string]*Tree), //路由表,一个办法一棵树        policies: make(map[string]*Tree),        pool: sync.Pool{            New: func() interface{} {                return beecontext.NewContext()            },        },        cfg:          cfg,        filterChains: make([]filterChainConfig, 0, 4),    }    res.chainRoot = newFilterRouter("/*", res.serveHttp, WithCaseSensitive(false))    return res}

最初,咱们再看下路由注册的过程。路由注册有三种形式,这里咱们只看其中的一种:用可执行函数进行注册,如下:

web.Get("/home", HomeHandler)func HomeHandler(ctx *beecontext.Context){        ctx.Output.Body([]byte("Hi, this is beego home"))}

这里HomeHandler就是一个函数类型。咱们随着web.Get的源码一路找上来,发现最终会返回一个ControllerInfo路由信息:

func (p *ControllerRegister) createRestfulRouter(f HandleFunc, pattern string) *ControllerInfo {    route := &ControllerInfo{}    route.pattern = pattern    route.routerType = routerTypeRESTFul    route.sessionOn = p.cfg.WebConfig.Session.SessionOn    route.runFunction = f    return route}

大家看,第6行的f就是HomeHandler这个函数,给路由的runFunction进行了赋值。 在路由匹配阶段,找到了对应的路由信息后,就执行route.runFunction即可。

好了,beego框架解决http申请的流程根本就是这样,具体的路由实现咱们后续再独自起一篇文章介绍。如下是该框架解决http申请的一个全景图:

四、总结

通过以上两个风行的开源框架gin和beego以及go规范包net/http解决http申请的剖析,能够得悉所有的web框架启动http服务和解决http的流程都是基于go规范包net/http执行的。 其本质流程都都是通过net/http启动服务,而后调用handler中的ServeHTTP办法。而框架只有实现了http.Handler接口中的ServeHTTP办法,并作为http服务的默认入口,就能够在框架中的ServeHTTP办法中进行路由散发了。如下图:


特地举荐:一个专一go我的项目实战、我的项目中踩坑教训及避坑指南、各种好玩的go工具的公众号,「Go学堂」,专一实用性,十分值得大家关注。点击下方公众号卡片,间接关注。关注送《100个go常见的谬误》pdf文档。