Gin 源码学习(四)丨Gin 对请求的处理流程

在上一篇文章 Gin 源码学习(三)丨路由是如何构建和匹配的? 中,讲解了 Gin 的路由是如何实现的,那么,当路由成功匹配后,或者匹配失败后,在 Gin 内部会对其如何处理呢?json

在这一篇文章中,将讲解 Gin 对一个 HTTP 请求的具体处理流程是怎样的。缓存

下面,将对一个请求进入 Gin 的处理范围后的内容,进行一步步展开,讲解 Gin 对请求的处理流程。安全

Go 版本:1.14cookie

Gin 版本:v1.5.0闭包

目录

  • 请求的处理流程
  • 小结

请求的处理流程

在上一篇文章中,咱们讲到 Gin 其实实现了 Go 自带函数库 net/http 库中的 Handler 接口,而且从实现的源代码中能够发现,当一个 HTTP 请求到达 Gin 处理的范围时,首先是在 Gin 的 Engine 类型中的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法中对 Gin 保存上下文信息的 gin.Context 进行属性设置和重置操做,而后才是使用 engine.handleHTTPRequest(c *Context) 方法来对 HTTP 请求进行处理的,下面,咱们一步一步来对相关源代码进行分析:并发

// ServeHTTP conforms to the http.Handler interface.
// 符合 http.Handler 接口的约定
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// 从对象池中获取已存在的上下文对象
	c := engine.pool.Get().(*Context)
	// 重置该上下文对象的 ResponseWriter 属性
	c.writermem.reset(w)
	// 设置该上下文对象的 Request 属性
	c.Request = req
	// 重置上下文中的其余属性信息
	c.reset()

	// 对请求进行处理
	engine.handleHTTPRequest(c)

	// 将该上下文对象从新放回对象池中
	engine.pool.Put(c)
}

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
// 上下文是 Gin 最重要的部分.
// 它容许咱们在中间件之间传递变量, 管理流程, 例如验证请求的 JSON 并呈现 JSON 响应.
type Context struct {
	// 对 net/http 库中的 ResponseWriter 进行了封装
	writermem responseWriter
	// 请求对象
	Request   *http.Request
	// 非 net/http 库中的 ResponseWriter
	// 而是 Gin 用来构建 HTTP 响应的一个接口
	Writer    ResponseWriter

	// 存放请求中的 URI 参数
	Params   Params
	// 存放该请求的处理函数切片, 包括中间件加最终处理函数
	handlers HandlersChain
	// 用于标记当前执行的处理函数
	index    int8
	// 请求的完整路径
	fullPath string

	// Gin 引擎对象
	engine *Engine

	// Keys is a key/value pair exclusively for the context of each request.
	// 用于上下文之间的变量传递
	Keys map[string]interface{}

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	// 与处理函数/中间件对应的错误列表
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	// 接受格式列表
	Accepted []string

	// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
	// 用于缓存请求的 URL 参数
	queryCache url.Values

	// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	// 用于缓存请求体中的参数
	formCache url.Values
}
复制代码

上面源代码中,展现了 engine.ServeHTTP(w http.ResponseWriter, req *http.Request) 方法的执行过程以及 gin.Context 类型的内部结构,须要注意的是,gin.Context 实现了 Go 的 Context 接口,可是并无对其作并发安全处理,所以,应该避免多个 goroutine 同时访问同一个 Context,若是存在这种状况,需使用 gin.Context.Copy() 方法,对 gin.Context 进行复制使用。ide

而且,Gin 使用对象池来存放上下文信息,这是一个很是巧妙的设计思想,由于在 Gin 中,会将请求的许多处理信息存放于 gin.Context 中,而 Go 是一门带有 GC(垃圾回收)的语言,假如在访问量较大的场景下,若是不使用对象池来缓冲 gin.Context 对象的话,那么为每个请求建立一个 gin.Context 对象,而且在完成请求的处理后,将该 gin.Context 对象交给 GC 去处理,这无疑对 GC 增添了许多压力。因为 gin.Context 只是用于保存当前请求的处理信息,用于上下文之间的参数传递,属于彻底能够复用的对象,所以,使用对象池对其进行存放能够在必定程度上减小 GC 压力。函数

下面先来看一下在 engine.ServeHTTP(w http.ResponseWriter, req *http.Request) 方法中对 gin.Context 初始化时设置了什么样的初始值:post

const (
	// 表示未写入
	noWritten     = -1
	// 200 状态码
	defaultStatus = http.StatusOK
)

type responseWriter struct {
	// net/http 库中的 ResponseWriter
	http.ResponseWriter
	// 响应内容大小
	size   int
	// 响应状态码
	status int
}

func (w *responseWriter) reset(writer http.ResponseWriter) {
	w.ResponseWriter = writer
	w.size = noWritten
	w.status = defaultStatus
}

func (c *Context) reset() {
	c.Writer = &c.writermem
	c.Params = c.Params[0:0]
	c.handlers = nil
	c.index = -1
	c.fullPath = ""
	c.Keys = nil
	c.Errors = c.Errors[0:0]
	c.Accepted = nil
	c.queryCache = nil
	c.formCache = nil
}
复制代码

这里须要留意的是 gin.Context.index 的初始值,Gin 经过该值来调用处理函数和判断当前上下文是否终止。学习

下面,咱们来看 Gin 对请求的处理流程,先来看一下 engine.handleHTTPRequest(c *Context) 方法,该方法在前面的几篇 Gin 源码学习的文章中出现过屡次,因此这里也是一样,只保留其与当前文章主题相关的源代码:

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
		// 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
		}
		// 省略...
		break
	}

	// 省略...
	c.handlers = engine.allNoRoute
	// 处理 404 错误
	serveError(c, http.StatusNotFound, default404Body)
}
复制代码

在上一篇文章中讲到过 Gin 请求路由的匹配是在 root.getValue(path string, po Params, unescape bool) 方法中实现的,而且,当返回的 value 对象中的 handlers 属性不为 nil 时,则表示该请求存在处理函数,而后将 value 对象中的处理函数切片集、请求参数以及请求的完整路径信息存放至该请求的上下文对象中,接着调用 context.Next() 方法,下面来看一下该方法的源代码:

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// Next 只能在中间件内部使用
// 它在调用处理程序内的链中执行挂起的处理程序
// 相似于递归调用或函数装饰器
func (c *Context) Next() {
    // index 初始值为 -1
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}
复制代码

context.Next() 方法的逻辑比较简单,其实就是遍历存放于 Gin 上下文中的中间件/处理函数切片,并调用,在上一篇文章中,咱们也讲过,context.handlers 切片中,在有多个 HandlerFunc 的时候,除了最后一个为该路由的处理函数以外,其他的都为中间件。

这里须要注意的点是,该 Next() 方法,在使用的时候,只能在中间件内部使用,也就是说,在平常开发中,该方法只能在本身编写的中间件中出现,而不能出如今其它地方。

下面,咱们以 gin.Default() 建立 gin.Engine 时添加的两个默认中间件 LoggerRecovery,并结合一个模拟身份验证的中间件 Auth 为例,来对 Gin 中间件的工做流程进行详细讲解,先来看一下 gin.Default() 方法添加的默认中间件 Logger 的相关源代码:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
// Logger 是一个中间件, 该中间件会将日志写入 gin.DefaultWriter.
// 默认的 gin.DefaultWriter 为 os.Stdout, 即标准输出流, 控制台
func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}

// LoggerConfig defines the config for Logger middleware.
// Logger 中间件的相关配置
type LoggerConfig struct {
	// Optional. Default value is gin.defaultLogFormatter
	// 用于输出内容的格式化, 默认为 gin.defaultLogFormatter
	Formatter LogFormatter

	// Output is a writer where logs are written.
	// Optional. Default value is gin.DefaultWriter.
	// 日志输出对象
	Output io.Writer

	// SkipPaths is a url path array which logs are not written.
	// Optional.
	// 忽略日志输出的 URL 切片
	SkipPaths []string
}

// LoggerWithConfig instance a Logger middleware with config.
// 使用 LoggerConfig 配置的 Logger 中间件
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
	formatter := conf.Formatter
	if formatter == nil {
		formatter = defaultLogFormatter
	}

	out := conf.Output
	if out == nil {
		out = DefaultWriter
	}

	notlogged := conf.SkipPaths

	// 是否输出至终端
	isTerm := true

	if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
		(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
		isTerm = false
	}

	// 标记忽略日志的 path
	var skip map[string]struct{}

	if length := len(notlogged); length > 0 {
		skip = make(map[string]struct{}, length)

		for _, path := range notlogged {
			skip[path] = struct{}{}
		}
	}

	return func(c *Context) {
		// Start timer
		// 记录开始时间
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		// 继续执行下一个中间件
		c.Next()

		// Log only when path is not being skipped
		// 若是 path 在 skip 中, 则忽略日志记录
		if _, ok := skip[path]; !ok {
			param := LogFormatterParams{
				Request: c.Request,
				isTerm:  isTerm,
				Keys:    c.Keys,
			}

			// Stop timer
			// 记录结束时间
			param.TimeStamp = time.Now()
			// 计算耗时
			param.Latency = param.TimeStamp.Sub(start)

			// 客户端 IP
			param.ClientIP = c.ClientIP()
			// 请求方法
			param.Method = c.Request.Method
			// 请求状态码
			param.StatusCode = c.Writer.Status()
			// 错误信息
			param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
			// 响应体大小
			param.BodySize = c.Writer.Size()

			if raw != "" {
				path = path + "?" + raw
			}

			param.Path = path

			// 日志打印
			fmt.Fprint(out, formatter(param))
		}
	}
}
复制代码

其实中间件也就是装饰器或闭包,实质上就是一种返回类型为 HandlerFunc 的函数,通俗地讲,就是一种返回函数的函数,目的就是为了在外层函数中,对内层函数进行装饰或处理,而后再将被装饰或处理后的内层函数返回。

因为 HandlerFunc 函数只能接受一个 gin.Context 参数,所以,在上面源代码中的 LoggerWithConfig(conf LoggerConfig) 函数中,使用 LoggerConfig 配置,对 HandlerFunc 进行装饰,并返回。

一样地,在返回的 HandlerFunc 匿名函数中,首先是记录进入该中间件时的一些信息,包括时间,而后再调用 context.Next() 方法,挂起当前的处理程序,递归去调用后续的中间件,当后续全部中间件和处理函数执行完毕时,再回到此处,若是要记录该 path 的日志,则再获取一次当前的时间,与开始记录的时间进行计算,便可得出本次请求处理的耗时,再保存其它信息,包括请求 IP 和响应的相关信息等,最后将该请求的日志进行打印处理,这就是使用 gin.Default() 实例一个 gin.Engine 默认添加的 Logger() 中间件的处理流程。

下面,咱们来看一下,另外一个默认中间件 Recovery() 的相关源代码:

// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// Recovery 中间件用于捕获处理流程中出现 panic 的错误
// 若是链接未断开, 则返回 500 错误响应
func Recovery() HandlerFunc {
	// DefaultErrorWriter = os.Stderr
	return RecoveryWithWriter(DefaultErrorWriter)
}

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
// 使用传递的 out 对 Recovery 中间件进行装饰
func RecoveryWithWriter(out io.Writer) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				// 用于标记链接是否断开
				var brokenPipe bool
				// 从错误信息中判断链接是否断开
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				// 省略, 日志打印相关...

				// If the connection is dead, we can't write a status to it.
				if brokenPipe {	// 若是链接已断开, 则已经没法为其写入状态码
					// 添加错误信息至上下文中, 用于日志输出
					c.Error(err.(error)) // nolint: errcheck
					// 终止该上下文
					c.Abort()
				} else {	// 链接未断开
					// 终止该上下文并写入 500 错误状态码
					c.AbortWithStatus(http.StatusInternalServerError)
				}
			}
		}()
		// 继续执行下一个中间件
		c.Next()
	}
}
复制代码

LoggerWithConfig(conf LoggerConfig) 函数同样,RecoveryWithWriter(out io.Writer) 函数仅为了对最终返回的中间件 HandlerFunc 函数进行装饰,在该中间件中,可分为两个逻辑块,一个是 defer,一个是 Next()Next()Logger() 中间件中的 Next() 做用相似,这里在 defer 中使用 recover() 来捕获在后续中间件中 panic 的错误信息,并对该错误信息进行处理。

在该中间件中,首先是判断当前链接是否已中断,而后是进行相关的日志处理,最后,若是链接已中断,则直接设置错误信息,并终止该上下文,不然,终止该上下文并返回 500 错误响应。

下面,咱们来看一下 context.Abort() 方法和 context.AbortWithStatus(code int) 方法的相关源代码:

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

// 终止上下文
func (c *Context) Abort() {
	c.index = abortIndex
}

// 判断上下文是否终止
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

// 终止上下文并将 code 写入响应头中
func (c *Context) AbortWithStatus(code int) {
	c.Status(code)
	c.Writer.WriteHeaderNow()
	c.Abort()
}
复制代码

context.Abort() 方法将当前上下文的 index 值设置为 63,用于标志上下文的终止。

context.AbortWithStatus(code int) 也是终止当前的上下文,只不过额外的使用了 code 参数,对响应的头信息进行了设置。

最后,咱们再来看一个模拟身份校验的中间件 Auth,其实现的相关源代码以下:

type RequestData struct{
	Action string `json:"action"`
	UserID int `json:"user_id"`
}

func main() {
	router := gin.Default()
	// 注册中间件
	router.Use(Auth())

	router.POST("/action", func(c *gin.Context) {
		var RequestData RequestData
		if err := c.BindJSON(&RequestData); err == nil {
			c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success"})
		}
	})

	router.Run(":8000")
}

func Auth() gin.HandlerFunc {
	// TODO: 可模仿 Logger() 或 Recovery() 中间件, 结合该函数的调用参数, 在此处作一些配置操做
	return func(c *gin.Context) {
		// TODO: 可模仿 Logger() 中间件, 在此处对请求的 path 进行忽略处理
		if auth(c.Request) {
			c.Next()
		} else {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"})
		}
	}
}

func auth(req *http.Request) bool {
	// TODO: 对 http.Request 中的信息进行校验, 如 cookie, token...
	return req.Header.Get("Auth") == "colelie"
}
复制代码

首先是 Auth() 函数,该函数用于装饰并返回 gin.HandlerFunc 函数,在该函数内,返回了一个 gin.HandlerFunc 匿名函数,在该匿名函数中,经过调用 auth(req *http.Request) 函数对请求信息进行校验,这里只是一个简单地对请求头中的 Auth 进行验证。

因此,在该案例中,当咱们访问 /action 接口时,首先会进入 Logger() 中间件,而后进入 Recovery() 中间件,再进入 Auth() 中间件,当前面的中间件都没有发生对上下文的终止操做时,才会进入咱们声明的 router.POST("/action", func) 处理函数。

当咱们向 /action 接口发起一个普通的 POST 请求时,会收到以下响应:

这是因为在 Auth() 中间件中身份校验没经过,咱们为该请求的头部信息中添加一个 Key 为 Auth,Value 为 colelie 的字段,会收到以下响应:

能够发现,一样的,出现了错误响应,而此次的错误响应码为 400,这是为何呢?

Gin 源码学习(二)丨请求体中的参数是如何解析的? 中,咱们讲过,在使用 MustBind 一类的绑定函数时,若是在参数解析过程当中出现错误,会调用 c.AbortWithError(http.StatusBadRequest, err) 方法,终止当前的上下文并返回 400 响应错误码,在上面的声明的对 /action 的处理函数中,使用了 context.BindJSON(obj interface{}) 方法对请求参数进行绑定操做,下面,咱们在为请求添加可以绑定成功的请求体,会收到以下响应:

此次,获得了正确的响应内容。

小结

在这篇文章中,咱们围绕 gin.Context 的内部结构、Gin 中间件和处理函数的工做流程,讲解了 Gin 对请求的处理流程。

首先,在 gin.Engine 中,使用对象池 sync.Pool 来存放 gin.Context 这样作的目的是为 Go GC 减小压力。

而后,在 Gin 内部,当路由匹配成功后,将调用 context.Next() 方法,开始进入 Gin 中间件和处理函数的执行操做,而且,须要注意的是,在平常开发中,该方法,只能在中间件中被调用。

最后,以使用 gin.Default() 方法建立 gin.Engine 时携带的两个默认中间件 Logger()Recovery(),和咱们本身编写的一个模拟身份校验的中间件 Auth(),结合注册的 path 为 /action 的路由,对 Gin 中间件和处理函数的工做流程进行了讲解。

至此,Gin 源码学习的第四篇也就到此结束了,感谢你们对本文的阅读~~

欢迎扫描如下二维码关注笔者的我的订阅号,获取最新文章推送:

相关文章
相关标签/搜索