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的监听入口开始逐渐摸索。

建立监听服务

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类型的对象,而该对象定义如下:

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 && 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函数之中,我们来看一下他是如何匹配路径和参数解析的:

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参数文件数据,所以我来看看这四种数据的获取来源:
首先是一个简单的示例:

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中的几个重要变量和获取参数的几个方法:

type Context struct {
	...省略
	//保存解析得到的参数,路径中的REST参数
    Params   Params

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

	//这个用于存储POST/PATCH等提交的body中的参数
	formCache url.Values
}
  • Query()方法
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()方法
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()则是获取客户端上传的文件,这有很大的不同,我们来看看:
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就相当于拿到客户端传过来的文件数据了。

你可能感兴趣的:(Golang,#,Gin源码解读,中间件,go,web)