iris框架号称是最快的web框架。今天就来深入研究下iris框架路由的底层实现原理。
那为什么需要深入了解web框架的路由呢?路由是web框架的核心。在业务开发中,我们在使用框架时,基本就是在注册路由、使用中间件、然后写对应的业务逻辑。那么注册路由、使用中间件都跟路由实现有关。所以,理解了一个web框架的路由底层实现逻辑,基本也就掌握了该框架的实现原理。
我们先来看下使用iris框架如何注册路由以及启动服务。如下代码:
package mainimport (
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Get("/home", Home)
app.Listen(":8080")
}
func Home(ctx iris.Context) {
ctx.Write([]byte("Hi, this is iris home"))
}
代码很简单,最基本的使用有三步:通过iris.New()构建一个iris的application、注册路由、启动服务。在浏览器中输入http://localhost:8080/home
,即可输出 "Hi, this is iris home"
。
首先,我们看iris.New
函数的作用。该函数就是创建了一个Application
结构体的实例 app
。然后后面的操作都是基于该实例 app
进行的操作。下面是该Application结构体的主要字段,这里只列出了和路由相关的主要字段,忽略其他字段。在Application的字段中,从名字上看有两个字段是和路由相关的:router.APIBuilder
和 router.Router
。那我们接着再分别看下这两个结构体的主要构成。
如下是router.APIBuilder
结构体的主要字段及其相关联的结构体:
从router.APIBuilder
结构体及其相关的reporitory
和Route
结构体可以看到,这里包含了路由的相关信息。例如,在repository
中的routes
字段,代表所有的路由,可以简单理解为一个简易的路由表。在Route
结构体中包含了请求方法Method
字段、请求路径字段Path
、对应的请求处理函数Handlers
字段。其中还有macro.Template
类型的Tmp
字段是针对正则路由的正则表达式。
在Application
结构体中还有一个字段是router.Router
字段,看名字这个是路由表,但实际上该字段中没有包含任何路由相关的信息。我们通过该结构体相关的方法列表可以发现,该结构体中有一个ServeHTTP
方法,在web
框架的请求流程一文中我们讲解过该方法是go
中处理HTTP
请求的入口方法。如下:
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
router.mainHandler(w, r)
}
也就是说,router.Router
主要功能是iris
框架处理http
请求的入口,核心是基于路由表进行路由匹配,并执行对应的请求处理函数。以下是router.Router
结构体的主要字段:
最后,我们看下通过iris.New
构造的Application
对象包含了存储路由相关的信息。如下:
由此可见,通过iris.New
构建的Application
对象实际上包含了处理请求的router.Router
以及管理路由的信息router.APIBuiler
。后续的路由注册以及启动服务都是基于这个Application
对象的。
当然,这里的router.APIBuilder
中的routes
并非是最终的路由表。在iris
中,会在服务的启动阶段,即app.Run
函数中将APIBuilder.routes
中的路由再转换成基于前缀树结构的路由表,以提高检索的速度。这个咱们在启动服务部分再仔细讲解。
接下来我们看路由注册部分。
实例化完Application
对象,接着就是路由注册了。也就是类似下面的代码:
app := iris.New()
app.Get("/home", HomeHandler)// 这里就是按照iris的路由规则定义的请求处理函数
func HomeHandler(ctx iris.Context) {
ctx.Write([]byte("Hi, this is iris home"))
}
我们主要看app.Get("/home", HomeHandler)
这个函数的实现。进入该Get
函数的源码,发现调用者是APIBuilder
结构体,如下:
func (api *APIBuilder) Get(relativePath string, handlers ...context.Handler) *Route {
return api.Handle(http.MethodGet, relativePath, handlers...)
}
这是因为在Application
结构体中嵌套了router.APIBuilder
结构体,所以Application
自然也就嵌套了APIBuilder
结构体的所有方法。
在Get
的这个方法中,我们看第二个参数handlers
的类型是context.Handler
,其定义如下是 type Handler func(*Context)�
,这就是为什么我们把HomeHandler
定义这种类型的原因。本质上也可以说没有为什么,就是iris框架这么规定的。
我们再接着源代码往下看,会看到如下代码,根据请求的方法、路径以及请求处理函数创建一个路由对象,然后将该路由对象加入到APIBuilder
的路由表routes
中。
如下是对应的源码:
func (api *APIBuilder) handle(errorCode int, method string, relativePath string, handlers ...context.Handler) *Route {
// 创建路由,返回来的是一个路由数组
// 因为传递的请求方法也是一个数据,一个请求方法对应一个路由,
// 所有返回的routes就是数组。这里method只有一个方法,所以routes数组就只有一个元素
routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...) var route *Route // the last one is returned.
var err error
for _, route = range routes {
if route == nil {
continue
}
// global
route.topLink = api.routes.getRelative(route)
// 将route添加到路由表
if route, err = api.routes.register(route, api.routeRegisterRule); err != nil {
api.logger.Error(err)
break
}
}
return route
}
在第 18 行中,api.routes.register
方法就是将路由加入到路由切片中的操作。只不过里面包含了一些对路由进行去重的逻辑。本质上就是append(api.routes, route)
操作。
咱们重点看下创建路由的过程。iris的路由分固定路由、正则路由。同时还支持路由分组、子域名路由等。
固定路由也叫全匹配路由。像app.Get("/home", HomeHandler)
就是一个固定路由。也就是只有 "/home"
路径才能匹配到HomeHandler
处理器。以下是该路由最终构建的route
结构体如下:
正则路由就是在路径中可以指定正则表达式,只要符合该正则表达式的路径都可以匹配到该路径及对应的请求处理函数。比如定义如下路由:
app.Get("/home/{username:string}", HomeHandler)
路径的中的{username:string}
部分,其中花括号{ }
代表是正则部分。username
是占位符,说明这部分可以通过username
名字获取到具体的参数值。另外的string
是限定了username
的类型是字符串。当然,iris框架中共计包含20个这样的类型,称为微指令。在源文件中iris/macro/macros.go中的Defaults变量列表,有兴趣可以继续深入研究。
路径 "/home/yufuzi"
,"/home/goxuetang"
等都可以匹配到该路由。因为在路由中指定了username
为string
类型,所以路径中的这部分都作为字符串类型看待。指定类型的另外一个作用就是在路由匹配中对路径的这部分内容做对应的类型校验。比如app.Get("/home/{username:email}")
,那么路径中的这部分username就必须是一个邮件格式,否则就匹配不到该路径。
这里的username
只是参数的变量名,可以通过ctx.Params().Get("username"))�
来获取具体的参数值。
那么,该路径生成的对应的路由对象如下:
我们看到红色部分是和第一个路由的主要区别。这里主要关注Tmp字段,发现Tmp字段中Param有了对应的指令,这里是"string"
。
对路由进行分组也是在路由注册时常用的路由注册方法。在iris中使用以下代码对路由进行分组:
app := iris.New() // user分组
userGroup := app.Party("/user")
userGroup.Get("/login", Home) //最终的路径实际上是/user/login
这里通过使用app.Party
方法对路由进行了分组。在上文咱们说过,Party方法实际上返回的是一个APIBuilder对象。大家还记得吗,app
里也是嵌套了APIBuilder
结构的,那么app.Party
实际上是给app
中的APIBuilder
创建了一个子APIBuilder
对象,同时给子APIBuilder
中的relativePath
设置成了 "/user"
。也就是通过该子APIBuilder
对象注册的路由,路径都是相对于relativePath
的,即 "/user"
设置的。如下是APIBuilder中的父子关系:
当然,每个分组的APIBuilder
中还可以设置自己的中间件函数。这也就实现了针对不同的分组使用不同的中间。
接下来,我们再看看针对 "/user"
分组设置的"/login"
生成的路由结构体。如下:这里主要的区别就是路由中的Party
字段指向不一样。这里的Party
字段指向的是分组的APIBuilder
。
在iris框架中,还支持子域名路由。通过以下方式就可以支持子域名路由:
adminDomain := app.Subdomain("admin")
adminDomain.Get("/home", Home)
通过app.Subdomain
函数就可以指定子域名。Subdomain
的实现其实还是调用了APIBuilder.Party
函数。所以本质上也是一个分组。只不过是按子域名进行分组的。如下是通过app.Subdomain("admin")
生成的APIBuilder
的结构体实例:
通过Subdomain
函数生成的依然是一个APIBuilder
实例,只不过该实例中relativePath
的值是子域名的值而已。
那么,adminDomain.Get("/home", Home)
就是相对于子域名分组下生成的路由,其对应的Route
实例如下:
这里可以看到,在Route
结构体的Subdomain
字段中,有了具体的子域名的值。其他字段和普通的路由是一致的。
iris框架中注册的路由,最终都是基于Route
结构体的,其他更多的特性也是这样。但这里还并不是最终的路由,因为我们知道如果每次请求是基于该切片进行搜索匹配路由的话,那效率就极低了。
接下来我们看iris.Run
函数中,iris是如何基于上述的路由表将路由编译成基于前缀树结构的。
为了提高路由的匹配效率,大多数框架都基于前缀树结构构建的路由表。iris
框架也不例外。但是,iris
框架是在服务启动阶段才对已注册的路由进行转换的,即在iris.Run
函数中。
在前缀树路由结构中,子域名和请求方法唯一确定一棵树。也就是子域名相同且方法也相同,则在同一个树结构下。以下是前缀树路由表的大体数据结构及核心字段说明:
我们以下面三个路由为例,来看看最终生成的路由前缀树。
app.Get("/home", Home)
app.Get("/home/{userid:int}", Home) adminDomain := app.Subdomain("admin")
adminDomain.Get("/home", Home)
adminDomain.Get("/home/{userid:int}", Home)
根据上面刚分析过的,请求方法method
和子域名subdomain
两者唯一确定一棵树。即同样method和同样的subdomain的路由在同一棵树下。所以,上面的路由就有两棵树:Get方法+空子域名
和Get方法+admin子域名
。同时我们看到,在每一棵树中都有共同的前缀/home
,所以会形成home->{userid:int}
这样的父子关系。
以下是最终生成的前缀树路由:
上面图看着挺多,其实很简单,就是通过trieNode
中的children
字段组成的一个属性结构,同时通过parent
指向父节点。
本文通过从iris的启动,到路由注册以及转换成基于前缀树结构的路由表三个方面讲述了iris路由的生成过程。iris路由表的生成和其他web框架不同的是在app.Run
阶段才生成,而其他web框架是在注册过程中就直接生成了树形结构。以上希望对大家有所帮助。
推荐阅读