背景
net/http 虽然具有了路由注册功能,并且可以启动监听端口,实现一个简单的 http 服务,但是 net/http 本身提供的功能还是比较简单、原始,而且暴露的函数签名的参数是 (w http.ResponseWriter, req *http.Request),开发者解析请求和回写结果都不是很方便,因此产生了很多 http 框架。
gin 框架基于 httprouter 实现最重要的路由模块,采用类似字典树一样的数据结构来存储路由与 handle 方法的映射,HTTP 实现和标准库 net/http 是一致的。
其他改进了 net/http 框架的还有 fasthttp,采用了不同于 net/http 的 HTTP 实现,具有极快的运行性能。net/http 的实现是一个连接新建一个 goroutine;fasthttp 是利用一个 worker 复用 goroutine,减轻 runtime 调度 goroutine 的压力。
web 服务启动流程
- 核心概念:
Request:用户请求的信息:用来解析用户的请求信息,包括 post、get、cookie、url 等信息。Response:服务器需要反馈给客户端的信息。- 用户的每次请求连接。
Handler:处理请求和生成返回信息的处理函数。Router:路由根据url找到对应的Handler并执行
- 客户端与服务端的交互来自
client的request和server的response。- Router 实现一个 Multiplexer, Multiplexer 的目的是为了找到 path 对应的处理函数 handler
- handler 对 request 进行处理,同时构建 response
- 具体流程是:
Client->Requests->Multiplexer(router)->handler->Response->Client
- Golang 中的 http 服务最重要的就是
Multiplexer和handler,Golang 中的Multiplexer基于ServeMux结构实现。
net/http 库中的 web 服务启动流程
核心结构
Handler接口: 所有请求的处理器、路由ServeMux都满足该接口。任何结构体,只要实现了ServeHTTP方法,这个结构就可以称之为handler对象。ServeMux结构体: HTTP 请求的Multiplexer(路由)。负责将每一个接收到的请求的 URL 与一个注册模式的列表进行匹配,并调用和 URL 最匹配的模式的处理器,其内部用一个 map 来保存所有处理器Handler。muxEntry结构体:保存了Handler请求处理器和匹配的模式字符串- 包级别变量
DefaultServeMux:ServeMux的一个实例,表示默认路由,使用包级别的http.Handle()、http.HandleFunc()方法注册处理器时都是注册到该路由中。 ServerMux结构体的ServeHTTP()方法: 位于ServerMux结构体中,满足Handler接口。主要用于间接调用它所保存的muxEntry中保存的Handler处理器的ServeHTTP()方法。
http.HandlerFunc适配器:- 满足
Handler接口,这个类型默认就实现了ServeHTTP - 只要函数的签名为
func(w http.ResponseWriter, r *http.Request),均可作为处理函数,因为它可以被转换为http.HandlerFunc函数类型
- 满足
Server结构体: 用户可以定义一个 struct,其中实现了ServeHTTP方法,作为handler对象传给Server。// Handler接口 type Handler interface { ServeHTTP(ResponseWriter, *Request) //路由具体实现 } // ServeMux结构体 type ServeMux struct { mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 m map[string] muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 hosts bool // 是否在任意的规则中带有host信息 } // muxEntry 结构体 type muxEntry struct { h Handler // 控制器 pattern string // 匹配字符串 } // 包级别变量 DefaultServeMux func NewServeMux() *ServeMux { return new(ServeMux) } var DefaultServeMux = NewServeMux() // HandlerFunc 适配器 type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } // Server 结构体 type Server struct { Addr string // TCP address to listen on, ":http" if empty Handler Handler // Handler是一个interface ... }
HTTP 的使用模式
- 使用默认的路由来注册处理函数
- 使用自定义的路由来注册处理函数
直接自定义一个
Server实例:该模式可以很方便的管理服务端的行为func main() { ... // 1. 向默认路由注册处理器函数 http.HandleFunc("/", serveHome) //或http.Handle("/", http.HandlerFunc(serveHome)) http.Handle("/file", myHandler("somefile")) err := http.ListenAndServe(*addr, nil) //启动监听,第二个参数nil表示使用默认路由DefaultServeMux中注册的处理器 if err != nil { log.Fatalln("ListenAndServe: ", err) } // 2. 使用自定义的路由来注册处理函数 mux := http.NewServeMux() //新建一个自定义的路由 mux.Handle("/file", myHandler("somefile")) mux.HandleFunc("/", serveHome) err := http.ListenAndServe(*addr, mux) //启动监听 if err != nil { log.Fatalln("ListenAndServe: ", err) } // 3. 直接自定义一个Server实例 mux := http.NewServeMux() mux.Handle("/file", myHandler("somefile")) mux.HandleFunc("/", serveHome) s := &http.Server{ Addr: ":8080", Handler: mux, //指定路由或处理器,不指定时为nil,表示使用默认的路由DefaultServeMux ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, ConnState: //指定连接conn的状态改变时的处理函数 //.... } log.Fatal(s.ListenAndServe()) }
完整执行过程:监听端口、建立连接和处理 client 请求
- 通过
http.ListenAndServe(addr string, handler Handler)启动服务,实例化Server,调用Server对象的ListenAndServer方法,并将该方法的返回值error返回给调用方。 server.ListenAndServe()内部调用net.Listen("tcp", addr),该方法内部又调用net.ListenTCP()创建并返回一个net.Listener监听器 ln。- 然后把监听器 ln 断言转换为
TCPListener类型,并根据它构造一个tcpKeepAliveListener对象并传递给server.Serve()方法。这是因为TCPListener实现了Listener接口,所以tcpKeepAliveListener也实现了Listener接口,并且它重写了Accept()方法,目的是为了调用SetKeepAlive(true),让操作系统为收到的每一个连接启动发送keepalive消息(心跳,为了保持连接不断开)。 server.Serve()方法调用tcpKeepAliveListener对象的Accept()方法返回一个连接conn(该连接启动了心跳),并为每一个conn创建一个新的go协程执行conn.server()方法conn.server()方法会读取请求,然后根据conn内保存的server来构造一个serverHandler类型,并调用它的ServeHTTP()方法:serverHandler{c.server}.ServeHTTP(w, w.req)- 路由
ServeMux的ServeHTTP方法则会根据当前请求提供的信息来查找最匹配的Handler 查找到的
Handler接口值 h 就是我们事先注册到路由中与请求匹配的Handler;而h的动态类型是HandlerFunc类型(它也满足Handler接口);所以,以上h.ServeHTTP(w, r)实际上调用的是接口值 h 中持有的动态值(也就是我们定义的处理函数,详见http.HandlerFunc适配器)// (1) 实例化 server // The handler is typically nil, in which case the DefaultServeMux is used. func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } // (2) 返回监听器 func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } // Accepted connections are configured to enable TCP keep-alives. return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) } // (3) 启动发送keepalive消息 type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } tc.SetKeepAlive(true) //发送心跳 tc.SetKeepAlivePeriod(3 * time.Minute) //发送周期 return tc, nil } // (4) 启用 go 协程执行 conn.server() 方法 func (srv *Server) Serve(l net.Listener) error { defer l.Close() if fn := testHookServerServe; fn != nil { fn(srv, l) } var tempDelay time.Duration //重试间隔 if err := srv.setupHTTP2_Serve(); err != nil { return err } srv.trackListener(l, true) //缓存该监听器 defer srv.trackListener(l, false) //从缓存中删除当前监听器 baseCtx := context.Background() ctx := context.WithValue(baseCtx, ServerContextKey, srv) //新建一个context用来管理每个连接conn的Go协程 for { rw, e := l.Accept() //调用tcpKeepAliveListener对象的 Accept() 方法 if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed //退出Serve方法,并执行延迟调用(从缓存中删除当前监听器) default: } //如果发生了net.Error错误,则隔一段时间就重试一次,间隔时间每次翻倍,最大为1秒 if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c := srv.newConn(rw) //该方法根据net.Conn、srv构造了一个新的http.conn类型 c.setState(c.rwc, StateNew) //缓存该连接的状态,如果方法:Server.ConnState(net.Conn, ConnState)不为nil,就根据当前连接的状态执行它 go c.serve(ctx) } } // (5) 调用ServeHTTP()方法 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler // handler == nil 时使用默认的DefaultServeMux路由 // 否则使用在第1步中为Serve指定了的Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) } // (6) 查找最匹配的Handler func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) //规范化请求的路径格式,查找最匹配的Handler h.ServeHTTP(w, r) }
Gin 框架中的路由设置
Gin 核心结构
gin.Context
- 最重要的结构,贯穿一个 http 请求的所有流程,包含全部上下文信息。
- 提供了很多内置的数据绑定和响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它会为每一种形式都单独定制一个渲染器
engine的
ServeHTTP方法,在响应一个用户的请求时,都会先从临时对象池中取一个 context 对象。使用完之后再放回临时对象池。为了保证并发安全,如果在一次请求新起一个协程,那么一定要 copy 这个 context 进行参数传递type Context struct { writermem responseWriter Request *http.Request // 请求对象 Writer ResponseWriter // 响应对象 Params Params // URL 匹配参数 handlers HandlersChain // 请求处理链 index int8 engine *Engine // This mutex protect Keys map mu sync.RWMutex // Keys is a key/value pair exclusively for the context of each request. Keys map[string]interface{} // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string ... }
gin.Engine
- 框架的入口,通过
Engine对象来定义服务路由信息、组装插件、运行服务,是框架的核心发动机,整个Web服务的都是由它来驱动的。 - 用的是 Go 语言内置的
http server,Engine的本质只是对内置的 HTTP 服务器的包装。 关键字段
- trees:Router Tree 基于基数树实现。每个节点都会挂接若干请求处理函数构成一个请求处理链
HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。 - addRoute 方法:用于添加 URL 请求处理器,它会将对应的路径和处理器挂接到相应的请求树中。
RouterGroup:RouterGroup是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine结构体继承了RouterGroup,所以Engine直接具备了 RouterGroup 所有的路由管理功能。同时RouteGroup对象里面还会包含一个Engine的指针,可以调用engine的addRoute方法。IRouter接口:RouterGroup实现了IRouter接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中RouterGroup内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, basePath: "/", root: true, }, FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, HandleMethodNotAllowed: false, ForwardedByClientIP: true, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, TrustedProxies: []string{"0.0.0.0/0"}, AppEngine: defaultAppEngine, UseRawPath: false, RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), delims: render.Delims{Left: "{{", Right: "}}"}, secureJSONPrefix: "while(1);", } engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { return engine.allocateContext() } return engine }
- trees:Router Tree 基于基数树实现。每个节点都会挂接若干请求处理函数构成一个请求处理链
gin.RouterGroup
- 方便对路由进行管理,可以对组进行设置中间件。
RouterGroup是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了RouterGroup,所以Engine直接具备了RouterGroup所有的路由管理功能。同时RouteGroup对象里面还会包含一个Engine的指针,可以调用 engine 的addRoute方法。RouterGroup实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用Engine.addRoute方法将请求处理器挂接到路由树中。RouterGroup内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。Engine 对象内嵌的RouterGroup对象的前缀路径是 /,它表示根路径。RouterGroup支持分组嵌套,使用Group方法就可以让分组下面再挂分组。
radix tree
前缀树是一个多叉树,广泛应用于字符串搜索,每个树节点存储一个字符,从根节点到任意一个叶子结点串起来就是一个字符串。
radix tree 是优化之后的前缀树,对空间进一步压缩,从上往下提取公共前缀,非公共部分存到子节点,这样既节省了空间,同时也提高了查询效率(左边字符串sleep查询需要5步, 右边只需要3步),Gin 的路由树就是用 radix tree 实现的。

Gin为每一种请求都维护了一个 radix tree,不同的请求会被解析并送到对应的 radix tree 进行处理。

Gin 为了更具扩展性,每一层的节点按照 priority 排序,一个节点的 priority 值表示他包含的所有子节点(子节点,孙节点等)的数量,这样做有两个好处:
- 被最多路径包含的节点会被最先评估。这样可以让尽量多的路由快速被定位。
有点像成本补偿。最长的路径可以被最先评估,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先评估(即每次拿子节点都命中),那么所花时间不一定比短路径的路由长。
//树节点 type node struct { path string //保存节点的路径,像上面radix tree图中节点里面的值 indices string //子节点的首字符根据priority排列组成的字符串,为了方便遍历 wildChild bool //标识孩子节点是否有通配符 nType nodeType //节点类型 priority uint32 //优先级 children []*node handlers HandlersChain //处理函数链 fullPath string //保存完整路径 } // node结构中nodeType节点类型 const ( static nodeType = iota // default 普通节点,默认 root // 根节点 param // 参数路由,比如 /user/:id catchAll // 匹配所有内容的路由,比如 /article/*key )
中间件与请求链
- 每个路由节点都挂一个函数链。
- 只有函数链的尾部是业务处理,前面的部分都是插件函数。
- 在 Gin 中插件和业务处理函数形式是一样的,都是
func(*Context)。当我们定义路由时,Gin 会将插件函数和业务处理函数合并在一起形成一个链条结构。Gin 在接收到客户端请求时,找到相应的处理链,构造一个 Context 对象,再调用它的 Next() 方法就正式进入了请求处理的全流程。 4 种常用方法
- (1) c.Next() 利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。
- (2) c.Abort() 原理是将 Context.index 调整到一个比较大的数字,这样 Next() 方法中的调用循环就会立即结束(而不是中断执行流)。执行 Abort() 方法之后,当前函数内后面的代码逻辑还会继续执行
- (3) c.Set()
(4) c.Get()
// Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } } const abortIndex int8 = math.MaxInt8 / 2 // Abort prevents pending handlers from being called. Note that this will not stop the current handler. func (c *Context) Abort() { c.index = abortIndex }
完整启动流程
- Egnine 初始化:通过调用 gin.New() 方法来实例化 Engine。这个过程中同时会创建Context 对象池,减少频繁context实例化带来的资源消耗。
- 注册中间件:将路由处理函数(endpoint)和中间件函数(middleware) 注册到 radix tree 中,tree 中的每个节点可以认为是一个键值对,key 是路由,value 是[ ]HandlerFunc, 里面存储的就是按顺序执行的中间件和handle控制器方法。
- 注册全局中间件
- 注册路由组中间件
- 注册路由以及中间件
- 启动:通过调用 net/http 来启动服务,由于 engine 实现了 ServeHTTP 方法,只需要直接传 engine 对象就可以完成初始化并启动
- 处理请求:通过请求方法和路由找到相对应的树节点,获取储存的 []HandlerFunc 列表,通过调用 c.Next() 处理请求,通过不停的移动下标递归,最后完成处理返回结果
