写点什么

「Go 框架」深入理解 iris 框架的路由底层结构

作者:Go学堂
  • 2023-01-15
    北京
  • 本文字数:4615 字

    阅读完需:约 15 分钟

「Go框架」深入理解iris框架的路由底层结构

大家好,我是渔夫子。本号新推出「Go 工具箱」系列,意在给大家分享使用 go 语言编写的、实用的、好玩的工具。同时了解其底层的实现原理,以便更深入地了解 Go 语言。


iris 框架号称是最快的 web 框架。今天就来深入的研究下 iris 框架路由的底层实现原理。


那为什么需要深入了解 web 框架的路由呢?路由是 web 框架的核心。在业务开发中,我们在使用框架时,基本就是在注册路由、使用中间件、然后写对应的业务逻辑。那么注册路由、使用中间件都跟路由实现有关。所以,理解了一个 web 框架的路由底层实现逻辑,基本也就掌握了该框架的实现原理。

一、iris 的基本使用

我们先来看下使用 iris 框架如何注册路由以及启动服务。如下代码:


package main
import ( "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 路由实现原理

2.1 从 iris.New 说起

首先,我们看iris.New函数的作用。该函数就是创建了一个Application结构体的实例 app。然后后面的操作都是基于该实例 app 进行的操作。下面是该 Application 结构体的主要字段,这里只列出了和路由相关的主要字段,忽略其他字段。



在 Application 的字段中,从名字上看有两个字段是和路由相关的:router.APIBuilderrouter.Router。那我们接着再分别看下这两个结构体的主要构成。


如下是router.APIBuilder结构体的主要字段及其相关联的结构体:



router.APIBuilder结构体及其相关的reporitoryRoute结构体可以看到,这里包含了路由的相关信息。例如,在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中的路由再转换成基于前缀树结构的路由表,以提高检索的速度。这个咱们在启动服务部分再仔细讲解。


接下来我们看路由注册部分。

2.2 路由注册

实例化完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 的路由分固定路由、正则路由。同时还支持路由分组、子域名路由等。

2.2.1 固定路由

固定路由也叫全匹配路由。像app.Get("/home", HomeHandler)就是一个固定路由。也就是只有 "/home" 路径才能匹配到HomeHandler处理器。以下是该路由最终构建的route结构体如下:


2.2.2 正则路由

正则路由就是在路径中可以指定正则表达式,只要符合该正则表达式的路径都可以匹配到该路径及对应的请求处理函数。比如定义如下路由:


app.Get("/home/{username:string}", HomeHandler)
复制代码


路径的中的{username:string}部分,其中花括号{ } 代表是正则部分。username是占位符,说明这部分可以通过username名字获取到具体的参数值。另外的string是限定了username的类型是字符串。当然,iris 框架中共计包含 20 个这样的类型,称为微指令。在源文件中 iris/macro/macros.go 中的 Defaults 变量列表,有兴趣可以继续深入研究。


路径 "/home/yufuzi""/home/goxuetang"等都可以匹配到该路由。因为在路由中指定了usernamestring类型,所以路径中的这部分都作为字符串类型看待。指定类型的另外一个作用就是在路由匹配中对路径的这部分内容做对应的类型校验。比如app.Get("/home/{username:email}"),那么路径中的这部分 username 就必须是一个邮件格式,否则就匹配不到该路径。


这里的username只是参数的变量名,可以通过ctx.Params().Get("username"))� 来获取具体的参数值。


那么,该路径生成的对应的路由对象如下:



我们看到红色部分是和第一个路由的主要区别。这里主要关注 Tmp 字段,发现 Tmp 字段中 Param 有了对应的指令,这里是"string"

2.2.3 路由分组

对路由进行分组也是在路由注册时常用的路由注册方法。在 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

2.2.4 子域名路由

在 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 是如何基于上述的路由表将路由编译成基于前缀树结构的。

2.3 基于前缀树结构的路由表

为了提高路由的匹配效率,大多数框架都基于前缀树结构构件路由表。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 框架是在注册过程中就直接生成了树形结构。以上希望对大家有所帮助。


---特别推荐---

特别推荐:一个专注 go 项目实战、项目中踩坑经验及避坑指南、各种好玩的 go 工具的公众号,「Go 学堂」,专注实用性,非常值得大家关注。点击下方公众号卡片,直接关注。关注送《100 个 go 常见的错误》pdf 文档。

发布于: 刚刚阅读数: 3
用户头像

Go学堂

关注

关注「Go学堂」,学习更多编程知识 2019-08-06 加入

专注Go编程知识、案例、常见错误及原理分析。意在通过阅读更多优秀的代码,提高编程技能。同名公众号「Go学堂」期待你的关注

评论

发布
暂无评论
「Go框架」深入理解iris框架的路由底层结构_golang_Go学堂_InfoQ写作社区