Golang之Gin框架源码解读——第三章

Gin是使用Go语言编写的高性能的web服务框架,根据官方的测试,性能是httprouter的40倍左右。要使用好这套框架呢,首先我们就得对这个框架的基本结构有所了解,所以我将从以下几个方面来对Gin的源码进行解读。

  • 第一章:Gin是如何储存和映射URL路径到相应的处理函数的
  • 第二章:Gin中间件的设计思想及其实现
  • 第三章:Gin是如何解析客户端发送请求中的参数的
  • 第四章:Gin是如何将各类格式(JSON/XML/YAML等)数据解析返回的

Gin Github官方地址

Gin是如何解析客户端发送请求中的参数的

事实上,Gin也是基于http包封装来实现的网络通信,底层仍旧使用的是http.ListenAndServe来创建的监听端口和服务,只不过将接收到的数据解析为GinContext上下文后,最终再传递到type HandlerFunc func(*Context)处理函数中去的。

再了解一个大致的数据处理过程之后,我们就从Gin的监听入口开始逐渐摸索。

建立监听服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()
}

通过上面这个过程可以了解到Ginhttp通信框架建立联系是通过engine *Engine实现的,同时ListenAndServe要求传入的是一个Handler类型的对象,而该对象定义如下:

1
2
3
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这咋一看,瞬间就明白了许多,ResponseWriter, *Request这两个参数一目了然——请求与响应流http包就是底层处理过后将这两个数据通过该接口传递到Gin框架内部的,所以我们找到该接口的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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通信过程中所有的数据都保存在这个对象中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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后,我们来进入正式的数据解析过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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 && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            //启动路径修复后,当/../foo找不到匹配路由时,
            // 会自动删除..部分路由,然后重新匹配直到找到匹配路由,并重定向
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }
    //是HTTP方法不匹配,而路径匹配则返回405
    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
            }
        }
    }
    //如果都找不到路由则返回404
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

上述代码就是整个请求的处理过程,而节点查找和参数解析都在getValue函数之中,我们来看一下他是如何匹配路径和参数解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
    //先保存原有的REST参数列表
    value.params = po
walk: //这个标号使用中递归的,这里使用的是循环式的递归
    for {
        // 当前节点的路径
        prefix := n.path
        //如果该路径与当前节点路径刚好匹配
        if path == prefix {
            //如果处理函数是一样的
            // 则说明已经搜索过了更新路径后跳出。
            if value.handlers = n.handlers; value.handlers != nil {
                value.fullPath = n.fullPath
                return
            }
           
            //这种情况直接推荐重定向
            if path == "/" && n.wildChild && n.nType != root {
                //这个表示重定向后可以找到满足条件的节点
                value.tsr = true
                return
            }

            //如果以上条件都未匹配,则根据索引去搜索子节点
            indices := n.indices
            for i, max := 0, len(indices); i < max; i++ {
                if indices[i] == '/' {
                    n = n.children[i]
                    value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
                        (n.nType == catchAll && n.children[0].handlers != nil)
                    return
                }
            }

            return
        }
       
        //这里这种情况说明的是path的前缀刚好和该节点吻合
        //所以进入子节点搜索
        if len(path) > len(prefix) && path[:len(prefix)] == prefix {
            path = path[len(prefix):]
            //如果该节点没有通配符子节点,则根据索引查找子节点
            if !n.wildChild {
                c := path[0]
                indices := n.indices
                for i, max := 0, len(indices); i < max; i++ {
                    if c == indices[i] {
                        n = n.children[i]
                        continue walk
                    }
                }

                //如果没找到匹配的子节点,则建议重定向搜索
                value.tsr = path == "/" && n.handlers != nil
                return
            }

            //下面是子节点是统配符节点的情况
            // 需要根据传入的URL对路径中的参数进行解析
            // 因为如果n.wildChild为true的话,那么n就只能有一个子节点
            n = n.children[0]
            switch n.nType {
            //子节点为参数节点
            case param:
                //寻找参数的字符长度
                end := 0
                for end < len(path) && path[end] != '/' {
                    end++
                }

                //根据maxParams来预分配更大的参数列表(仅仅是容量)
                if cap(value.params) < int(n.maxParams) {
                    value.params = make(Params, 0, n.maxParams)
                }
                i := len(value.params)
                //拓展参数列表长度
                value.params = value.params[:i+1]
                //获取参数名从1开始是因为一般都是*:开头的
                value.params[i].Key = n.path[1:]
                // 获取参数值
                val := path[:end]
                //如果需要转义则调用转义函数
                if unescape {
                    var err error
                    if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
                        value.params[i].Value = val // fallback, in case of error
                    }
                } else {
                    value.params[i].Value = val
                }

                //如果path还没解析完
                if end < len(path) {
                    // 进入其子节点
                    if len(n.children) > 0 {
                        path = path[end:]
                        n = n.children[0]
                        continue walk
                    }

                    // 若仅仅是多了个"/",则推荐重定向
                    value.tsr = len(path) == end+1
                    return
                }

                if value.handlers = n.handlers; value.handlers != nil {
                    value.fullPath = n.fullPath
                    return
                }
                if len(n.children) == 1 {
                    //如果子节点有匹配"/"的,则推荐重定向
                    n = n.children[0]
                    value.tsr = n.path == "/" && n.handlers != nil
                }
                return
            //这个类型表明所有的参数都已经匹配完了
            case catchAll:
                //下面的过程和上面差不多
                if cap(value.params) < int(n.maxParams) {
                    value.params = make(Params, 0, n.maxParams)
                }
                i := len(value.params)
                value.params = value.params[:i+1] // expand slice within preallocated capacity
                value.params[i].Key = n.path[2:]
                if unescape {
                    var err error
                    if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
                        value.params[i].Value = path // fallback, in case of error
                    }
                } else {
                    value.params[i].Value = path
                }
                //获取节点中保存的处理函数链
                value.handlers = n.handlers
                //获取该节点下的完整路径
                value.fullPath = n.fullPath
                return

            default:
                panic("invalid node type")
            }
        }

        // 说明该节点是个,则只有推荐重定向了
        value.tsr = (path == "/") ||
            (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
                path == prefix[:len(prefix)-1] && n.handlers != nil)
        return
    }
}

其实从上面都能看出,这个过程就是从搜索树的根节点依次向下搜索,每次搜索完毕后,都会更新当前路径path,例如:Path:/test/add、当前节点路径为/test,那么进入子节点后Path就会变为/add,按这种模式一直匹配,直到path为空或者为/,如果是/通常都是将value.tsr设置为true然后返回,这样就会使得服务器返回一个对路径优化过(/test/优化为/test)的重定向命令,然后再重新路由。

解析客户端发送的数据

一般来说,客户端发送的数据一般有REST参数Query参数Form参数文件数据,所以我来看看这四种数据的获取来源:
首先是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {
    router := gin.Default()

    //curl --location --request POST \
    // '127.0.0.1:8080/welcome?name=jhonson'
    router.GET("/welcome", func(c *gin.Context) {
        //
        name := c.Query("name")
        c.String(http.StatusOK, "Hello %s", name)
    })
   
    // curl --location --request POST '127.0.0.1:8080/user/jack/get'
    router.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    })

    // curl --location --request POST '127.0.0.1:8080/table' \
    // --form 'message=everthing is ok'
    router.POST("/table", func(c *gin.Context) {
        message := c.PostForm("message")
        c.String(http.StatusOK, message)
    })

    // curl -X POST http://localhost:8080/upload \
    // -F "file=@/Users/appleboy/test.zip" \
    // -H "Content-Type: multipart/form-data"
    router.POST("/upload", func(c *gin.Context) {
        //获取文件
        file, _ := c.FormFile("file")
        log.Println(file.Filename)

        c.SaveUploadedFile(file, dst)
    })

    router.Run(":8080")
}

首先我们回顾一下Context中的几个重要变量和获取参数的几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
type Context struct {
    ...省略
    //保存解析得到的参数,路径中的REST参数
    Params   Params

    //存储URL中的查询参数,如:/test?name=jhon&age=11
    // 这样的参数储存在这个对象里
    queryCache url.Values

    //这个用于存储POST/PATCH等提交的body中的参数
    formCache url.Values
}

  • Query()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (c *Context) Query(key string) string {
    value, _ := c.GetQuery(key)
    return value
}

func (c *Context) GetQuery(key string) (string, bool) {
    if values, ok := c.GetQueryArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) GetQueryArray(key string) ([]string, bool) {
    c.getQueryCache()
    if values, ok := c.queryCache[key]; ok && len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

func (c *Context) getQueryCache() {
    if c.queryCache == nil {
        c.queryCache = c.Request.URL.Query()
    }
}

从这里一眼就能看出Query参数的值来自于ContextqueryCache

  • Param()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (c *Context) Param(key string) string {
    return c.Params.ByName(key)
}

func (ps Params) ByName(name string) (va string) {
    va, _ = ps.Get(name)
    return
}

func (ps Params) Get(name string) (string, bool) {
    for _, entry := range ps {
        if entry.Key == name {
            return entry.Value, true
        }
    }
    return "", false
}

REST参数来源于ContextParams

  • PostForm()方法

form参数就不再赘述,基本和Query参数查询的过程一样,来源于ContextformCache

  • FormFile()方法
    前面几个方法都是参数的获取,而FormFile()则是获取客户端上传的文件,这有很大的不同,我们来看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
type FileHeader struct {
    //文件名
    Filename string
    //文件型
    Header   textproto.MIMEHeader
    //文件大小
    Size     int64
    //文件内容(保存在内存中时)
    content []byte
    //临时文件名,当设置的maxMemory小于上传文件时,
    // 会被磁盘化,并利用变量记录临时文件的位置
    tmpfile string
}

func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
    if c.Request.MultipartForm == nil {
        //这个就是解析form参数
        if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            return nil, err
        }
    }
    f, fh, err := c.Request.FormFile(name)
    if err != nil {
        return nil, err
    }
    f.Close()
    return fh, err
}

func (r *Request) ParseMultipartForm(maxMemory int64) error {
    if r.MultipartForm == multipartByReader {
        return errors.New("http: multipart handled by MultipartReader")
    }
    if r.Form == nil {
        err := r.ParseForm()
        if err != nil {
            return err
        }
    }
    if r.MultipartForm != nil {
        return nil
    }

    mr, err := r.multipartReader(false)
    if err != nil {
        return err
    }

    //我们重点看这个方法
    f, err := mr.ReadForm(maxMemory)
    if err != nil {
        return err
    }

    if r.PostForm == nil {
        r.PostForm = make(url.Values)
    }
    for k, v := range f.Value {
        r.Form[k] = append(r.Form[k], v...)
        // r.PostForm should also be populated. See Issue 9305.
        r.PostForm[k] = append(r.PostForm[k], v...)
    }

    r.MultipartForm = f

    return nil
}

type Form struct {
    Value map[string][]string
    File  map[string][]*FileHeader
}

func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
    form := &Form{make(map[string][]string), make(map[string][]*FileHeader)}
    defer func() {
        if err != nil {
            form.RemoveAll()
        }
    }()

    // 需要额外的10 MB的空间存储非Part-form的数据
    maxValueBytes := maxMemory + int64(10<<20)
    for {
        p, err := r.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }

        name := p.FormName()
        if name == "" {
            continue
        }
        filename := p.FileName()

        var b bytes.Buffer
        //如果文件名为空,则认为客户端上传的是
        //会被认为是form表单参数,添加到PostForm中,
        // 最终传递到Context的formCache中
        if filename == "" {
           
            n, err := io.CopyN(&b, p, maxValueBytes+1)
            if err != nil && err != io.EOF {
                return nil, err
            }
            maxValueBytes -= n
            if maxValueBytes < 0 {
                return nil, ErrMessageTooLarge
            }
            form.Value[name] = append(form.Value[name], b.String())
            continue
        }

       
        fh := &FileHeader{
            Filename: filename,
            Header:   p.Header,
        }
        //读取数据到缓冲区中
        n, err := io.CopyN(&b, p, maxMemory+1)
        if err != nil && err != io.EOF {
            return nil, err
        }
        //如果文件过大,则写到磁盘上的临时文件再继续读
        if n > maxMemory {
            // too big, write to disk and flush buffer
            file, err := ioutil.TempFile("", "multipart-")
            if err != nil {
                return nil, err
            }
            size, err := io.Copy(file, io.MultiReader(&b, p))
            if cerr := file.Close(); err == nil {
                err = cerr
            }
            if err != nil {
                os.Remove(file.Name())
                return nil, err
            }
            //内存容量不足时,将tmpfile记录为临时文件名称
            fh.tmpfile = file.Name()
            fh.Size = size
        } else {
            //如果文件能存储在内存中,就记录数据位置
            fh.content = b.Bytes()
            fh.Size = int64(len(fh.content))
            maxMemory -= n
            maxValueBytes -= n
        }
        form.File[name] = append(form.File[name], fh)
    }

    return form, nil
}

这个文件获取的过程比较长,我就只对比较关键的位置进行了注释。概括一下就是客户端传过来的文件,最初会被写入到缓冲区(大小由maxMemory决定)中。如果缓冲区无法容纳整个文件时,就会被写入到临时文件夹中,作为一个临时文件被磁盘化,而FileHeader.tmpfile就记录了临时文件的位置。如果缓冲区能够容纳时,则返回缓冲区中有效数据的字节数组切片,保存在FileHeader.content中。所以拿到FileHeader就相当于拿到客户端传过来的文件数据了。