- 第一章:
Gin 是如何储存和映射URL 路径到相应的处理函数的 - 第二章:
Gin 中间件的设计思想及其实现 - 第三章:
Gin 是如何解析客户端发送请求中的参数的 - 第四章:
Gin 是如何将各类格式(JSON/XML/YAML 等)数据解析返回的
Gin Github官方地址
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() } |
通过上面这个过程可以了解到
1 2 3 | type Handler interface { ServeHTTP(ResponseWriter, *Request) } |
这咋一看,瞬间就明白了许多,
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) } |
服务处理
在正式开始了解这个处理过程之前,我们先来了解一下
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 } |
在了解完
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) } |
上述代码就是整个请求的处理过程,而节点查找和参数解析都在
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 } } |
其实从上面都能看出,这个过程就是从搜索树的根节点依次向下搜索,每次搜索完毕后,都会更新当前路径
解析客户端发送的数据
一般来说,客户端发送的数据一般有
首先是一个简单的示例:
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") } |
首先我们回顾一下
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() } } |
从这里一眼就能看出
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 } |
PostForm() 方法
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 } |
这个文件获取的过程比较长,我就只对比较关键的位置进行了注释。概括一下就是客户端传过来的文件,最初会被写入到缓冲区(大小由