Beego
小案例
package main
import "github.com/beego/beego/v2/server/web"
func main() {
web.BConfig.CopyRequestBody = true
c := &UserController{}
web.Router("/user", c, "get:GetUser")
web.Router("/user", c, "post:CreateUser")
web.Run(":8080")
}
type UserController struct {
web.Controller
}
func (c *UserController) GetUser() {
c.Ctx.WriteString("hello, world")
}
func (c *UserController) CreateUser() {
u := &User{}
err := c.Ctx.BindJSON(u)
if err != nil {
c.Ctx.WriteString(err.Error())
return
}
c.Ctx.JSONResp(u)
}
type User struct {
Name string
}
Controller 抽象
Beego 是基于 MVC (Model-View-Controller) 的,所以它定义了一个核心接口 ControllerInterface。ControllerInterface 定义了一个控制器必须要解决什么问题。
同时 ControllerInterface 的默认实现 Controller 提供了实现自定义控制器的各种辅助方法,所以在 Beego 里面, 一般都是组合 Controller 来实现自己的 Controller。
// 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()
}
注意到用户虽然被要求组合 Controller,但是路由注册和服务器启动是通过另外一套机制来完成的。
func main() {
web.BConfig.CopyRequestBody = true
c := &UserController{}
web.Router("/user", c, "get:GetUser")
web.Router("/user", c, "post:CreateUser")
web.Run(":8081")
}
HttpServer 和 ControllerRegister
ControllerInterface 可以看做核心接口,因为它直接体现了 Beego 的设计初衷:MVC 模式。同时它也是用户核心接入点。
但是如果从功能特性上来说,HttpServer 和 ControllerRegister 才是核心。
- HttpServer:代表一个
服务器
,大多数时候它就是一个进程。 - ControllerRegister:真正干活的人。注册路由,路由匹配和执行业务代码都是透过它来完成的。
// HttpServer defines beego application with a new PatternServeMux.
type HttpServer struct {
Handlers *ControllerRegister
Server *http.Server
Cfg *Config
LifeCycleCallbacks []LifeCycleCallback
}
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
...
}
Context 抽象
用户操作请求和响应是通过 Ctx 来达成的。它代表的是整个请求执行过程的上下文。
进一步,Beego 将 Context 细分了几个部分:
- Input:定义了很多和处理请求有关的方法
- Output:定义了很多和响应有关的方法
- Response:对 http.ResponseWriter 的二次封装
// 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
}
func (c *UserController) CreateUser() {
u := &User{}
err := c.Ctx.BindJSON(u)
if err != nil {
c.Ctx.WriteString(err.Error())
return
}
_ = c.Ctx.JSONResp(u)
}
// Controller defines some basic http request handler operations, such as
// http context, template and view, session and xsrf.
type Controller struct {
// context data
Ctx *context.Context
Data map[interface{}]interface{}
...
}
路由树实现
Beego 的核心结构体是三个:
- ControllerRegister:类似于容器,放着所有的路由树
- 路由树是按照 HTTP method 来组织的, 例如 GET 方法会对应有一棵路由树
- Tree:它代表的就是路由树,在 Beego 里面,一棵路由树被看做是由子树组成的
- leafInfo:代表叶子节点
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
routers map[string]*Tree
enablePolicy bool
enableFilter bool
...
}
// 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
}
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 式的定义,而是采用递归式的定义,即一棵树是由根节点+子树
构成。
抽象总结
ControllerRegister 最为基础,它解决了路由注册和路由匹配这个基础问题。
Context 和 Controller 为用户提供了丰富 API,用于辅助构建系统。
HttpServer 作为服务器抽象,用于管理应用生命周期和资源隔离单位。
- ControllerRegister
- Context
- input
- output
- Controller
- HttpServer
- Context
Gin
小案例
用户主动用的 MVC 模式
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
ctrl := &UserController{}
g.GET("/user", ctrl.GetUser)
apiG := g.Group("/api")
v1Api := apiG.Group("/v1")
v1Api.GET("/test", func(context *gin.Context) {
context.String(200, "hello, world, v1")
})
g.Run(":8080")
}
type UserController struct {
}
func (c *UserController) GetUser(ctx *gin.Context) {
ctx.String(200, "hello, world")
}
IRoutes 接口
核心接口 IRoutes:提供的是注册路由的抽象。它的实现类 Engine 类似于 ControllerRegister。
Use 方法提供了用户接入自定义逻辑的能力,这个一般情况下也被看做是插件机制。
还额外提供了静态文件的接口。
Gin 没有 Controller 的抽象。所以在这方面我和 Gin 的倾向比较一致,即 MVC 应该是用户组织 Web 项目的模式, 而不是我们中间件设计者要考虑的。
// 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
StaticFileFS(string, string, http.FileSystem) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
Engine 实现
Engine 可以看做是 Beego 中 HttpServer 和 ControllerRegister 的合体。
- 实现了路由树功能,提供了注册和匹配路由的功能
- 它本身可以作为一个 Handler 传递到 http 包,用于启动服务器
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
Engine 的路由树功能本质上是依赖于 methodTree 的。
methodTrees 和 methodTree
methodTree 才是真实的路由树。
Gin 定义了 methodTrees,它实际上代表的是森林,即每一个 HTTP 方法都对应到一棵树。
type Engine struct {
...
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
// Engine 字段
trees methodTrees
maxParams uint16
maxSections uint16
trustedProxies []string
...
}
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree
HandlerFunc 和 HandlersChain
HandlerFunc 定义了核心抽象 —— 处理逻辑。
在默认情况下,它代表了注册路由的业务代码。
HandlersChain 则是构造了责任链模式。
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
最后一个则是封装了业务逻辑的 HandlerFunc
Context 抽象
Context 也是代表了执行的上下文,提供了丰富的 APl:
- 处理请求的 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
...
}
路由树实现
Gin 的关键结构体更加直观:
- methodTrees:也就是路由树也是按照 HTTP 方法组织的,例如 GET 会有一棵路由树
- methodTree:定义了单棵树。树在 Gin 里面采用的是 children 的定义方式,即树由节点构成(注意对比 Beego)
- node:代表树上的一个节点,里面维持住了 children,即子节点。同时有 nodeType 和 wildChild 来标记一些特殊节点
抽象总结
- methodTree & node
- HandlerFunc
- Context
- Engine
Iris
小案例
package main
import (
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Get("/", func(ctx iris.Context) {
_, _ = ctx.HTML("Hello <strong>%s</strong>!", "World")
})
_ = app.Listen(":8080")
}
Application
Application 是 Iris 的核心抽象,它代表的是应用
。实际上这个语义更加接近 Beego 的 HttpServer和 Gin 的 Engine。
它提供了:
- 生命周期控制功能,如 Shutdown 等方法
- 注册路由的 API
路由相关
lris 的设计非常复杂。在 Beego 和 Gin 里面能够明显看到路由树的痕迹,但是在 lris 里面就很难看出来。
和处理路由相关的三个抽象:
- Route:直接代表了已经注册的路由。在 Beego 和 Gin 里面,对应的是路由树的节点
- APIBuilder:创建 Route 的 Builder 模式,Party 也是它创建的
- repository:存储了所有的 Routes,有点接近 Gin 的 methodTrees 的概念
总结:设计过于复杂,职责不清晰,不符合一般人的直觉,新人学习和维护门槛高。
// repository passed to all parties(subrouters), it's the object witch keeps
// all the routes.
type repository struct {
routes []*Route
pos map[string]int
}
// Route contains the information about a registered Route.
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
Name string `json:"name"` // "userRoute"
Method string `json:"method"` // "GET"
Context 抽象
Context 也是代表上下文。
Context 本身也是提供了各种处理请求和响应的方法。
基本上和 Beego 和 Gin 的 Context 没啥区别。
比较有特色的是它的 Context 支持请求级别的添加 Handler,即 AddHandler 方法。
// AddHandler can add handler(s)
// to the current request in serve-time,
// these handlers are not persistenced to the router.
//
// Router is calling this function to add the route's handler.
// If AddHandler called then the handlers will be inserted
// to the end of the already-defined route's handler.
func (ctx *Context) AddHandler(handlers ...Handler) {
ctx.handlers = append(ctx.handlers, handlers...)
}
抽象总结
- Route Party APIBuilder
- Handler
- Context
- Application
Echo
小案例
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
// Handle
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
Echo
Echo 是它内部的一个结构体,类似于 Beego 的 HttpServer 和 Gin 的 Engine:
- 暴露了注册路由的方法,但是它并不是路由树的载体
- 生命周期管理:如 Shutdown 和 Start 等方法
在 Echo 里面有两个相似的字段:
- Route:这其实就是代表路由树
- Routers:这代表的是根据 Host 来进行分组组织,可以看做是近似于 namespace 之类的概念,既是一种组织方式,也是一种隔离机制
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool
Route 和 node
Router 代表的就是路由树,node 代表的是路由树上的节点。
node 里面有一个很有意思的设计:staticChildren、 paramChild 和 anyChild。利用这种设计可以轻松实现路由优先级和路由冲突检测。
它里面还有一个字段叫做 echo 维护的是使用 Route 的是 echo。这种设计形态在别的地方也能见到,比如说在 sql.Tx 里面维持了一个 sql.DB 的实例。
// Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing.
Router struct {
tree *node
routes map[string]*Route
echo *Echo
}
node struct {
kind kind
label byte
prefix string
parent *node
staticChildren children
originalPath string
methods *routeMethods
paramChild *node
anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes
Context
一个大而全的接口,定义了处理请求和响应的各种方法。
和 Beego、Gin、Iris 的 Context 没有什么区别。
核心抽象
- Route & node
- HandlerFunc
- Context
- Echo
框架对比
所以实际上我们要造一个 Web 框架,就是要建立我们自己的这几个抽象。
Web 框架小结
- Web 框架拿来做什么?处理 HTTP 请求,为用户提供便捷 API,为用户提供无侵入式的插件机制,提供如上传下载等默认功能
- 为什么都已经有了 http 包,还要开发 Web 框架?高级路由功能、封装 HTTP 上下文以提供简单 API、封装 Server 以提供生命周期控制、设计插件机制以提供无侵入式解决方案
- Web 框架的核心?路由树、上下文 Context、 Server(按照我理解的重要性排序)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。