gin源码学习-路由注册(2)

gin框架主要是在标准库net/http的基础上对路由进行改写,本文将从net/httpgin的路由注册分享路由方面的一些理解。

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)

请求调用流程:

参考:

posted on 2022-12-08 16:45  进击的davis  阅读(271)  评论(0编辑  收藏  举报

导航