大家好,我是渔夫子。本号新推出「Go 工具箱」系列,意在给大家分享使用 go 语言编写的、实用的、好玩的工具。同时了解其底层的实现原理,以便更深入地了解 Go 语言。
在实际工作中,大家一定会用到 go 的 web 框架。那么,你知道各框架是如何处理 http 请求的吗?今天就主流的 web 框架gin
、beego
框架以及 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/
的时候,就会执行到HomeHandler
的ServeHTTP
方法,并返回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
,然后调用该handler
的ServeHTTP
方法。在开始的实例中,就是我们的HomeHandler
的ServeHTTP
方法。
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("")
}
复制代码
在该函数中,调用了BeeApp
的Run
方法。 这里你会发现有两次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
函数进行的初始化。其定义如下:
我们看下 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)
就是路由匹配的过程。实际的路由匹配和执行过程实际上是在ControllerRegister
的serveHttp
方法中,这里注意和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 文档。
评论