gin源码学习-项目启动(1)

最近公司后台项目也在用golang的gin框架进行重构,从gin官方给出的性能对比数据来看,性能十分优秀,而且干净整洁,少量代码就可以起一个web项目,简直不要太爽,目前github上的star数量已经65k(截至2022-12),十分亮眼。

已经有一定实践经验之后,这下来看看gin的具体实现,一来巩固,二来学习优秀开源框架的一些思想。

后面的源码学习与gin的使用,初步计划从这个方面入手:

1.项目quickstart

代码:

package main

import (
	"gin-any/api" // 项目内的handlerfunc
	"github.com/gin-gonic/gin"
)

func main()  {
        // 1.新建engine引擎
	engine := gin.Default()
        // 2.注册路由
        engine.GET("/hello", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{
			"msg": "hello gin!",
		})
	})
	engine.POST("/login", api.Login)
        // 注册路由,同时加入中间件
	engine.GET("/user", api.AuthJWTMiddleware(), api.User)
        // 3.启动项目
	engine.Run(":8080")
}

我们看到gin项目的server仅需要简单几步,即可起来web server,接下来分别从代码中的3个步骤说明gin项目启动。

首先了解几个重要概念:

  • engine:总的引擎,保存各个组件信息
  • RouterGroup:路由组,保存路由信息
  • engine.pool:sync.pool,复用context
  • tree:radix tree或者认为是字典树,保存url与handler的映射关系
  • context:请求与响应,用于request传递

1.gin.Default()

调用流程

源码

// gin-gonic/gin/gin.go
func Default() *Engine {
	debugPrintWARNINGDefault() // 打印日志
	engine := New() // 新建engine
	engine.Use(Logger(), Recovery()) // 默认加入log/recovery中间件
	return engine
}

func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{ // 新建engine实例
		RouterGroup: RouterGroup{ // RouterGroup
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
		TrustedPlatform:        defaultPlatform,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		trees:                  make(methodTrees, 0, 9),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
		trustedCIDRs:           defaultTrustedCIDRs,
	}
	engine.RouterGroup.engine = engine // 初始化RouterGroup
	engine.pool.New = func() any { // 初始化pool
		return engine.allocateContext()
	}
	return engine
}

// 加入中间件
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
	Handlers HandlersChain // type HandlersChain []HandlerFunc
	basePath string
	engine   *Engine
	root     bool
}

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...) // 加入到handlers的切片中, group.Handlers是组handlers或是全局的handlers
	return group.returnObj()
}

从源码中可以看到,这一步的主要工作就是新建engine实例,初始化RouterGroup和pool,顺带把默认的log和recovery中间件注册进来。

在测试的时候,通过gin.Default()新建engine是没问题的,但如果你读过net/http的源码就会发现,这里会有问题?
什么问题呢?很多第三方的库其实也是基于net/http库构建的,其中很多也是用到了Servemux的默认设置,这里就是问题,所以这里就引出另外一个问题,gin.New()

gin.New()
同样先来看看demo:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.New()

	server := http.Server{
		Addr: ":8080",
		// called handler, if nil default http.DefaultServeMux
		Handler:      engine,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	engine.GET("/ping", pong)

	err := server.ListenAndServe()
	if err != nil {
		log.Fatalln("server start failed:", err)
	}
}

func pong(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, gin.H{
		"msg": "pong",
	})
}

我们知道gin是基于net/http来构建自己的service,那就通过gin.New()构建自定义的gin server,中间件就是自己定义了,而且我们还可以设置handler,超时时间等,当然其他的和gin.Default()没啥不一样的了。

2.engine.Get() | engine.Post()等

调用流程

源码

// gin-gonic/gin/routerGroup.go

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { // 注册路由
	return group.handle(http.MethodGet, relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
        // 根据传入的handlers,比如可能只是添加url的处理函数,或者添加中间件+url的处理函数,新建个handlerfuncs,
        // 将原有的gorup.handlers copy过去,然后加添加的handlers加到尾部
	handlers = group.combineHandlers(handlers)  
	group.engine.addRoute(httpMethod, absolutePath, handlers) // 根据method/path/handlers加入到路由
	return group.returnObj()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	assert1(finalSize < int(abortIndex), "too many handlers")
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}


func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	...

	debugPrintRoute(method, path, handlers)

	root := engine.trees.get(method) // 根据method尝试获取root节点
	if root == nil {                 // root节点为空,新建root节点
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root}) // 加入到radix tree中
	}
	root.addRoute(path, handlers) // adds a node with the given handle to the path.

	// Update maxParams
	...
}

从源码中我们可以看到,基本上可以认为,结合method/path/handlers(加入了组内的handlers,即中间件),将这些信息加入到节点中,即完成路由的注册。
可以理解为,每添加一个路由,对应路由的handlers最终都是加入了组内的或者是全局的中间件的handlers,故后面request过来的时候递归调用,即中间件1--->中间件2--->视图函数--->中间件2--->中间件1。

3.engine.Run()

调用流程

源码

// go-gonic/gin/gin.go

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) { // Run方法
	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()) // 最终调用http包的函数起server
	return
}

// net/http/server.go

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr) // 监听服务
	if err != nil {
		return err
	}
	return srv.Serve(ln) //处理连接
}

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error { // 每来一个新请求,起个go程处理请求
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}

	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)

	baseCtx := context.Background()
	if srv.BaseContext != nil {
		baseCtx = srv.BaseContext(origListener)
		if baseCtx == nil {
			panic("BaseContext returned a nil context")
		}
	}

	var tempDelay time.Duration // how long to sleep on accept failure

	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, err := l.Accept()
		if err != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := err.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

从这段源码中可以看到,其实gin的server也是简单封装了标准库中的net/http中的相关函数。

4.gin处理请求

调用流程

将请求流转串连起来的调用流程:

源码

// gin-gonic/gin/gin.go

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context) // 从pool中取出context
	c.writermem.reset(w)              // 重置
	c.Request = req                   // req赋给context的Request
	c.reset()

	engine.handleHTTPRequest(c)       // 处理具体的context,ctx中有request

	engine.pool.Put(c)                // 用完放回去
}

func (engine *Engine) handleHTTPRequest(c *Context) {
	...

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root         // 取出root
		// Find route in tree
		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
		}
		...
	}

	...
}

// gin-gonic/gin/context.go

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) { // 遍历handlers,依次处理ctx
		c.handlers[c.index](c)
		c.index++
	}
}

从源码中可以看到,每次处理请求时,从pool中取出context,重置后遍历handlers调用,处理完请求然后放回context,达到复用context的目的。

借用下简书-LinkinStar

总结

基于gin的学习,在engine/context/pool/tree做以下总结:

  • engine
    web server实例,包含复用器、中间件、配置,作为容器对象,是gin框架的基础。

  • context
    gin将请求request和响应response都封装在context中,通过传递context指针,实现请求的处理,算是golang的特性。

  • pool
    利用pool来重复利用对象,因为请求很多,所以会产生很多数量的context,利用sync.pool进行复用,从而减少内存的分配也提高了效率,值得学习。

  • tree
    gin采用radix tree(字典树的扩展)的数据结构存储、查找相关信息,最早还以为只是使用map结构,后面随着经验增加,觉得map底层用的也是array,总有容量问题,就会有扩容问题,当然有没有可能用其他数据结构,使得其更加高效呢?
    tree代码的结构图:

上述内容基于http的简单应用,https也都差不多,大体流程熟悉了,后面的适当修改即可。

源码解析不易,如果觉得有用欢迎转载。

本文参考文档:

posted on 2022-12-07 15:27  进击的davis  阅读(242)  评论(0编辑  收藏  举报

导航