Gin框架原理

标准库net/http、Gin框架原理

[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)

跟着路由一起注册后,调用RouterGroupcombineHandlers方法,将这些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源码阅读

Last updated