路由规则设计
路由
HTTP请求包含请求头和请求体 请求体一般存放业务数据,请求头存放和请求状态有关的信息,比如User-Agent浏览器信息,Accept支持返回的文本类型
RequestLine Method Method、Request-URI、HTTP-Version
Request-URI 请求路径 浏览器域名外的剩余部分
HTTP-Version 1.0/1.1/2.0
GET /home.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/testpage.html
路由设计和匹配规则
路由设计
REST 业务代码倾向设计REST接口风格
前缀匹配 定制URI倾向于同类型URI归于一类
需求 1:HTTP 方法匹配早期的 WebService 比较简单,HTTP 请求体中的 Request Line 或许只会使用到 Request-URI 部分,但是随着 REST 风格 WebService 的流行,为了让 URI 更具可读性,在现在的路由输入中,HTTP Method 也是很重要的一部分了,所以,我们框架也需要支持多种 HTTP Method,比如 GET、POST、PUT、DELETE。
需求 2:静态路由匹配静态路由匹配是一个路由的基本功能,指的是路由规则中没有可变参数,即路由规则地址是固定的,与 Request-URI 完全匹配。我们提到的 DefaultServerMux 这个路由器,从内部的 map 中直接根据 key 寻找 value ,这种查找路由的方式就是静态路由匹配。
需求 3:批量通用前缀因为业务模块的划分,我们会同时为某个业务模块注册一批路由,所以在路由注册过程中,为了路由的可读性,一般习惯统一定义这批路由的通用前缀。比如 /user/info、/user/login 都是以 /user 开头,很方便使用者了解页面所属模块。所以如果路由有能力统一定义批量的通用前缀,那么在注册路由的过程中,会带来很大的便利。
需求 4:动态路由匹配这个需求是针对需求 2 改进的,因为 URL 中某个字段或者某些字段并不是固定的,是按照一定规则(比如是数字)变化的。那么,我们希望路由也能够支持这个规则,将这个动态变化的路由 URL 匹配出来。所以我们需要,使用自己定义的路由来补充,只支持静态匹配的 DefaultServerMux 默认路由。
需求1+需求2 :
有两个待匹配的规则,Request-URI 和 Method,所以自然联想到可以使用两级哈希表来创建映射。
第一级 hash 是请求 Method,第二级 hash 是 Request-URI。
需求3 批量通用前缀:
通过一个 Group 方法归拢路由前缀地址,处理批量前缀问题
Group 方法,它的参数是一个前缀字符串,返回值应该是包含 Get、Post、Put、Delete 方法的一个结构,我们给这个结构命名 Group,在其中实现各种方法。
这么设计直接返回 Group 结构,确实可以实现功能,但试想一下,随着框架发展,如果我们发现 Group 结构的具体实现并不符合我们的要求了,需要引入实现另一个 Group2 结构,该怎么办?
直接修改 Group 结构的具体实现么?其实更好的办法是使用接口来替代结构定义。在框架设计之初,我们要保证框架使用者,在最少的改动中,就能流畅迁移到 Group2
如果返回接口 IGroup,而不是直接返回 Group 结构,就不需要修改 core.Group 的定义了,只需要修改 core.Group 的具体实现,返回 Group2 就可以。尽量使用接口来解耦合,是一种比较好的设计思路。怎么实现呢,这里我们定义 IGroup 接口来作为 Group 方法的返回值
接口是一种协议,它忽略具体的实现,定义的是两个逻辑结构的交互,因为两个函数之间定义的是一种约定,不依赖具体的实现。你可以这么判断:如果你觉得这个模块是完整的,而且后续希望有扩展的可能性,那么就应该尽量使用接口来替代实现。
多大程度使用接口进行逻辑结构的交互,是评价框架代码可扩展性的一个很好的标准
使用 IGroup 接口后,core.Group 这个方法返回的是一个约定,而不依赖具体的 Group 实现
需求4 动态路由匹配:
一旦引入了动态路由匹配的规则,之前使用的哈希规则就无法使用了。因为有通配符,在匹配 Request-URI 的时候,请求 URI 的某个字符或者某些字符是动态变化的,无法使用 URI 做为 key 来匹配。那么,我们就需要其他的算法来支持路由匹配
这个问题本质是一个字符串匹配,而字符串匹配,比较通用的高效方法就是字典树,也叫 trie 树。这里,我们先简单梳理下 trie 树的数据结构。trie 树不同于二叉树,它是多叉的树形结构,根节点一般是空字符串,而叶子节点保存的通常是字符串,一个节点的所有子孙节点都有相同的字符串前缀。
这个 trie 树是按照路由地址的每个段 (segment) 来切分的,每个 segment 在 trie 树中都能找到对应节点,每个节点保存一个 segment。树中,每个叶子节点都代表一个 URI,对于中间节点来说,有的中间节点代表一个 URI(比如上图中的 /subject/name),而有的中间节点并不是一个 URI(因为没有路由规则对应这个 URI)
可以用 matchNode 方法,寻找某个路由在 trie 树中匹配的节点,如果有匹配节点,返回节点指针,否则返回 nil。matchNode 方法的参数是一个 URI,返回值是指向 node 的指针,它的实现思路是使用函数递归
修改文件
定义路由 map
两级Map处理REST风格+Request-URI
注册路由
这里将 URL 全部转换为大写了,在后续匹配路由的时候,也要记得把匹配的 URL 进行大写转换,这样我们的路由就会是“大小写不敏感”的,对使用者的容错性就大大增加了
匹配路由
填充 ServeHTTP 方法