关于go:Go框架一文读懂主流web框架中路由的实现原理

1次阅读

共计 8936 个字符,预计需要花费 23 分钟才能阅读完成。

大家好,我是渔夫子。本号新推出「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 {}

// 实现 ServeHTTP
func (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 main

import (
    "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 文档。

正文完
 0