通过分析gin、beego源码,读懂web框架对http请求处理流程的本质

386次阅读  |  发布于1年以前

在实际工作中,大家一定会用到go的web框架。那么,你知道各框架是如何处理http请求的吗?今天就主流的web框架ginbeego框架以及go标准库net/http来总结一下http请求的流程。

一、标准库 net/http 的请求处理流程

首先,我们来看下http包是如何处理请求的。通过以下代码我们就能启动一个http服务,并处理请求:

import (
 "net/http"
)

func main() {
    // 指定路由
 http.Handle("/home", &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/home的时候,就会执行到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框架处理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请求的一个全景图:

image.png

四、总结

通过以上两个流行的开源框架gin和beego以及go标准包net/http处理http请求的分析,可以得知所有的web框架启动http服务和处理http的流程都是基于go标准包net/http执行的。 其本质流程都都是通过net/http启动服务,然后调用handler中的ServeHTTP方法。而框架只要实现了http.Handler接口中的ServeHTTP方法,并作为http服务的默认入口,就可以在框架中的ServeHTTP方法中进行路由分发了。如下图:

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8