go 进阶 gin底层原理相关: 一. gin框架基础与初始化启动原理

目录

  • 一. 基础解释
  • 二. 初始化容器
    • Engine
    • RouterGroup
    • 总结 Engine 与 RouterGroup 关系,引出(IRouter)
      • IRouter
    • HandlerFunc
    • Context
    • Engine.Run() 启动服务
  • 三. 总结
    • 初始化总结
    • 启动总结

一. 基础解释

  1. 什么是gin: 一个用 Go 编写的HTTP Web 框架,内部重点是基于httprouter采用类似字典树一样的数据结构来存储路由与handle方法的映射,实现了路由功能
  2. 了解gin框架前,gin基于httprouter开发,httprouter又是为了解决net/http不足出现的,可以先了解一下go标准库中的"net/http"包,在提供httpServer服务时内部又可以细分为以下几个部分下,结合这几个部分去看gin的源码:
  1. 原生httpServer服务启动监听原理
  2. 原生httpServer中的多路复用器及路由注册原理
  3. 原生httpServer接收请求后的路由发现原理
  1. 接着说一下httprouter,内部比较重要的分三部分,Router,node,在Router内部有一个trees属性,启动服务时会将路由与对应的path路径存储到trees树中,Router实现了ServeHTTP(),后续在接收到请求后会执行ServeHTTP()方法,在该方法内部会调用getValue()方法获取对应的Handle执行业务逻辑
  2. gin框架也可以在一下几个角度进行分析
  1. 服务启动前的始化容器创建Engine引擎
  2. 中间件的注册(中间件又分为全局中间件,路由组中间件)
  3. 路由的注册
  4. 服务启动
  5. 接收请求的处理
  1. 注意点: gin的高性能主要依靠trees,每一个节点的内容你可以想象成一个key->value的字典树,key是路由,而value则是一个[]HandlerFunc,里面存储的就是按顺序执行的中间件和handle控制器方法
  2. 编写一个简单gin服务代码示例
//注意go服务的启动类package要编写为main
package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	//1.创建router,实际返回的是一个Engine引擎,也可以说成初始化容器创建Engine引擎
	router := gin.Default()
	//router:=gin.New() 了解Default与New的区别

	//2.注册全局中间件
	router.Use(MiddlewareFunc())

	//3.注册路由
	router.GET("/test", func(context *gin.Context) {
		context.Writer.Write([]byte("返回参数"))
	})

	//4.注册路由组,Group()方法会返回一个新生成的RouterGroup指针,用来区分不同的路由组与执行对应不同的中间件等
	groupRouter := router.Group("/groupPath")

	//5.路由组注册中间件
	groupRouter.Use(MiddlewareFunc())
	{
		//6.路由组注册路由
		groupRouter.GET("/hello", func(context *gin.Context) {
			context.Writer.Write([]byte("返回参数"))
		})
	}
 
 	//7.添加资源路径
	router.Static("/static", "dist/static")
	//前端接口
	//这样只要启动后端代码,访问根目录就直接访问到静态资源了
	router.StaticFile("/", "dist/index.html")
	
	//8.监听端口启动服务
	router.Run(":8080")
}

//中间件函数
func MiddlewareFunc() gin.HandlerFunc {
	return func(c *gin.Context) {
		log.Printf("[INFO] %s", "中间件业务执行")
		c.Next()
	}
}
  1. 查看上方代码可以简单解释为:
  1. 通过调用gin.New() 或Default方法来创建router初始化容器,实际返回的是一个Engine引擎,也可以说成初始化容器创建Engine引擎
  2. 调用Use()方法注册中间件
  3. 通过Group()方法返回一个新生成的RouterGroup指针,实现路由分组,并且在执行完Group()后,会复制全局中间件到新生成的RouterGroup.Handlers中,接下来路由注册的时候就可以一起写入树节点中
  4. 调用GET或POS方法注册路由
  5. 调用Run方法启动服务

二. 初始化容器

Engine

  1. 我们先看一下Engine结构,Engine 对象是 gin 的框架实例
type Engine struct {
	//管理路由和中间件的组件,它定义了 URL 路径与处理函数的映射关系。
    RouterGroup
    //如果当前路径的处理函数不存在,但是路径+'/'的处理函数存在,则允许进行重定向,默认为 true
    RedirectTrailingSlash bool
    //允许修复当前请求路径,如/FOO和/..//Foo会被修复为/foo,并进行重定向,默认为 false。
    RedirectFixedPath bool
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool
    AppEngine bool
    //使用未转义的请求路径(url.RawPath),默认为 false
    UseRawPath bool
	//对请求路径值进行转义(url.Path),默认为 true
    UnescapePathValues bool
    MaxMultipartMemory int64
	//去除额外的反斜杠,默认为 false
    RemoveExtraSlash bool

    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    pool             sync.Pool
    //每一个 HTTP 方法会有一颗方法树,方法树记录了路径和路径上的处理函数
    trees            methodTrees
}
  1. 在我们编写gin服务时,会调用gin.New() 或Default方法来创建router初始化容器,实际返回的是一个Engine,Engine 对象是 gin 的框架实例,它其中包括了路由定义以及一些配置相关的参数,查看new方法,负责创建Context对象,采用sync.Pool减少频繁context实例化带来的资源消耗,重点分为以下几个步骤
  1. 创建一个默认的RouterGroup,RouterGroup中Handlers存放的是当前全局中间件, basePath 就是存放这个分组的基础路由路径"/"
  2. 通过make()函数初始化trees属性,tress负责采用类似字典树的结构存储路由和handle方法的映射
  3. 通过sync/pool 实现context池,减少频繁context实例化带来的资源消耗
func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		//实例化默认的RouterGroup,其中Handlers为中间件数组
		RouterGroup: RouterGroup{ 
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		AppEngine:              defaultAppEngine,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		//trees 是最重要的点!!!!负责存储路由和handle方法的映射,采用类似字典树的结构
		trees:                  make(methodTrees, 0, 9), 
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJsonPrefix:       "while(1);",
	}
	engine.RouterGroup.engine = engine
	//这里采用 sync/pool 实现context池,减少频繁context实例化带来的资源消耗
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

RouterGroup

  1. 先看一下结构
type RouterGroup struct {
	//用来保存当前路由组的函数链
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}
  1. 在我们通过gin.New()初始化容器时,会创建一个Engine 实例, 在该实例中存在一个RouterGroup 属性,或者我们通过拿到的Engine 实例调用Group()方法会返回一个新的RouterGroup
  2. RouterGroup是用来配置 HTTP 路由的,它关联了一个路径前缀和其对应的处理函数,同时 RouterGroup 也包含了关联它的 Engine 对象,当调用 RouterGroup 的路由定义方法时会在 Engine 的路由树上创建路径与其处理函数

总结 Engine 与 RouterGroup 关系,引出(IRouter)

  1. 在代码逻辑层面 Engine 与 RouterGroup 关系
  1. 我们执行gin.New/gin.Default函数初始化容器获取到Engine实例,Engine中存在一个RouterGroup属性,在初始化时会创建一个默认的RouterGroup,这个默认的RouterGropu中的Handlers存放的是当前全局中间件, basePath 就是存放这个分组的基础路由路径"/"(参考gin.New源码)
  2. 在Engine 实例调用Group()方法会返回一个新的RouterGroup,并且在执行完Group()后,会复制全局中间件到新生成的RouterGroup.Handlers中,接下来路由注册的时候就可以一起写入树节点中
  1. 在结构继承关系层面
  1. Engine 中包含一个RouterGroup属性,该属性中保存的是初始化容器时提供的默认路由组
  2. RouterGroup 中也持有着Engine属性指针,能给更方便的获取上下文相关数据
  3. Engine 与 RouterGroup 都实现了IRouter 接口,也可以将Engine 看为RouterGroup
var _ IRouter = &RouterGroup{}
var _ IRouter = &Engine{}

IRouter

type IRouter interface {
	IRoutes
	//创建一个新的RouterGroup
	Group(string, ...HandlerFunc) *RouterGroup
}

type IRoutes interface {
	//添加中间件/处理函数即用append方法向HandlerFunc切片添加一个HandlerFunc方法
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	//对所有已知类型调用一遍handle,接收该路由所有类型请求
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes
	//把本地filepath路径下的单个静态文件当作服务提供给relativePath路由
	//包装了go的http.ServeFile函数作为处理器,注册了GET和HEAD路由
	StaticFile(string, string) IRoutes
	StaticFileFS(string, string, http.FileSystem) IRoutes
	//root路径下的文件当作静态目录作为路由提供服务,不能使用通配符
	//计算绝对路径
	//调用go的http.StripPrefix函数,实现把静态目录映射为路由
	//封装http.FileSystem.Open(),Close(),httpServeHTTP()等函数作为处理器
	//加上/*filepath后缀(用于处理器中当作路径取文件),注册GET,HEAD类型的路由
	Static(string, string) IRoutes
	//和Static一样,Static方法实际上是把root字符串通过go的http.Dir()函数
	//包装成http.FileSystem对象直接调用StaticFS实现的
	StaticFS(string, http.FileSystem) IRoutes
}

HandlerFunc

  1. HandlerFunc 是路由的处理函数,它在 gin 中的定义如下,是一个接收 *Context 作为参数的函数,HandlersChain 是处理函数的调用链,通常包括了路由上定义的中间件以及最终处理函数
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
 
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
 
// Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
    if length := len(c); length > 0 {
        return c[length-1]
    }
    return nil
}

Context

  1. Context 是处理函数调用链传递的对象,将Request的处理封装到Context中,内部包括了 HTTP 的请求对象,请求参数,和构造 HTTP 响应的对象,它允许使用者在调用链中传递自定义变量,并在调用链的其它地方通过 Context 对象把它取出来
// Context是gin最重要的部分。它允许我们在中间件之间传递变量,
// 管理流程,验证请求的JSON并渲染JSON响应等。
type Context struct {
	//响应处理(实现了http.ResponseWriter 和 gin.ResponseWriter)
	writermem responseWriter
	// Request是一个指向http.Request类型的指针,表示当前的HTTP请求
	Request   *http.Request 
	// Writer是一个ResponseWriter类型的接口,表示当前的HTTP响应
	Writer    ResponseWriter 
	// Params是一个Params类型的切片,表示URL中的参数
	Params   Params 
	// handlers是一个HandlersChain类型的切片,表示注册的中间件和处理函数
	handlers HandlersChain 
	// 表示当前执行到第几个中间件或处理函数
	index    int8 
	// 表示当前请求的完整路径
	fullPath string 
	// 个指向Engine类型的指针,表示当前使用的Gin引擎
	engine   *Engine 
	// Params类型的指针,表示URL中的参数(与Params字段相同)
	params   *Params 
	// 指向skippedNode类型切片的指针,表示跳过的路由节点
	skippedNodes *[]skippedNode 
	// sync.RWMutex类型的变量,用于保护Keys字段
	mu sync.RWMutex 
	//键值对映射表,专门用于每个请求的上下文。
	Keys map[string]interface{}

	// 错误列表,附加到所有使用这个上下文的处理器/中间件
	Errors errorMsgs

	// 定义了一个手动接受的格式列表,用于内容协商
	Accepted []string

	// queryCache是一个url.Values类型的变量,缓存了c.Request.URL.Query()的结果
	queryCache url.Values 

	//一个url.Values类型的变量,缓存了c.Request.PostForm,它包含了从POST, PATCH或PUT请求体中解析出来的表单数据
	formCache url.Values 

	// 一个http.SameSite类型的变量,允许服务器定义一个cookie属性,使得浏览器不能在跨站请求中发送这个cookie
	sameSite http.SameSite 
}
  1. 封装Context的优点:
  1. 在Context中维护了一个调用链HandlersChain,可以链式执行Handler
  2. 提供了一个Keys属性,可以理解为一个上下文级别的缓存有对应的get,set方法
  3. 提供了一个Params属性,可以获取路由(路径)变量
  4. 提供了一个Query属性,可以获取url传参
  5. 提供了大量的Bind方法,基于提供的Bind方法可以很方便的解析获取例如json, xml, protobuf, form, query, yaml各种个样的数据格式, 提高我们的开发速度
  1. 注意: 在调用链中的某一个handler中调用了c.Abort之类的函数,调用链会直接退出也是通过c.index来控制的
//Abort源码,意思是将index移动末尾,所以原来后面的就不再执行
func (c *Context) Abort() {
	c.index = abortIndex
}

Engine.Run() 启动服务

  1. 我们通过Engine.Run()启动服务,查看该方法, 调用 resolveAddress 解析传入的地址,若没有传入地址,则默认使用 PORT 环境变量作为端口号,在此端口上运行 HTTP 服务,接着调用 net/http.ListenAndServe ,把自身作为一个 http.Handler,监听和处理 HTTP 请求
  1. Engine 实现了 net/http.Handler 下的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法,本身是一个Handler
  2. 在当前这个Run()方法中会调用 net/http下的ListenAndServe()函数,这里就来到了net/http标准库,可以参考前面做的笔记说一下net/http
    HttpServer 多路复用器及路由注册原理
    HttpServer 服务启动到Accept等待接收连接
    HttpServer 服务启动接到连接后的处理逻辑
    多路复用 netpoller 初始化
    多路复用 Accept/Read/Write
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}
  1. Engine 实现了 net/http.Handler 下的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法,接收到 HTTP 请求后会统一走到 ServeHTTP 方法中,首先 Engine 会从 context 对象池中取出一个 *Context,使用对象池管理 Context 可以尽量减少频繁创建对象带来的 GC,拿出一个 *Context 之后,它就会作为请求的上下文,传递到请求方法里
// 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)
}
  1. 在 Engine 的 handleHTTPRequest 方法中,进行了下面几个步骤
  1. 根据配置参数决定是否使用编码后的 URL 路径,以及去除多余的反斜杠。
  2. 根据 HTTP 请求方法找到对应的方法树,若找到对应的方法树,从方法树中获取路由信息,并把处理函数,参数路径信息记录到 Context 上,调用 Context 的 Next 方法开始执行调用链上的函数。若方法树中不存在路由信息,则判断路径+'/'的路由定义是否存在,并尝试进行重定向。
  3. 如果没有找到对应路由信息,根据配置参数返回 HTTP 404 (NOT FOUND) 或 405 (METHOD NOT ALLOWED) 错误
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
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // 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
        // Find route in tree
        value := root.getValue(rPath, c.Params, unescape)
        if value.handlers != nil {
            c.handlers = value.handlers
            c.Params = value.params
            c.fullPath = value.fullPath
            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
    }

    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
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

三. 总结

初始化总结

  1. gin一个用 Go 编写的HTTP Web 框架,基于httprouter开发,httprouter又是为了解决net/http不足出现的,这三个可能要一块说一下
  2. gin框架也可以在一下几个角度进行分析
  1. 服务启动前的始化容器创建Engine引擎
  2. 中间件的注册(中间件又分为全局中间件,路由组中间件)
  3. 路由的注册
  4. 服务启动
  5. 接收请求的处理
  1. 我们这里先说一下gin初始化启动, 可以分为Engine实例的初始化创建与执行Engine的Run()方法启动服务两个部分去理解
  2. 在编写gin服务时,会调用gin.New() 或Default函数来创建一个Engine实例,以new()函数为例,函数内部创建Engine结构体变量,并初始化Engine内部的属性
  1. 实例化默认的RouterGroup,RouterGroup内部有一个Handlers属性,是一个用来存储中间件的数组,
  2. 调用make函数初始化trees属性,这个属性内部采用类似字典树的结构存储了路由和handle方法的映射
  3. 调用allocateContext()方法,采用 sync/pool 的方式初始化创建gin.Context
  4. 最终Engine创建完成并返回
  1. Engine 内部的RouterGroup属性,在初始化时会创建一个默认的RouterGroup,这个默认的RouterGropu中的Handlers存放的是当前全局中间件, basePath 就是存放这个分组的基础路由路径"/",后续Engine实例在调用Group()进行分组时,会返回一下新的RouterGroup
  2. gin.Context上下文,运行的核心结构,内部包括了表示当前的HTTP请求的Request,表示当前的HTTP响应的Writer,用来缓存数据的Keys属性

启动总结

  1. 了解gin服务启动前,先了解一下Engine的继承结构,Engine 实现了 net/http.Handler 下的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法,实现了IRouter,IRoutes等
  2. 调用Engine的Run()方法启动服务,查看该方法内部
  1. 调用 resolveAddress 解析传入的地址,若没有传入地址,则默认使用 PORT 环境变量作为端口号,在此端口上运行 HTTP 服务
  2. Engine 实现了 net/http.Handler 下的 ServeHTTP()本身是一个Handler,
  3. 在当前这个Run()方法中会调用 net/http下的ListenAndServe()函数,这里就来到了net/http启动服务
  4. 继续了解net/http下的ListenAndServe(),内部会调用

4.1: " net.Listen(“tcp”, addr)“: 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
4.2: “srv.Serve(ln)””: 等待接收客户端连接Accept(),与接收到连接后的处理流程

  1. net/http.ListenAndServe()内部net.Listen(“tcp”, addr) 多路复用初始化:
  1. 执行"DefaultResolver.resolveAddrList"根据协议名称和地址取得 Internet 协议族地址列表,
  2. 根据协议执行listenTCP或listenUnix 进行实际socket创建,fd创建,监听等操作, 具体根据传入的协议族来确定
  3. 会初始化多路复用epoll相关业务,以TCP为例,通过listenTCP()函数最终会调用到net/sock_posix.go下的一个socket()
  4. 最终会调用newFD(),FD.Init(),pollDesc.init(),封装一个epoll文件描述符实例epollevent
  5. 执行poll_runtime_pollOpen: 调用alloc()初始化总大小约为 4KB的pollDesc结构体,调用netpollopen(),将可读,可写,对端断开,边缘触发 的监听事件注册到epollevent中
  6. 具体参考前面做的笔记netpoller 初始化
  1. net/http.ListenAndServe()内部srv.Serve(ln)监听端口等待客户端请求方法内可以重点简化为
  1. 方法内通过for开启了一个死循环
  2. 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
  3. 当接收到连接请求Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
  4. 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
  5. 当接收到一个请求后for循环继续执行等待下一个连接

你可能感兴趣的:(#,十二.,gin,底层原理与基本使用,golang,gin,中间件)