[TOC]
标准库net/http
demo:
定义了一个路由 /hello
,绑定了一个handler,输出当前path,ListenAndServe方法启动web服务,第一个参数表示监听的端口,第二个参数代表 处理所有的HTTP请求 的实例,等于nil时会使用默认的DefaultServeMux
。
该demo是基于标准库实现的Web框架入口。
func main() {
http.HandleFunc("/hello", func (w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
// http.ListenAndServe()方法第二个参数的实现:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// 注册handler默认使用DefaultServeMux,与ListenAndServe第二个参数使用的处理器一致。
// 将path和对应的handler方法保存在DefaultServeMux的map中,等请求进来进行匹配处理。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
http.ListenAndServe
是整个web的总流程,本质上还是走socket那一套,bind -》 listen -》 accept -》 read、write -》 close。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
...
ln, err := net.Listen("tcp", addr)
...
return srv.Serve(ln)
}
func (srv *Server) Serve(l net.Listener) error {
...
// 将listener封装进server
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
// 无限循环,accept接收客户端的请求
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
... // 设置默认超时时间
time.Sleep(tempDelay)
continue
}
return err
}
// 封装accept到的连接,起一个goroutine来处理这个连接,之后继续等待accept的返回
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
...
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
// 每次有连接进来都会起一个协程来处理
go c.serve(connCtx)
}
}
func (c *conn) serve(ctx context.Context) {
...
// 处理连接、tls处理之类的,从连接中读取数据,封装成request、response,交给serverHandler处理
...
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
// 该方法会起一个新协程在后台读取连接,就靠它来实现请求还没处理完就能进行取消的功能
// 实际上,取消后,只是连接被中断了,业务代码该执行还是在执行,执行完才能收到cancel信号
w.conn.r.startBackgroundRead()
}
// 默认的serverHandler.ServeHTTP方法会进行判断:
// 如果handler为空就会使用默认的DefaultServeMux,否则就用用户定义的ServeHTTP来处理请求
serverHandler{c.server}.ServeHTTP(w, w.req)
...
// 请求结束之后cancel掉context
w.cancelCtx()
}
// 处理连接时,处理会把连接交给serverHandler,还会起一个协程监控连接的状态
func (cr *connReader) startBackgroundRead() {
cr.lock()
defer cr.unlock()
...
cr.inRead = true
cr.conn.rwc.SetReadDeadline(time.Time{})
go cr.backgroundRead()
}
// 如果连接有问题,就会cancel掉
func (cr *connReader) backgroundRead() {
n, err := cr.conn.rwc.Read(cr.byteBuf[:])
cr.lock()
if n == 1 {
cr.hasByte = true
}
// 如果连接超时了,就会收到EOF的错误
if ne, ok := err.(net.Error); ok && cr.aborted && ne.Timeout() {
} else if err != nil {
// 出错时就会触发cancel,从而结束掉请求
cr.handleReadError(err)
// handleReadError方法内就是下面这两句
// cr.conn.cancelCtx()
// cr.closeNotify()
}
cr.aborted = false
cr.inRead = false
cr.unlock()
cr.cond.Broadcast()
}
// DefaultServeMux的ServeHTTP方法,对请求进行路由匹配,用的就是http.HandleFunc里的map
// 根据path找到对应的handler,执行
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
}
// 获取注册的路由handler
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
// 路由handler的逻辑非常简单,就是直接根据path在map里匹配找到处理的handler
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
// 只是做简单的路由匹配
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
Gin对net/http的封装
// http.ListenAndServe()方法第二个参数的实现:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Gin做的,就是实现这个Handler接口,掌管所有HTTP请求,提供丰富的能力:如路由分发、请求上下文的复用、中间件调用链等功能。
import "github.com/gin-gonic/gin"
func handlePing(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
}
// gin.Default还会默认包含两个中间件Logger和Recovery,还有对404、405处理的handler
func main() {
routerEngine := gin.Default()
// 三种注册路由的方式
routerEngine.GET("/ping", handlePing)
routerEngine.Handle(http.MethodGet, "/ping2", handlePing)
// 分组
v1 := routerEngine.Group("/v1")
v1.GET("/ping3", handlePing)
// run的底层仍然是http.ListenAndServe
routerEngine.Run() // 默认监听 0.0.0.0:8080
// -----
// 也可以用下面这种方法启动,本质还是调用gin.Default()的ServeHTTP方法
server := &http.Server{
Addr: ":8080",
Handler: routerEngine,
}
log.Fatal(server.ListenAndServe())
}
so,本质上也可以说gin是一个http router,主要看gin.Default()方法返回的engin,由gin的engin来进行请求封装、路由匹配、转发到对应的handler处理。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// gin.Context对象复用池
c := engine.pool.Get().(*Context)
// 重置gin.context对象
c.writermem.reset(w)
c.Request = req
c.reset()
// 交给engine处理请求,进入路由树匹配
engine.handleHTTPRequest(c)
// 处理完成把context放回复用池
engine.pool.Put(c)
}
路由
在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵「前缀树」,树的节点按照 URL 中的 / 符号进行层级划分,URL 支持 :name 形式的名称匹配,还支持 *subpath
形式的路径通配符。
每个节点都会挂一系列的handler组成的处理链来处理匹配到的请求。
之所以要使用前缀树,是为了支持如/hello/:name
这种动态路由,路径上的每一段/{名字}
作为前缀树的一个节点,通过树结构查询,如果中间某一层节点不匹配,则直接结束查询。
// engine与Route人Group相互持有的目的是为了实现分组路由时,注册到同一棵路由前缀树
// 并且可以使得中间件既可以注册在全路径上,也可以注册在分组上
type Engine struct {
RouterGroup
...
}
type RouterGroup struct {
...
engine *Engine
...
}
// 上面那三种路由注册方式,最终调用的还是RouterGroup的handle方法
// 由这个方法生成前缀树
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 解析路径
absolutePath := group.calculateAbsolutePath(relativePath)
// 组装成handlers chain
handlers = group.combineHandlers(handlers)
// 构建路由前缀树
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
// 当收到客户端请求时,在前缀树种找到对应的路由handler,处理请求
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
rPath = cleanPath(rPath)
// 遍历所有http method树,找到对应http method的前缀树,进行路由前缀匹配,找到对应的handler
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 获取对应的handler
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
// 执行handler方法
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
// 找不到http method对应的前缀树,使用默认的405Handler
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
// 即找不到http method对应的前缀树或path,使用默认的404handler
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
中间件
gin的中间件指的是handler方法,所有的中间件包括请求处理的handler都要满足这种方法签名:
type HandlerFunc func(*Context)
跟着路由一起注册后,调用RouterGroup
的combineHandlers
方法,将这些handler组成一个HandlersChain
,本质上是[]HandlerFunc
,当请求进来时,通过路径找到对应的HandlersChain
,注入到context中,调用Next方法处理请求。
// 执行HandlersChain
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
在gin中,context的Next方法
是会增加HandlersChain
索引值,执行下一个方法,而Abort方法
是直接更新HandlersChain
索引值到一个比较大的数字,使得循环调用结束, Abort() 方法并不是通过 panic 的方式中断执行流,执行 Abort() 方法之后,当前函数内后面的代码逻辑还会继续执行。
gin就是通过context的Next方法和Abort方法,使得执行HandlersChain
方法时可以嵌套执行:先从前往后顺序执行HandlersChain
中在Next方法前的逻辑,直到最后一个,然后再从后往前执行Next方法后的逻辑。
gin.Context
主要是包装请求*http.Request
、响应http.ResponseWriter
,让用户无需知道太多细节,比如消息头、消息体、状态码,消息类型等,直接提供现成包装好的方法比如context.Json()
;
Context 会随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载,比如动态路由/hello/:name
中的name参数;
为处理请求的handler方法提供统一的入参,使得context可以持有处理请求的一系列handler方法,实现中间件处理逻辑;
参考
极客兔兔 - Web框架 - Gee
轻量级 Web 框架 Gin 结构分析
gun源码阅读