内容简介:在上一篇文章在这一篇文章中,将讲解 Gin 对一个 HTTP 请求的具体处理流程是怎样的。下面,将对一个请求进入 Gin 的处理范围后的内容,进行一步步展开,讲解 Gin 对请求的处理流程。
在上一篇文章 Gin 源码学习(三)丨路由是如何构建和匹配的? 中,讲解了 Gin 的路由是如何实现的,那么,当路由成功匹配后,或者匹配失败后,在 Gin 内部会对其如何处理呢?
在这一篇文章中,将讲解 Gin 对一个 HTTP 请求的具体处理流程是怎样的。
下面,将对一个请求进入 Gin 的处理范围后的内容,进行一步步展开,讲解 Gin 对请求的处理流程。
Go 版本:1.14
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
进行复制使用。
并且,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
初始化时设置了什么样的初始值:
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
时添加的两个默认中间件 Logger
和 Recovery
,并结合一个模拟身份验证的中间件 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 源码学习的第四篇也就到此结束了,感谢大家对本文的阅读~~
欢迎扫描以下二维码关注笔者的个人订阅号,获取最新文章推送:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- flask 源码解析5:请求
- Retrofit网络请求源码解析
- Okhttp同步请求源码分析
- Volley 源码解析之网络请求
- Nginx源码阅读笔记-处理HTTP请求
- Nginx源码阅读笔记-接收HTTP请求流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。