写点什么

「Go 框架」通过分析 gin、beego 框架, 读懂 web 框架对 http 请求的处理流程

作者:Go学堂
  • 2022-12-28
    北京
  • 本文字数:6257 字

    阅读完需:约 21 分钟

「Go框架」通过分析gin、beego框架,读懂web框架对http请求的处理流程

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

发布于: 2022-12-28阅读数: 49
用户头像

Go学堂

关注

关注「Go学堂」,学习更多编程知识 2019-08-06 加入

专注Go编程知识、案例、常见错误及原理分析。意在通过阅读更多优秀的代码,提高编程技能。同名公众号「Go学堂」期待你的关注

评论

发布
暂无评论
「Go框架」通过分析gin、beego框架,读懂web框架对http请求的处理流程_golang_Go学堂_InfoQ写作社区