dimfeld/httptreemux

GitHub: dimfeld/httptreemux

一个基于 Patricia 树的 Go 语言高速 HTTP 路由器,在性能接近 httprouter 的同时提供了更灵活的路由模式匹配规则。

Stars: 618 | Forks: 59

# httptreemux [![Build Status](https://travis-ci.org/dimfeld/httptreemux.png?branch=master)](https://travis-ci.org/dimfeld/httptreemux) [![GoDoc](https://godoc.org/github.com/dimfeld/httptreemux?status.svg)](https://godoc.org/github.com/dimfeld/httptreemux) 高速、灵活、基于树的 Go 语言 HTTP 路由器。 它的灵感来源于 [Julien Schmidt 的 httprouter](https://www.github.com/julienschmidt/httprouter),因为它同样使用了 patricia 树,但实现方式截然不同。具体来说,路由规则被放宽了,使得单个路径段在某条路由中可以是通配符,而在另一条路由中可以是静态 token。这在高性能与路由模式设计的极大便利性之间实现了绝佳的结合。在[基准测试](https://github.com/julienschmidt/go-http-routing-benchmark)中,httptreemux 的表现接近但略慢于 httprouter。 发布说明可以在 [Github releases 标签页](https://github.com/dimfeld/httptreemux/releases)中找到。版本号遵循 [Semantic Versioning 2.0.0](http://semver.org/) 规范,每次代码更改后都会发布一个新版本。 ## 使用 Go Modules 安装 使用 Go Modules 时,请通过 `import "github.com/dimfeld/httptreemux/v5"` 导入此仓库,以确保获取正确的版本。 ## 为什么开发这个路由器? 市面上已经有很多优秀的路由器了。但在考察了那些真正轻量级的路由器后,我发现找不到完全符合我所需路由模式的东西。代码本身很简单,所以我花了一个晚上写了这个。 ## Handler handler 是一个简单的函数,其原型为 `func(w http.ResponseWriter, r *http.Request, params map[string]string)`。params 参数包含了从 URL 中的通配符和 catch-all 解析出来的参数,如下所述。此类型被别名为 httptreemux.HandlerFunc。 ### 使用 http.HandlerFunc 由于 Go 1.7 引入了 [context](https://godoc.org/context) 包,`httptreemux` 现在支持 [http.HandlerFunc](https://godoc.org/net/http#HandlerFunc) 类型的 handler。有两种方法可以启用此支持。 #### 适配现有的 Router `UsingContext` 方法会将 router 或 group 包装在相同路径下的一个新 group 中,但会进行适配以供 `context` 和 `http.HandlerFunc` 使用。 ``` router := httptreemux.New() group := router.NewGroup("/api") group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) { id := params["id"] fmt.Fprintf(w, "GET /api/v1/%s", id) }) // UsingContext returns a version of the router or group with context support. ctxGroup := group.UsingContext() // sibling to 'group' node in tree ctxGroup.GET("/v2/:id", func(w http.ResponseWriter, r *http.Request) { ctxData := httptreemux.ContextData(r.Context()) params := ctxData.Params() id := params["id"] // Useful for middleware to see which route was hit without dealing with wildcards routePath := ctxData.Route() // Prints GET /api/v2/:id id=... fmt.Fprintf(w, "GET %s id=%s", routePath, id) }) http.ListenAndServe(":8080", router) ``` #### 带有 Context 支持的新 Router `NewContextMux` 函数返回一个预配置为可使用 `context` 和 `http.HandlerFunc` 的 router。 ``` router := httptreemux.NewContextMux() router.GET("/:page", func(w http.ResponseWriter, r *http.Request) { params := httptreemux.ContextParams(r.Context()) fmt.Fprintf(w, "GET /%s", params["page"]) }) group := router.NewGroup("/api") group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request) { ctxData := httptreemux.ContextData(r.Context()) params := ctxData.Params() id := params["id"] // Useful for middleware to see which route was hit without dealing with wildcards routePath := ctxData.Route() // Prints GET /api/v1/:id id=... fmt.Fprintf(w, "GET %s id=%s", routePath, id) }) http.ListenAndServe(":8080", router) ``` ## 路由规则 这里的语法也是模仿 httprouter 的。路径中的每个变量只能匹配一个段,但在 URL 末尾的 catch-all 变量除外。 以下是一些有效的 URL 模式示例: * `/post/all` * `/post/:postid` * `/post/:postid/page/:page` * `/post/:postid/:page` * `/images/*path` * `/favicon.ico` * `/:year/:month/` * `/:year/:month/:post` * `/:page` 请注意,上述所有 URL 模式可以同时存在于 router 中。 以 `:` 开头的路径元素表示路径中的通配符。通配符只会匹配单个路径段。也就是说,模式 `/post/:postid` 会匹配 `/post/1` 或 `/post/1/`,但不会匹配 `/post/1/2`。 以 `*` 开头的路径元素是 catch-all,其值将是一个字符串,包含 URL 中由通配符匹配到的所有文本。例如,如果模式为 `/images/*path` 且请求的 URL 为 `images/abc/def`,则 path 将包含 `abc/def`。catch-all 路径不会匹配空字符串,因此在此示例中,如果您还想匹配 `/images/`,则需要注册一个单独的路由。 #### 在路由模式中使用 : 和 * 通过使用反斜杠进行转义,可以在路径段的开头使用 `:` 和 `*` 字符。在段开头的双反斜杠会被解析为单个反斜杠。这些转义仅在路径段的最开头进行检查;在 token 的其他地方不需要,也不会被处理。 ``` router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken router.GET("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle router.GET("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\* router.GET("/foo/\\\\*backslashWithStar") // matches /foo/\*backslashWithStar ``` ### 路由组 (Routing Groups) 允许您创建具有给定路径前缀的新路由组。这让创建以下路径集群变得更加容易: * `/api/v1/foo` * `/api/v1/bar` 要使用此功能,您可以这样做: ``` router = httptreemux.New() api := router.NewGroup("/api/v1") api.GET("/foo", fooHandler) // becomes /api/v1/foo api.GET("/bar", barHandler) // becomes /api/v1/bar ``` ### 路由优先级 router 中的优先级规则很简单。 1. 静态路径段具有最高优先级。如果某个段及其子树能够匹配该 URL,则返回该匹配。 2. 通配符具有第二优先级。要让特定的通配符匹配,该通配符及其子树必须与 URL 匹配。 3. 最后,当之前的路径段已经匹配,且没有静态或通配符条件匹配时,catch-all 规则才会进行匹配。catch-all 规则必须位于模式的末尾。 因此,对于改编自 [simpleblog](https://www.github.com/dimfeld/simpleblog) 的以下模式,我们将看到特定的匹配结果: ``` router = httptreemux.New() router.GET("/:page", pageHandler) router.GET("/:year/:month/:post", postHandler) router.GET("/:year/:month", archiveHandler) router.GET("/images/*path", staticHandler) router.GET("/favicon.ico", staticHandler) ``` #### 示例场景 - `/abc` 将匹配 `/:page` - `/2014/05` 将匹配 `/:year/:month` - `/2014/05/really-great-blog-post` 将匹配 `/:year/:month/:post` - `/images/CoolImage.gif` 将匹配 `/images/*path` - `/images/2014/05/MayImage.jpg` 也将匹配 `/images/*path`,其中 `/images` 之后的所有文本都将存储在变量 path 中。 - `/favicon.ico` 将匹配 `/favicon.ico` ### 特殊方法行为 如果 TreeMux.HeadCanUseGet 设置为 true,则在处理 HEAD 请求时,如果该模式未添加 HEAD handler,router 将调用该模式的 GET handler。此行为默认处于启用状态。 Go 的 http.ServeContent 及相关函数已经通过仅发送 header 正确处理了 HEAD 方法,因此在大多数情况下,您的 handler 不需要为其做任何特殊处理。 默认情况下,TreeMux.OptionsHandler 是一个空 handler,它不会影响您的路由。如果您设置了这个 handler,它将在对已由其他方法注册的路径发起 OPTIONS 请求时被调用。如果您使用 `router.OPTIONS` 设置了特定于路径的 handler,它将覆盖该路径的全局 Options Handler。 ### 尾部斜杠 router 对带有尾部斜杠的路径有特殊处理。如果向 router 添加带有尾部斜杠的模式,则任何对该模式且不带尾部斜杠的匹配都会被重定向到带斜杠的版本。如果模式不带尾部斜杠,则对该模式且带尾部斜杠的匹配会被重定向到不带斜杠的版本。 尾部斜杠标志对于单个模式仅存储一次。也就是说,如果为某个带有尾部斜杠的方法添加了模式,那么无论其他方法是否指定了尾部斜杠,该模式下的所有其他方法也将被视为带有尾部斜杠。 但是,可以通过将 TreeMux.RedirectTrailingSlash 设置为 false 来关闭此行为。默认情况下它被设置为 true。 此规则的一个例外是 catch-all 模式。默认情况下,catch-all 模式会禁用尾部斜杠重定向,因为无法预测整个 URL 的结构和所需的模式。如果希望在 catch-all 模式中移除尾部斜杠,请将 TreeMux.RemoveCatchAllTrailingSlash 设置为 true。 ``` router = httptreemux.New() router.GET("/about", pageHandler) router.GET("/posts/", postIndexHandler) router.POST("/posts", postFormHandler) GET /about will match normally. GET /about/ will redirect to /about. GET /posts will redirect to /posts/. GET /posts/ will match normally. POST /posts will redirect to /posts/, because the GET method used a trailing slash. ``` ### 自定义重定向 RedirectBehavior 用于设置 router 在使用 RedirectTrailingSlash 或 RedirectClean 将请求重定向到所请求 URL 的规范版本时的行为。默认行为是返回 301 状态码,将浏览器重定向到与给定模式匹配的 URL 版本。 这些是 RedirectBehavior 接受的值。您还可以将这些值添加到 RedirectMethodBehavior 映射中,以定义基于方法的自定义重定向行为。 * Redirect301 - HTTP 301 Moved Permanently;这是默认值。 * Redirect307 - HTTP/1.1 Temporary Redirect * Redirect308 - RFC7538 Permanent Redirect * UseHandler - 不重定向到规范路径。而是直接调用 handler。 ### 不区分大小写的路由 您可以通过将 router 的 _CaseInsensitive_ 属性设置为 true,来选择性地启用不区分大小写的路由。 这允许您使所有路由都不区分大小写。例如: ``` router := httptreemux.New() router.CaseInsensitive router.GET("/My-RoUtE", pageHandler) ``` 在这个示例中,对 /my-route 发起 GET 请求将匹配到该路由并执行 _pageHandler_ 的功能。 需要注意的是,当使用不区分大小写的路由时,必须在定义路由之前设置 CaseInsensitive 属性,否则可能会产生意外的副作用。 #### 原理/用法 在 POST 请求中,大多数接收到 301 状态码的浏览器会向重定向后的 URL 发起 GET 请求,这意味着任何数据都可能丢失。如果您想处理并避免这种行为,您可以使用 Redirect307,它会促使大多数浏览器使用原始方法和请求体重新提交请求。 由于 307 应该是一个临时重定向,因此人们提出了新的 308 状态码。它的处理方式相同,但它能正确指示该重定向是永久性的。这里最大的警告是,该 RFC 相对较新,旧版或不兼容的浏览器可能无法处理它。因此,除非您真的清楚自己在做什么,否则不建议使用它。 最后,UseHandler 值只会简单地调用该模式的 handler 函数,而不会重定向到 URL 的规范版本。 ### RequestURI 与 URL.Path #### 转义的斜杠 Go 会自动处理 URL 中的转义字符,将 + 转换为空格,并将 %XX 转换为相应的字符。当 URL 中包含 %2f(它会被反转义为 '/')时,这可能会引发问题。这对大多数应用来说不是问题,但它会阻止 router 正确匹配路径和通配符。 例如,模式 `/post/:post` 不会匹配 `/post/abc%2fdef`,因为后者会被反转义为 `/post/abc/def`。我们期望的行为是它能匹配成功,并且 `post` 通配符被设置为 `abc/def`。 因此,此 router 默认使用存储在 Request.RequestURI 变量中的原始 URL。匹配到的通配符和 catch-all 随后会被反转义,以提供预期的行为。 长话短说:如果请求的 URL 包含 %2f,此 router 依然会采取正确的处理方式。由于 [Go issue 3659](https://code.google.com/p/go/issues/detail?id=3659) 的问题,某些 Go HTTP router 可能无法做到这一点。 #### 转义字符 如上所述,当使用 RequestURI 确定匹配的路由时,URL 中的字符不会被反转义。如果这对您来说是个问题,并且由于上述原因您无法切换到 URL.Path,您可以将 `router.EscapeAddedRoutes` 设置为 `true`。此选项会通过 `URL.EscapedPath` 函数处理每个添加的路由,并在转义版本不同时添加一个额外的路由。 #### http 包的实用工具函数 虽然使用 RequestURI 避免了上述问题,但某些实用工具函数(例如 `http.StripPrefix`)会修改 URL.Path,并期望底层 router 使用该字段来做出决策。如果您正在使用这些函数,请将 router 的 `PathSource` 成员设置为 `URLPath`。这将放弃上述对转义斜杠的正确处理,但能让 router 与这些实用工具函数正常配合工作。 ## 并发 router 包含一个用于仲裁对树访问的 `RWMutex`。这允许同时安全地从多个 goroutine 中添加路由。 当仅从树中读取数据时,不需要并发控制,因此默认行为是在处理请求时不使用 `RWMutex`。这避免了在 `RWMutex` 内部竞争原子整数操作而在高使用率场景下导致的理论性能下降。如果您的应用程序在开始处理请求后向 router 添加路由,您应该通过将 `router.SafeAddRoutesWhileRunning` 设置为 `true` 来避免潜在的竞态条件,以便在处理请求时使用 `RWMutex`。 ## 错误处理器 ### NotFoundHandler 可以设置 TreeMux.NotFoundHandler 来提供自定义的 404 错误处理。默认实现是 Go 的 `http.NotFound` 函数。 ### MethodNotAllowedHandler 如果某个模式匹配成功,但该模式没有与请求方法关联的 handler,router 将调用 MethodNotAllowedHandler。此 handler 的默认版本仅写入状态码 `http.StatusMethodNotAllowed`,并适当地设置响应 header 的 `Allowed` 字段。 ### Panic 处理 可以设置 TreeMux.PanicHandler 以提供自定义的 panic 处理。`SimplePanicHandler` 仅写入状态码 `http.StatusInternalServerError`。改编自 [gocraft/web](https://github.com/gocraft/web) 的 `ShowErrorsPanicHandler` 函数会以一种易于阅读的格式将 panic 错误打印到浏览器中。 ## 与其他 Router 的意外差异 为了简单和高性能,此 router 有意减少了功能特性。 如果您之前使用的是在后台进行更重处理的其他 router,您可能会遇到一些意外行为。这个列表绝不详尽, 仅涵盖了用户遇到过的一些不明显的情形。 ### gorilla/pat 查询字符串修改 在匹配路由中的参数时,`gorilla/pat` router 会修改 `Request.URL.RawQuery`,使得这些参数看起来像是存在于查询字符串中一样。 `httptreemux` 不会这样做。有关更多详细信息以及如果您需要的话可以为您执行此转换的 代码片段,请参阅 [Issue #26](https://github.com/dimfeld/httptreemux/issues/26)。 ### httprouter 与 catch 参数 使用 `httprouter` 时,带有 catch-all 参数的路由(例如 `/images/*path`)会匹配诸如 `/images/` 之类的 URL,此时 catch-all 参数为空。此 router 不会匹配空的 catch-all 参数,但可以通过添加一个不带 catch-all 的路由(例如 `/images/`)来复现该行为。 ## Middleware 此包不提供任何 middleware。但市面上有很多出色的选择,而且自己编写也非常容易。router 提供了 `Use` 和 `UseHandler` 函数来简化 middleware 链的创建。(这些函数的详细文档即将推出。) # 致谢 * 灵感来源于 Julien Schmidt 的 [httprouter](https://github.com/julienschmidt/httprouter) * Show Errors panic handler 来源于 [gocraft/web](https://github.com/gocraft/web)
标签:EVTX分析, Go, HTTP路由, Ruby工具, SOC Prime, Syscall, Web开发, 开发工具, 开发框架, 日志审计