Gin
是使用Go语言编写的高性能的web
服务框架,根据官方的测试,性能是httprouter
的40倍左右。要使用好这套框架呢,首先我们就得对这个框架的基本结构有所了解,所以我将从以下几个方面来对Gin
的源码进行解读。
- 第一章:
Gin
是如何储存和映射URL
路径到相应的处理函数的 - 第二章:
Gin
中间件的设计思想及其实现 - 第三章:
Gin
是如何解析客户端发送请求中的参数的 - 第四章:
Gin
是如何将各类格式(JSON/XML/YAML
等)数据解析返回的
Gin是如何解析客户端发送请求中的参数的
事实上,Gin
也是基于http
包封装来实现的网络通信,底层仍旧使用的是http.ListenAndServe
来创建的监听端口和服务,只不过将接收到的数据解析为Gin
的Context
上下文后,最终再传递到type HandlerFunc func(*Context)
处理函数中去的。
再了解一个大致的数据处理过程之后,我们就从Gin
的监听入口开始逐渐摸索。
建立监听服务
if err := router.Run();err != nil {
log.Println("something error");
}
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
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{
Addr: addr, Handler: handler}
return server.ListenAndServe()
}
通过上面这个过程可以了解到Gin
和http
通信框架建立联系是通过engine *Engine
实现的,同时ListenAndServe
要求传入的是一个Handler
类型的对象,而该对象定义如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
这咋一看,瞬间就明白了许多,ResponseWriter, *Request
这两个参数一目了然——请求与响应流
,http
包就是底层处理过后将这两个数据通过该接口传递到Gin
框架内部的,所以我们找到该接口的实现。
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)
}
服务处理
在正式开始了解这个处理过程之前,我们先来了解一下Context
这个贯穿整个Gin
框架的上下文对象,在C/S通信过程中所有的数据都保存在这个对象中了。
type Context struct {
//响应输出流(私有,供框架内部数据写出)
writermem responseWriter
//客户端发送的所有信息都保存在这个对象里面
Request *http.Request
//响应输出流(公有,供给处理函数写出)
// 在初始化后,由writermem克隆而来的
Writer ResponseWriter
//保存解析得到的参数,路径中的REST参数
Params Params
//该请求对应的处理函数链,从树节点中获取
handlers HandlersChain
//记录已经被处理的函数个数
index int8
//当前请求的完整路径
fullPath string
//Gin的核心引擎
engine *Engine
//并发读写锁
KeysMutex *sync.RWMutex
//用于保存当前会话的键值对,用于不同处理函数中传递
Keys map[string]interface{
}
//处理函数链输出的错误信息
Errors errorMsgs
//客户端希望接受的数据类型,如:json、xml、html
Accepted []string
//存储URL中的查询参数,如:/test?name=jhon&age=11
// 这样的参数储存在这个对象里
queryCache url.Values
//这个用于存储POST/PATCH等提交的body中的参数
formCache url.Values
//用来限制第三方 Cookie,一个int值,有Strict、Lax、None
// Strict:只有当前网页的 URL 与请求目标一致,才会带上 Cookie
// Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,
// 但是导航到目标网址的 Get 请求除外
// 设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击
sameSite http.SameSite
}
在了解完Context
后,我们来进入正式的数据解析过程:
func (engine *Engine) handleHTTPRequest(c *Context) {
//获取客户端的http请求方法
httpMethod := c.Request.Method
//获取请求的URL地址,这里的URL是进过处理的
rPath := c.Request.URL.Path
//是否不启动字符转义
unescape := false
//判断是否启用原URL,未转义字符
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
//判断是否需要移除多余的分隔符"/"
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
//首先获取到指定HTTP方法的搜索树的根节点
root := t[i].root
//从根节点开始搜索匹配该路径的节点
value := root.getValue(rPath, c.Params, unescape)
//将节点中的存储的信息,拷贝到Context上下文中
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
//这里就是在遍历执行处理函数链
// func (c *Context) Next() {
// c.index++
// for c.index < int8(len(c.handlers)) {
// c.handlers[c.index](c)
// c.index++
// }
// }
c.Next()
//写出响应状态码
c.writermem.WriteHeaderNow()
return
}
//如果没有找到对应的匹配节点,则考虑是否是以下的特殊情况
if httpMethod != "CONNECT" && rPath != "/" {
//如果启动自动重定向,删除最后的"/"并重定向
if value.tsr