gin源码学习-路由注册(2)
gin框架主要是在标准库net/http的基础上对路由进行改写,本文将从net/http
与gin
的路由注册分享路由方面的一些理解。
1.net/http的路由注册
1.1 路由注册
首先来个demo:
package main
import (
"log"
"net/http"
)
func main() {
// 1.注册处理器
http.HandleFunc("/ping", pong)
// 2.监听端口,启动服务
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln("start http server fail: ", err)
}
}
func pong(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}
可以看到,一个简单的http server通过调用http.HandleFunc(path, handler)
实现路由的注册,我们顺着继续看:
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
可以看到,其实就是调用默认的servemux的HandleFunc()
方法,我们看看这个默认的servemux:
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux // 创建一个空的servemux
var defaultServeMux ServeMux
// ServeMux also takes care of sanitizing the URL request path and the Host
// header, stripping the port number and redirecting any request containing . or
// .. elements or repeated slashes to an equivalent, cleaner URL.
type ServeMux struct {
// 保证m的并发安全,注册处理器时加写锁保证map的数据正确
mu sync.RWMutex
// path与handler的映射,key-path,value-实体muEntry{path, handler}
m map[string]muxEntry
// 存储muEntry实体切片,map只是静态路由,当map中没有找到匹配,遍历此切片,进行前缀匹配,切片按路由长度倒序排序
es []muxEntry // slice of entries sorted from longest to shortest.
// 处理特殊case,如路由未以“/”打头,注册路由应包含host,故匹配路由时加上host
hosts bool // whether any patterns contain hostnames
}
接下来继续看调用情况:
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler)) // 调用内部的Handle方法注册路由
}
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
// 保护mx.m
mux.mu.Lock()
defer mux.mu.Unlock()
// 处理空path
if pattern == "" {
panic("http: invalid pattern")
}
// 处理空handler
if handler == nil {
panic("http: nil handler")
}
// 处理已经存在的path
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
// 懒汉模式,为空就创建个map用来存储路由信息,path-handler
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
// 路由实体
e := muxEntry{h: handler, pattern: pattern}
// 注册路由信息
mux.m[pattern] = e
// 处理path最后带“/”的,加到es中做前缀匹配
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
// 路由不以"/"打头,host属性设为true,后面匹配路由时,path: "/" + pattern
if pattern[0] != '/' {
mux.hosts = true
}
}
从源码来看,net/http注册源码十分简单粗暴,根本没有restful的风格,不区分请求方法,GET/POST/DELETE一概不管,上生产日积月累,这代码好维护?
net/http的路由注册就是简单的通过map来存储路由信息,key为路由url,value为同时存储url和handler的结构体,注册前不存在就插入,存在就报错,真的很简单的处理。
1.2 net/http的请求处理
从gin的源码分析可以知道,最终的请求是通过具体的ServeHTTP方法实现,不妨看看servemux的ServeHTTP是怎样处理的。
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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) // handler处理请求,如pong这个handler
}
// Handler returns the handler to use for the given request,
// consulting r.Method, r.Host, and r.URL.Path. It always returns
// a non-nil handler. If the path is not in its canonical form, the
// handler will be an internally-generated handler that redirects
// to the canonical path. If the host contains a port, it is ignored
// when matching handlers.
//
// The path and host are used unchanged for CONNECT requests.
//
// Handler also returns the registered pattern that matches the
// request or, in the case of internally-generated redirects,
// the pattern that will match after following the redirect.
//
// If there is no registered handler that applies to the request,
// Handler returns a “page not found” handler and an empty pattern.
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
return mux.handler(r.Host, r.URL.Path)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
return RedirectHandler(u.String(), StatusMovedPermanently), pattern
}
return mux.handler(host, r.URL.Path) // 返回handler
}
// handler的主要实现
// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path) // 继续看调用
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
// 真正地拿到handler
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path] // map中匹配到直接返回
if ok {
return v.h, v.pattern
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es { // 如果在map中未找到匹配的路由信息,在es切片遍历实体muEntry,对实体中的pattern进行前缀匹配
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
源码看完,顺带给个net/http实现的ServeHTTP的处理流程:
小结
-
注册处理器
标准库用map存储路由信息,key-pattern, val-struct{pattern, handler},只做静态路由匹配,另外通过[]muEntry切片存储实体(按实体的pattern长度倒序排列),只要以"/"结尾的路由,都会在切片中存储,后面匹配路由时,如果在map中未匹配到,通过es进行前缀匹配。 -
监听端口,启动服务
net/http.server通过为每个client连接请求创建新的go程来处理,每个http都开go程,如果短时间内有大量连接请求,瞬间server会起大量的goroutine,可能是个性能瓶颈。
2.gin的路由注册
gin的路由功能,其实也是基于net/http重写了路由,其中存储路由的数据结构由net/http的map结构变为radix tree的结构,前缀树,字典树的扩展,具体树的代码,可以去看看gin-gonic/gin/tree.go的代码。
简单说说tree,gin为每种method的都维护了一颗树,比如gin server中注册有GET、POST、DELETE的若干路由,那么gin就创建了GET tree,POST tree,DELETE tree,这三棵树,每棵树又通过radix tree的结构存储了路由节点,其中节点中有相关的handlers的存储数据,这样就实现了通过radix tree存储路由信息。相较map存储,更加合理,效率方面也不错。
以下是tree的node定义:
// 节点结构体
type node struct {
path string
indices string
wildChild bool
nType nodeType // 标识节点类型
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain // 包含中间件加控制器的handler的handlers切片
fullPath string
}
// 方法树结构体
type methodTree struct {
method string
root *node
}
// 方法树切片
type methodTrees []methodTree
2.1 路由注册
依然先看个demo:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default()
engine.GET("/hello", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "hello gin!",
})
})
group := engine.Group("/app/v1")
group.Get("/ping", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
})
engine.Run(":8080")
}
分析了net/http的源码后,依葫芦画瓢来看看gin的路由注册的源码。
在之前的gin的项目启动源码分析中,我们知道engine中就有RouterGroup结构,所以路由注册注册就是从RouterGroup这个结构体做文章,从engine.GET()方法入手吧。
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers) // 实际注册路由的方法
}
gin作为web框架,也实现了POST/DELETE/PATCH等的注册,这里其实就是通过不同的method,注册了method/pattern/handler的路由信息。
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodDelete, relativePath, handlers)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPatch, relativePath, handlers)
}
// PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPut, relativePath, handlers)
}
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodOptions, relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodHead, relativePath, handlers)
}
当然如果不指定method,也可以直接通过Any
的方法注册:
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
for _, method := range anyMethods {
group.handle(method, relativePath, handlers)
}
return group.returnObj()
}
// 包含“GET”/"POST"/"DELETE"等method
// anyMethods for RouterGroup Any method
anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}
回到GET方法,我们看看具体执行的注册方法。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) // 拼接路径
handlers = group.combineHandlers(handlers) // 组合handlers
group.engine.addRoute(httpMethod, absolutePath, handlers) // 加入路由树中
return group.returnObj()
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
// 组合路由就是将组内的handlers(中间件)复制一份,再加上传入的handler,组合成一个handlers的切片
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
然后来看看是怎样加入到路由树中的。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) // tree slice中获取对应方法的tree的root
if root == nil { // 方法树的根节点为nil,就新建个对应method的方法树
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers) // 关键处理,加入路由信息
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
// tree.go
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 { // 空树直接加到root节点
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
// 接入点子节点
walk:
for {
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
i := longestCommonPrefix(path, n.path)
...
// Split edge
...
// Make new node a child of this node
...
// Check if a child with the next path byte exists
...
// Otherwise insert it
...
n.insertChild(path, fullPath, handlers) // 插入路由节点
return
}
// Otherwise add handle to current node
...
return
}
}
从源码来看,路由注册比较简单,概括就是将路由信息加入到方法树中,
- 没有对应方法树就新建,路由信息作为root节点设置,
- 如果存在对应的方法树,就根据树的结构特性插入到合适的位置。
2.2 路由分组
gin框架为我们提供了路由分组的处理,对于多版本的api设计很有帮助,看源码吧。
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
// 新建group
return &RouterGroup{
// 与单个路由handler的处理一致,复制一份全局的中间件+组路由自己的中间件(有的话)
Handlers: group.combineHandlers(handlers),
// 计算path
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
可以看到组路由的处理起始就是新建一个group,其他处理与单个路由的处理类似,在组路由下也可以继续嵌套其他的group,这里不多做说明。
总体来说,组路由的设置也很简单,符合gin框架的简单易用的特性,这些特点使得gin框架广受欢迎。
2.3 请求处理
参考上篇分享,gin源码学习-项目启动(1) 。
请求调用流程:
参考: