百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Gin源码分析 - 中间件(2)- 源码分析

bigegpt 2024-08-09 11:09 2 浏览

1 介绍

上一篇文章对中间件的使用方法以及如何开发中间件进行了简要的概述,本文主要对Gin的中间件的相关代码进行分析,从而对中间件有更深入的理解。

2 数据结构

2.1 RouterGroup

// 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
	basePath string
	engine   *Engine
	root     bool
}

RouterGroup管理一个路由分组

(1)HandlersChain,用于管理该分组中注册的中间件,HandlersChain实际上是Handlers的一个切片,定义如下:

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

HandlerFunc实际上是中间件返回的一个处理方法,定义如下:

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

(2)root,如果是真则是全局路由组,该路由实际上是Engine的组成部分,定义如下:

type Engine struct {
	RouterGroup
        ...
}

(3)basePath,路由组的前缀URL;

(4)engine,Enginer指针。

3 注册中间件

3.1 全局注册

从我们最常用的函数gin.Default()开始,它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件,代码如下:

// Default returns an Engine instance with the 
// Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

在上述代码中,Engine的Use方法实现了中间件的全局注册,方法如下:

// Use attaches a global middleware to the router. 
// ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. 
// Even 404, 405, static files...
// For example, this is the right place for a 
// logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

实际上调用了RouterGroup的Use方法,该方法定义如下:

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

首先将注册的多个中间件,添加到group的Handlers中,然后调用returnObj返回 IRoutes接口,该方法如下:

func (group *RouterGroup) returnObj() IRoutes {
	if group.root {
		return group.engine
	}
	return group
}

如果是全局路由组,则返回Engine,否则返回自身。IRoutes接口定义如下:

// IRoutes defines all router handle interface.
type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	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

	StaticFile(string, string) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

RouterGroup实现了所有这些方法,后续将展开分析,Engine中嵌入了RouterGroup类型,因此也实现了IRoutes接口。

3.2 局部注册

通过r.Group方法可以创建了一个路由分组,在创建的过程中可以直接注册本路由分组需要使用的中间件,代码如下:

r := gin.New()
user := r.Group("user", gin.Logger(), gin.Recovery())
{
	user.GET("info", func(context *gin.Context) {

	})
	user.GET("article", func(context *gin.Context) {

	})
}

// Group creates a new router group. 
// You should add all the routes that have common middlewares 
// or the same path prefix.
// For example, all the routes that use a common middleware for 
// authorization could be grouped.
func (group *RouterGroup) Group(
  relativePath string, 
  handlers ...HandlerFunc,
) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

RouterGroup的Group的方法实际上是创建了一个子路由组,并且可以直接进行中间件的组成工作:

(1)首先调用 combineHandlers将待注册中间件和父亲中间件进行合并;

(2)然后计算绝对路由;

(3)设置engine。

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

const abortIndex int8 = math.MaxInt8 / 2    // 127 / 2 = 63

(1)首先计算父亲中的中间件的数量,然后和待注册中间件的数量进行累加,一个路由组中注册的中间件数量不能超过63个;

(2)创建相应的数组,首先添加父路由组中的中间件,然后添加待注册的中间件。

3.3 单路由注册

在注册路由处理方法的时候也可以注册中间件,见下面的例子。

r.GET("/hello", gin.Logger(), gin.Recovery(), func(c *gin.Context) {
})

其实,我们自定义的处理函数,也是中间件,不过是最后一个中间件罢了,具体实现如下:

// 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 = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

首先GET方法内部调用了handle方法,在handle方法内容完成如下的工作:

(1)计算绝对请求路由;

(2)将路由组中已注册的中间件和待注册的中间以及处理函数进行合并;

(3)将合并后的处理方法数组调价的engine的路由树中,此函数在此不再展开,本系列中由专门的章节进行论述。

4 中间件的执行

4.1 上下文

每当有一个请求到达服务端时,Gin就会为这个请求分配一个Context, 该上下文中保存了这个请求对应的处理器链(中间件数组), 以及一个索引index用于记录当前处理到了哪个HandlerFunc,index的初始值为-1。

type Context struct {
        ...
	handlers HandlersChain
	index    int8
        ...
}

4.2 执行流程

在Gin中当接收到一个HTTP请求后,通过ServeHTTP方法完成对一个HTTP请求的处理。

// 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)
}

在上面的代码中,首先创建一个上下文,然后通过handleHTTPRequest处理HTTP请求。

func (engine *Engine) handleHTTPRequest(c *Context) {
	// (1)获取请求方法和请求URL
        httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
        ....
	
        // Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ 

                // (2)获取请求处理器链
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
                        // (3)通过c.Next()开始执行
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
	}

        ...
}

这个方法比较长,在此处主要对主要逻辑进行分析,

(1)获取处理请求的类型,GET,POST等;获取请求URL;

(2)从Engie.trees中查找该请求的处理器,如果找到则将处理器链(中间件数组)复制到上下文的handlers中;

(3)调用c.Next()方法开始中间件的执行,可以看出整个请求的流程主要由c.Next()这个方法控制。

4.3 c.Next方法

// 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)) {
		c.handlers[c.index](c)
		c.index++
	}
}

这个方法很简单,就是循环调用每一个中间件,但是由于每个中间件中还是会调用Next方法,因此理解起来就略有复杂了。下面以一个例子说明这个执行过程。

func indexHandler(c *gin.Context)  {
	fmt.Println("index")
	c.JSON(http.StatusOK, gin.H{
		"msg": "index",
	})
}

func m1(c *gin.Context) {
	fmt.Println("m1 in ...")
	c.Next() //调用后续的处理函数
	fmt.Printf("m1 out ...")
}

func m2(c *gin.Context)  {
	fmt.Println("m2 in ...")
	c.Next()  //调用后续的处理函数
	fmt.Println("m2 out ...")
}

func main()  {
	r := gin.New()
	r.GET("/index", m1, m2, indexHandler)
	r.Run(":8080")
}

运行上面这个例子,然后发起一个index请求,结果如下

m1 in ...
m2 in ...
index
m2 out ...
m1 out ...

析过程如下:

(1)handleHTTPRequest中调用c.Next(),此时index=0,因此获取第一个处理方法m1,并执行;

(2)m1中首先输出"m1 in...";

(3)m1中调用c.Next(),此时index=1,获取第二个处理方法m2,并执行;

(4)m2中首先输出"m2 in...";

(5)m2中调用c.Next(),此时index=2,获取第三个处理方法indexHandler,并执行;

(6)indexHandler完成函数执行,生成响应应答保存到上下文中,然后退出,返回m2中的Next继续执行;

(7)m2中index=3,已经大于等于整个处理器的数量了,从Next()中退出,然后输出"m2 out...",然后退出,返回m1中的Next继续执行;

(8)m1中index=4,已经大于等于整个处理器的数量了,从Next()中退出,然后输出"m1 out...",然后退出,返回handleHTTPReqest中的Next继续执行;

(9)handleHTTPReqest中index=5,已经大于等于整个处理器的数量了,从Next()中退出,然后向客户端发送HTTP响应,完成整个HTTP的执行流程。

4.4 c.Abort方法

当某一个中间件在执行的过程中如果出现异常,不在需要执行下一个处理方法时,可以调用Abort方法。

// Abort prevents pending handlers from being called. 
// Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates 
// that the current request is authorized.
// If the authorization fails (ex: the password does not match), 
// call Abort to ensure the remaining handlers
// for this request are not called.
func (c *Context) Abort() {
	c.index = abortIndex
}

// IsAborted returns true if the current context was aborted.
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

通过上面的分析可以看出整个执行控制是依靠index的数值进行控制的,当index大于当前的处理器的数量时则直接退出,在Gin定义了一个abortIndex,这个值是63,同时这个值也表示了一个处理器中最多能注册的中间件。

// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 // 63

func (group *RouterGroup) combineHandlers(
  handlers HandlersChain,
) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
  //最多的处理器的数量
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

Abort还有如下几个变形,方便开发人员使用。

(1)AbortWithStatus,返回的时候设置HTTP响应码;

(2)AbortWithStatusJSON,返回的时候可以设置HTTP相应码、JSON数据;

(3)AbortWithError,返回的时候可以设置HTTP响应码和异常信息。

// AbortWithStatus calls `Abort()` and writes 
// the headers with the specified status code.
// For example, a failed attempt to authenticate a request 
// could use: context.AbortWithStatus(401).
func (c *Context) AbortWithStatus(code int) {
	c.Status(code)
	c.Writer.WriteHeaderNow()
	c.Abort()
}

// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code 
// and return a JSON body.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
	c.Abort()
	c.JSON(code, jsonObj)
}

// AbortWithError calls `AbortWithStatus()` and `Error()` internally.
// This method stops the chain, writes the status code 
// and pushes the specified error to `c.Errors`.
// See Context.Error() for more details.
func (c *Context) AbortWithError(code int, err error) *Error {
	c.AbortWithStatus(code)
	return c.Error(err)
}

4.5 c.Get方法和c.Set方法

c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的。比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c;然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户 这个过程。

// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes  c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
	c.mu.Lock()
	if c.Keys == nil {
		c.Keys = make(map[string]interface{})
	}

	c.Keys[key] = value
	c.mu.Unlock()
}

// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
	c.mu.RLock()
	value, exists = c.Keys[key]
	c.mu.RUnlock()
	return
}

5 总结

整个中间件的处理流程设计的还是比较的精巧的,后面将分析一些常用的中间件的具体实现。

相关推荐

【Docker 新手入门指南】第十章:Dockerfile

Dockerfile是Docker镜像构建的核心配置文件,通过预定义的指令集实现镜像的自动化构建。以下从核心概念、指令详解、最佳实践三方面展开说明,帮助你系统掌握Dockerfile的使用逻...

Windows下最简单的ESP8266_ROTS_ESP-IDF环境搭建与腾讯云SDK编译

前言其实也没啥可说的,只是我感觉ESP-IDF对新手来说很不友好,很容易踩坑,尤其是对业余DIY爱好者搭建环境非常困难,即使有官方文档,或者网上的其他文档,但是还是很容易踩坑,多研究,记住两点就行了,...

python虚拟环境迁移(python虚拟环境conda)

主机A的虚拟环境向主机B迁移。前提条件:主机A和主机B已经安装了virtualenv1.主机A操作如下虚拟环境目录:venv进入虚拟环境:sourcevenv/bin/active(1)记录虚拟环...

Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能...

基于网络安全的Docker逃逸(docker)

如何判断当前机器是否为Docker容器环境Metasploit中的checkcontainer模块、(判断是否为虚拟机,checkvm模块)搭配学习教程1.检查根目录下是否存在.dockerenv文...

Python编程语言被纳入浙江高考,小学生都开始学了

今年9月份开始的新学期,浙江省三到九年级信息技术课将同步替换新教材。其中,新初二将新增Python编程课程内容。新高一信息技术编程语言由VB替换为Python,大数据、人工智能、程序设计与算法按照教材...

CentOS 7下安装Python 3.10的完整过程

1.安装相应的编译工具yum-ygroupinstall"Developmenttools"yum-yinstallzlib-develbzip2-develope...

如何在Ubuntu 20.04上部署Odoo 14

Odoo是世界上最受欢迎的多合一商务软件。它提供了一系列业务应用程序,包括CRM,网站,电子商务,计费,会计,制造,仓库,项目管理,库存等等,所有这些都无缝集成在一起。Odoo可以通过几种不同的方式进...

Ubuntu 系统安装 PyTorch 全流程指南

当前环境:Ubuntu22.04,显卡为GeForceRTX3080Ti1、下载显卡驱动驱动网站:https://www.nvidia.com/en-us/drivers/根据自己的显卡型号和...

spark+python环境搭建(python 环境搭建)

最近项目需要用到spark大数据相关技术,周末有空spark环境搭起来...目标spark,python运行环境部署在linux服务器个人通过vscode开发通过远程python解释器执行代码准备...

centos7.9安装最新python-3.11.1(centos安装python环境)

centos7.9安装最新python-3.11.1centos7.9默认安装的是python-2.7.5版本,安全扫描时会有很多漏洞,比如:Python命令注入漏洞(CVE-2015-2010...

Linux系统下,五大步骤安装Python

一、下载Python包网上教程大多是通过官方地址进行下载Python的,但由于国内网络环境问题,会导致下载很慢,所以这里建议通过国内镜像进行下载例如:淘宝镜像http://npm.taobao.or...

centos7上安装python3(centos7安装python3.7.2一键脚本)

centos7上默认安装的是python2,要使用python3则需要自行下载源码编译安装。1.安装依赖yum-ygroupinstall"Developmenttools"...

利用本地数据通过微调方式训练 本地DeepSeek-R1 蒸馏模型

网络上相应的教程基本都基于LLaMA-Factory进行,本文章主要顺着相应的教程一步步实现大模型的微调和训练。训练环境:可自行定义,mac、linux或者window之类的均可以,本文以ma...

【法器篇】天啦噜,库崩了没备份(天啦噜是什么意思?)

背景数据库没有做备份,一天突然由于断电或其他原因导致无法启动了,且设置了innodb_force_recovery=6都无法启动,里面的数据怎么才能恢复出来?本例采用解析建表语句+表空间传输的方式进行...