Golang 主流 Web 框架路由实现分析

Golang 2113 字

Beego 路由实现分析

Beego 的设计初衷是为了提供一个简单易用的 web 框架,同时也保证了高性能和可扩展性。它的设计理念是基于 MVC 模式,在 Go 最早期的时候算是最流行的框架之一。

下面的代码是 Beego 最简单的使用方法。

type UserController struct {
    web.Controller
}

func (c* UserController) GetUser() {
    c.Ctx.WriteString("hello maksim")
}

func main() {
    web.BConfig.CopyReqeustBody = true
    web.Router("/user", c, "get:GetUser")
    web.Run(":8080")
}

这是一段非常经典的基于 MVC 的代码结构。

web.Router 用来注册路由,当我们访问 /user 路径的时候,就去访问 UserController 中的 GetUser 方法,其中 ”get:User“中的 get 就是 http方法,如果是 Get 就执行,如果是 Post 就无法响应请求。

Controller 抽象

Beego 是基于 MVC(Model-View-Controller)的,所以它定义了一个核心接口ControllerInterface,在这个接口中我们可以看到很多和 MVC 相关的概念。

// ControllerInterface is an interface to uniform all controller handler.
type ControllerInterface interface {
    Init(ct *context.Context, controllerName, actionName string, app interface{})
    Prepare()
    Get()
    Post()
    Delete()
    Put()
    Head()
    Patch()
    Options()
    Trace()
    Finish()
    Render() error
    XSRFToken() string
    CheckXSRFCookie() bool
    HandlerFunc(fn string) bool
    URLMapping()
}

ControllerInterface定义了一个控制器必须要解决什么问题。同时 ContollerInterface 的默认实现 Controller 提供了实现自定义控制器的各种辅助方法,所以在 Beego里面,一般都是组合 Controller 来实现自己的Controller,例如案例中的代码那样:

type UserController struct {
    web.Controller
}

在 Beego 中路由注册和服务启动时通过另外一套机制实现的(HTTPServer + ControllerRegister),Controller 理解为只是帮助我们组织代码。

HTTPServer

ControllerInterface 可以看做是核心接口,因为它直接提现了 Beego 的设计初衷:MVC 模式。同时它也是用户核心切入点。但是如果从功能特性上来说 HTTPServer 和 ControllerRegister 才是核心。

HttpServer:代表一个”服务器“,大多数时候它就是一个进程,其结构体定义如下:

// HttpServer defines beego application with a new PatternServeMux.
type HttpServer struct {
    Handlers           *ControllerRegister
    Server             *http.Server
    Cfg                *Config
    LifeCycleCallbacks []LifeCycleCallback
}

我们可以看到 HttpServer 中其实就包含了我们的 http.Server ,这个结构体相信你不会陌生,在这里就不做赘述了,其实这里的 HttpServer 也起到了资源隔离的作用。

路由树

Beego 的核心结构体是三个: ControllerRegister、Tree,leafinfo

ControllerRegister:真正干活的人,注册路由,路由匹配和执行业务代码都是通过它来完成的,它类似容器,放着所有的路由树,路由树是按照 HTTP mthod来组织的,例如 Get 方法会对应有一颗路由树。

// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
    routers      map[string]*Tree
    enablePolicy bool
    enableFilter bool
    policies     map[string]*Tree
    filters      [FinishRouter + 1][]*FilterRouter
    pool         sync.Pool

    // the filter created by FilterChain
    chainRoot *FilterRouter

    // keep registered chain and build it when serve http
    filterChains []filterChainConfig

    cfg *Config
}

Tree:它代表 的就是路由树,在 Beego 里面,一颗路由树倍看做是由子树组成的。

// Tree has three elements: FixRouter/wildcard/leaves
// fixRouter stores Fixed Router
// wildcard stores params
// leaves store the endpoint information
type Tree struct {
    // prefix set for static router
    prefix string
    // search fix route first
    fixrouters []*Tree
    // if set, failure to match fixrouters search then search wildcard
    wildcard *Tree
    // if set, failure to match wildcard search
    leaves []*leafInfo
}

leafinfo:代表叶子结点的,结构如下

type leafInfo struct {
    // names of wildcards that lead to this leaf. eg, ["id" "name"] for the wildcard ":id" and ":name"
    wildcards []string

    // if the leaf is regexp
    regexps *regexp.Regexp

    runObject interface{}
}

Beego 的树定义,并没有采用 children 式的定义,而是采用递归式的定义,即一棵树是由根节点 + 子树构成。

2023-04-06T07:26:24.png

Context 抽象

用户操作请求和响应都是通过 Ctx 来达成的。它代表的是整个请求执行过程的上下文。

// Context Http request context struct including BeegoInput, BeegoOutput, http.Request and http.ResponseWriter.
// BeegoInput and BeegoOutput provides an api to operate request and response more easily.
type Context struct {
    Input          *BeegoInput
    Output         *BeegoOutput
    Request        *http.Request
    ResponseWriter *Response
    _xsrfToken     string
}

进一步,Beego 将 Context 细分了几个部分:

  • Input: 定义了很多和处理请求有关的方法
  • Outpu:定义了很多和响应有关的方法
  • Response:对 http.RequestWriter的二次封装

核心抽象总结

  • ControllerRegister 最为基础,它解决了路由注册和路由匹这个基础问题。
  • Context 和 Controller 位用户提供了丰富的 API,用于辅助构建系统。
  • HttpServer 作为服务器抽象,用于管理应用生命周期和资源隔离单位。

Gin 路由实现分析

Gin 框架可以说是目前国内最流行的 API 框架了,Gin没有Controller的抽象,对于如何组织我们的代码结构,和编码方式全都交给用户去决定。

其最简单的使用方法如下:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

如果我们想用 MVC 架构去组织我们的代码结构我们可以这样:

package main

import "github.com/gin-gonic/gin"

type UserController struct {

}

func (c * UserController) GetUser(ctx *gin.Context) {
    ctx.String(200, "hello world")
}

func main() {
    r := gin.Default()
    ctrl := &UserController{}
    g.Get("/user", ctrl.GetUser)
    r.Run() 

IRoutes 接口

核心接口 IRoutes:提供的是注册路由的抽象。它的实现 Engine类似于ControllerRegister。

// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}
  • Use方法提供了用户接入自定义逻辑的能力,这个一般情况下也被看做是插件机制。
  • StaticFile,Static,StaticFcs 额外提供了静态文件的接口。

Engine 实现

type Engine struct {
    ...
    
    trees    methodTrees
}

Engine 可以看做是 Beego 中 HttpServerControllerRegister 的合体。

  • 实现了路由树功能,提供了注册和匹配路由的功能
  • 它本身可以作为一个Handler传递到http包,用于启动服务器

路由树

Gin 的路由树的关键结构体设计的更加直观:methodTrees,methodTree,Node。

methodTreeEngine 中的一个结构体,Engine的路由树功能本质上是依赖于 methodTree 实现路由树的功能,methodTree 定义了单棵树,数在 Gin 里面采用的是 children 定义方式,即树由节点构成,定义如下:

type methodTree struct {
    method string
    root   *node
}

其中,method 代表 HTTP 方法,root 则是存储路由的根节点。

methodTrees 它实际上代表的是森林,即每一个 HTTP 方法都对应到一树上,例如 GET 会有一颗路由树,这一点和 Beego 的设计很类似。

type methodTrees []methodTree

node:代表树上的一个节点,里面维持了 children,即子节点。同时有 ndoeType 和 wildChild 来标记一些特殊节点。

type nodeType uint8

const (
    static nodeType = iota // default
    root
    param
    catchAll
)

type node struct {
    path      string
    indices   string
    children  []*node
    handlers  HandlersChain
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
    fullPath  string
}

Gin 是利用路由的公共前缀来构造路由树的。

2023-04-06T07:59:04.png

HandlerFunc 和 HandlersChain

HandlerFunc 定义了核心抽象——处理逻辑,它是一个函数类型,它接受一个 Context 参数并返回 void。这个函数的目的是处理请求并生成响应。

HandlersChain 是一个由 HandlerFunc 组成的切片类型,它的目的是让我们能够组合多个处理程序函数,这样的数据结构构造了一个责任链模式。

Gin 中,一个 HandlerFunc 可以调用 context.Next() 来转移控制到链中的下一个处理程序。这使得我们可以在多个处理程序之间共享状态,以及在处理程序之间传递控制。

Untitled

最后一个则是封装了业务逻辑的 HandlerFunc。

Context 抽象

Context 也是代表了执行的上下文,提供了丰富的 API:

  • 处理请求的 API,代表以 Get 和 Bind 为前缀的方法
  • 处理响应的 API,代表返回 Json 或者 XML 相应的方法
  • 渲染界面,如 HTML 方法
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter

    Params   Params
    handlers HandlersChain
    index    int8
    fullPath string
}

Untitled

Iris是一款Go语言编写的高性能Web框架,相对于 gin 在国内用的要稍微少一点,它的路由实现是基于httprouter的。Iris的路由功能强大,支持多种请求方法、正则表达式匹配和中间件等特性。

func main() {
    app := iris.New()

    app.Get("/", func (ctx iris.Context){
            _, _ = ctx.HTML("Hello <strong> %s </strong>", "World!")
    }) 

    _ = app.Listen(":8083")
}

Iris 路由实现分析

Application

Application 是 Iris 的核心抽象,它代表的是“应用”。实际上这个语义更接近 Beego 的 HttpServer 和 Gin 的 Engine。

// Application is responsible to manage the state of the application.
// It contains and handles all the necessary parts to create a fast web server.
type Application struct {

    *router.APIBuilder
    *router.Router
    router.HTTPErrorHandler 
    ContextPool             *context.Pool
    
    contextErrorHandler context.ErrorHandler

    config *Configuration

    // the golog logger instance, defaults to "Info" level messages (all except "Debug")
    logger *golog.Logger

    I18n *i18n.I18n

    // Validator is the request body validator, defaults to nil.
    Validator context.Validator
    minifier *minify.M

    view *view.View

    builded     bool
    defaultMode bool

    OnBuild func() error

    mu sync.Mutex

    name string

    Hosts             []*host.Supervisor
    hostConfigurators []host.Configurator
}

它提供了:

  • 生命周期控制功能,如 Shutdown 等方法
  • 注册路由的 API

路由相关

Iris 的设计非常复杂。在 Beego 和 Gin 里面能够明显看到路由树的审计,但是在 Iris 里面就很很难看出来。

在Iris中,路由的注册是通过HandleHandleFunc方法实现的。Handle方法接收一个iris.Handler类型的参数,而HandleFunc方法接收一个func(c iris.Context)类型的参数。这两个方法内部都会调用Party方法和addRoute方法完成路由的注册。

Party方法用于创建一个子路由分组,并返回一个新的Party对象。addRoute方法则将路由信息保存到路由表中,其中包括请求方法、路径、处理函数和中间件等信息。Iris的路由表是基于httprouter实现的,使用了Trie树和Radix树结构进行快速匹配。

处理路由相关的三个抽象:

  • Route:直接代表了已注册的路由。在 Beego 和 Gin 里面,对应的是路由树的节点。
  • APIBuilder:创建 Route 的 Builder 模式,Party 也是它创建的。
  • repository:存储了所有的 Routes 了,有点接近 Gin 的 methodTrees 的概念

    type repository struct {
        routes []*Route
        paths  map[string]*Route // only the fullname path part, required at CreateRoutes for registering index page.
    }

Context 抽象

Context 也是代表上下文。

Context 本身也是提供了各种处理请求和响应的方法。

基本上和 Beego 和 Gin 的 Context 没啥区别,比较有特色的是它的 Context 支持请求级别的添加 Handler,即 AddHandler 方法。

Echo 路由分析

Echo

Echo 是它内部的一个结构体,类似与 Beego 的 HttpServer 和 Gin 的 Engine

  • 暴露了注册路由的方法,但是它并不是路由树的载体
  • 生命周期管理:如 Shutdown 和 Start 等方法

在 Echo 里面有两个相似的字段:

  • router: 这其实就是代表路由树
  • routers:这代表的是根据 Host 进行分组组织,可以看到是近似于namespace 类似的概念,既是一种组织方式,也是一种隔离机制。

Route 和 node

Router代表的就是路由树,node 代表的是路由树上的节点。

node 里面有一个很有意思的设计:staticChildern、paramChild 和 anyChild。利用这个设计可以轻松实现路由优先级和路由冲突检测。

它里面还有一个字段叫做 echo,维护的事使用 Route 的 Echo 实例。这种设计形态在别的地方也能见到,比如说在 sql.Tx里维护了一个 sql.DB 的实例。

Context

一个大而全的接口,定义类处理请求和响应的各种方法。和 Beego、Gin、Iris 的 Contex 没有什么区别。

框架的对比

BeegoGinIrisEcho
代表服务器HttpServerEngineApplicationEcho
代表路由ControllerRegisterMethodThreeRouteRoute
上下文ContextContextContextContext
处理逻辑HanderFuncHanderFuncHanderFuncHanderFunc

路由树的总结

  • 归根结底就是设计一颗多叉树
  • 同时我们按照 HTTP 方式来组织路由树,每个 HTTP 方法一棵树
  • 节点维护住自己的子节点

参考资料

  1. 极客时间 - Golnag 实战训练营
  2. 慕课网 - Golang 高级工程师
  3. Gin 官网
maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见