大家好,我是渔夫子。本号新推出「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 {}// 实现ServeHTTPfunc (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 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("")}
在该函数中,调用了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
函数进行的初始化。其定义如下:
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)
就是路由匹配的过程。理论的路由匹配和执行过程实际上是在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文档。