博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Golang之Gin框架源码解读——第三章
阅读量:3898 次
发布时间:2019-05-23

本文共 14435 字,大约阅读时间需要 48 分钟。

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

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

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 = powalk: //这个标号使用中递归的,这里使用的是循环式的递归 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就相当于拿到客户端传过来的文件数据了。

转载地址:http://sqfen.baihongyu.com/

你可能感兴趣的文章
Webkit之平台相关层
查看>>
Webkit之UI层布局
查看>>
WebKit之InlineBox绘制阶段顺序
查看>>
WebKit之图像显示分析流程
查看>>
WebKit之addToJavaScriptWindowObject()分析
查看>>
资源之收集列表整理
查看>>
JS之kindeditor的用法简介
查看>>
Linux之最简字符驱动的编码模型
查看>>
服务之Windows平台上搭建SVN服务
查看>>
Python之封装diff命令的项目比较命令(格式化diff输出结果)
查看>>
Shell之定时拉起脚本
查看>>
Shell之导出数据库的表为Excel的脚本
查看>>
Shell之预启动脚本
查看>>
WebKit之Node的继承关系图
查看>>
WebKit之RenderObject继承关系图整理
查看>>
WebKit之JSCell的继承关系图
查看>>
WebKit之HTMLTreeBuilder类的解析框架
查看>>
WebKit之HTMLConstructionSite类组成
查看>>
Linux之so加载原理分析
查看>>
C之基于signal信号的交互式的测试功能模块(触发时机)
查看>>