封装了框架的 Context, 将请求结构 request 和返回结构 responseWriter 都封装在 Context 中。利用这个 Context, 我们将控制器简化为带有一个参数的函数 FooControllerHandler,这个控制器函数的输入和输出都是固定的。在框架层面,我们也定义了对应关于控制器的方法结构 ControllerHandler 来代表这类控制器的函数。
每一个请求逻辑,都有一个控制器 ControllerHandler 与之对应。那么一个请求,如何查找到指定的控制器呢?
路由
路由的功能,具体来说就是让 Web 服务器根据规则,理解 HTTP 请求中的信息,匹配查找出对应的控制器,再将请求传递给控制器执行业务逻辑,简单来说就是制定匹配规则。
一个 HTTP 请求包含请求头和请求体。请求体内一般存放的是请求的业务数据,是基于具体控制业务需要的,所以,我们不会用来做路由。
而请求头中存放的是和请求状态有关的信息,比如 User-Agent 代表的是请求的浏览器信息,Accept 代表的是支持返回的文本类型。以下是一个标准请求头的示例:
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
每一行的信息和含义都是非常大的课题, 这里重点讲第一行,叫做Request Line,由Method、Request-URI 和 HTTP-Version三部分组成:
Method 是 HTTP 的方法,标识对服务端资源的操作属性。
Method = "OPTIONS" ; Section 9.2
| "GET" ; Section 9.3
| "HEAD" ; Section 9.4
| "POST" ; Section 9.5
| "PUT" ; Section 9.6
| "DELETE" ; Section 9.7
| "TRACE" ; Section 9.8
| "CONNECT" ; Section 9.9
| extension-method
extension-method = token
Request-URI 是请求路径,也就是浏览器请求地址中域名外的剩余部分。
HTTP-Version 是 HTTP 的协议版本,目前常见的有 1.0、1.1、2.0。
Web Service 在路由中使用的就是 Method 和 Request-URI 这两个部分。如果框架支持 REST 风格的路由设计,那么使用者在写业务代码的时候,就倾向于设计 REST 风格的接口;如果框架支持前缀匹配,那么使用者在定制 URI 的时候,也会倾向于把同类型的 URI 归为一类。
这些设计想法通通会体现在框架的路由规则上,最终影响框架使用者的研发习惯,这个就是设计感。
路由规则的需求
我们希望使用者高效、易用地使用路由模块,那出于这一点考虑,基本需求可以有哪些呢?
按照从简单到复杂排序,路由需求有下面四点:
-
HTTP 方法匹配
早期简单的WebService只用到Request-URI部分,随着REST风格的流行,也需要支持多种HTTP Method -
静态路由匹配
静态路由匹配是一个路由的基本功能,指的是路由规则中没有可变参数,即路由规则地址是固定的,与 Request-URI 完全匹配。
net/http包中的DefaultServerMux 这个路由器,从内部的 map 中直接根据 key 寻找 value ,这种查找路由的方式就是静态路由匹配。 -
批量通用前缀
为某个业务模块注册一批路由,比如/user/info、 /user/login都是以/user开头。
所以如果路由有能力统一定义批量的通用前缀,那么在注册路由的过程中,会带来很大的便利。 -
动态路由匹配
针对静态路由改进,因为URL中某些字段并不是固定的,而是按照一定规则(如数字)变化。
功能 | Request-URI | HTTP Method |
---|---|---|
用户登录 | /user/login | POST |
增加专题 | /subject/add | POST |
删除专题 | /subject/1 | DELETE |
修改专题 | /subject/1 | PUT |
查找专题 | /subject/1 | GET |
获取专题列表 | /subject/list | GET |
那么该如何实现这几个需求呢?
需求实现
实现 HTTP 方法和静态路由匹配
对于第一、二个需求,很容易想到使用两层哈希表实现:
func NewCore() *Core {
getRouter := map[string]ControllerHandler{}
postRouter := map[string]ControllerHandler{}
putRouter := map[string]ControllerHandler{}
deleteRouter := map[string]ControllerHandler{}
router := map[string]map[string]ControllerHandler{}
router["GET"] = getRouter
router["POST"] = postRouter
router["PUT"] = putRouter
router["DELETE"] = deleteRouter
return &Core{router: router}
}
// 对应 Method = Get
func (c *Core) Get(url string, handler ControllerHandler) {
upperUrl := strings.ToUpper(url)
c.router["GET"][upperUrl] = handler
}
// 匹配路由,如果没有匹配到,返回nil
func (c *Core) FindRouteByRequest(request *http.Request) ControllerHandler {
// uri 和 method 全部转换为大写,保证大小写不敏感
uri := request.URL.Path
method := request.Method
upperMethod := strings.ToUpper(method)
upperUri := strings.ToUpper(uri)
// 查找第一层map
if methodHandlers, ok := c.router[upperMethod]; ok {
// 查找第二层map
if handler, ok := methodHandlers[upperUri]; ok {
return handler
}
}
return nil
}
func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(r, w)
router := c.FindRouteByRequest(r)
if router == nil {
ctx.Json(404, "not found")
return
}
if err := router(ctx); err != nil {
ctx.Json(500, "inner error")
return
}
}
批量实现通用前缀
批量通用前缀,类似下面这样使用:
// 注册路由规则
func registerRouter(core *framework.Core) {
// 需求3:批量通用前缀
subjectApi := core.Group("/subject")
{
subjectApi.Get("/list", SubjectListController)
}
}
这里core.Group方法接收一个字符串前缀,返回的是一个包含Get、Post、Put、Delete方法的结构,我们将其命名为Group,因为是一个实现了各种方法的结构,因此我们这里采用接口定义IGroup:
// IGroup 代表前缀分组
type IGroup interface {
Get(string, ControllerHandler)
Post(string, ControllerHandler)
Put(string, ControllerHandler)
Delete(string, ControllerHandler)
}
// Group struct 实现了IGroup
type Group struct {
core *Core
prefix string
}
// 初始化Group
func NewGroup(core *Core, prefix string) *Group {
return &Group{
core: core,
prefix: prefix,
}
}
// 实现Get方法
func (g *Group) Get(uri string, handler ControllerHandler) {
uri = g.prefix + uri
g.core.Get(uri, handler)
}
....
// 从core中初始化这个Group
func (c *Core) Group(prefix string) IGroup {
return NewGroup(c, prefix)
}
实现动态路由
希望实现如下所示的动态路由:
func registerRouter(core *framework.Core) {
// 需求1+2:HTTP方法+静态路由匹配
core.Get("/user/login", UserLoginController)
// 需求3:批量通用前缀
subjectApi := core.Group("/subject")
{
// 需求4:动态路由
subjectApi.Delete("/:id", SubjectDelController)
subjectApi.Put("/:id", SubjectUpdateController)
subjectApi.Get("/:id", SubjectGetController)
subjectApi.Get("/list/all", SubjectListController)
}
}
引入动态路由,则之前的哈希规则无法使用了,因为字符是动态变化的,无法使用URI作为key来匹配。
这个问题本质是一个字符串匹配