设计一个用于RESTFUL API 路由器
1. 什么是REST
REST是一组架构约束条件和原则,一般满足以下条件则可以说某个资源是restful风格的:
- 每一个URI代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个HTTP(GET、PUT、POST、DELETE)动词,对服务器端资源进行操作,实现"表现层状态转化"。
- Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答。
2. GithubAPI 很好地使用了RESTFUL架构,按表示层需要的数据组织 API 比按系统功能组织具有明显的优势。
3. 设计过程:
本次实现参考了 GoRestful、Go-json-rest、Gorilla Mux等包的设计架构和部分方法的实现
-
http请求:
//封装一个http.Request,构造restful风格的Request type Request struct { *http.Request PathParams map[string]string Env map[string]interface{} } //读入Request并判断是本地地址还是其它地址 func (r *Request) BaseUrl() *url.URL //解析URL中的路径信息 func (r *Request) UrlFor(path string, queryParams map[string][]string) *url.URL //将Request中的信息解码为Json风格 func (r *Request) DecodeJsonPayload(v interface{}) error //跨域资源请求CORS type CorsInfo struct { IsCors bool IsPreflight bool Origin string OriginUrl *url.URL AccessControlRequestMethod string AccessControlRequestHeaders []string }
-
http响应:
//ResponseWriter 将http.response响应封装为json风格 type ResponseWriter interface { //ResponseWriter头部信息 Header() http.Header // The Content-Type header is set to "application/json" WriteJson(v interface{}) error // 将数据解码为JSON格式 EncodeJson(v interface{}) ([]byte, error) //与WriteJson相似 WriteHeader(int) } // 对应于 http.Error(w, "error message", code)的错误 func Error(w ResponseWriter, error string, code int) // 如果地址为不存在,返回404 func NotFound(w ResponseWriter, r *Request) //封装 ResponseWriter type responseWriter struct { http.ResponseWriter wroteHeader bool } //设计ResponseWriter的方法 //写头部信息 func (w *responseWriter) WriteHeader(code int) //解码为Json格式 func (w *responseWriter) EncodeJson(v interface{}) ([]byte, error)
-
路由 Route设计
//定义一个路由 type Route struct { HttpMethod string PathExp string Func HandlerFunc } //路由路径匹配,通过路由表中的信息访问 func (route *Route) Makepath(pathParams map[string]string) string //获取路径头部信息 func getHead(path string, handlerFunc HandlerFunc) *Route //http请求Get方法,获取信息 func Get(path string, handlerFunc HandlerFunc) *Route //http请求Post方法,插入信息 func Post(path string, handlerFunc HandlerFunc) *Route //http请求Patch方法,不安全的更新信息 func Patch(path string, handlerFunc HandlerFunc) *Route //http请求Delete方法,删除信息 func Delete(path string, handlerFunc HandlerFunc) *Route //http请求Options方法,URL安全验证 func Options(path string, handlerFunc HandlerFunc) *Route
-
路由器设计,用一个动态路由表记录路由信息
//路由器结构 type router struct { Routes []*Route//保存多个路由 disable bool //是否可达 index map[*Route]int trie *trie.Trie //trie树保存路由信息 } //将路由封装到app中 func MakeRouter(routes ...*Route) (App, error) //在URL中进行信息匹配 func (rt *router) AppFunc() HandlerFunc func escapedPath(urlObj *url.URL) string func escapedPathExp(pathExp string) (string, error) // 将路径保存到Trie路由结构中 func (rt *router) start() error // 返回第一个定义的路由 func (rt *router) ofFirstDefinedRoute(matches []*trie.Match) *trie.Match // 返回匹配到的第一个路由 func (rt *router) findRouteFromURL(httpMethod string, urlObj *url.URL) // 解析URL中的路由地址 func (rt *router) findRoute(httpMethod, urlStr string) (*Route, map[string]string, bool, error)
-
middle中间架构通过接口设计,使得函数设计更加简单
// 定义一个HandelerFunc,功能类似于http.handle type HandlerFunc func(ResponseWriter, *Request) //定义一个app接口,包含一个AppFunc方法用于封装 type App interface { AppFunc() HandlerFunc } //一个app适配器,使得函数编写变得简单 type AppSimple HandlerFunc // AppFunc 使得 AppSimple 继承App接口 func (as AppSimple) AppFunc() HandlerFunc { return HandlerFunc(as) } //定义一个中间件返回解析后的HandlerFunc type Middleware interface { MiddlewareFunc(handler HandlerFunc) HandlerFunc } // 对中间件进行解码 func WrapMiddlewares(middlewares []Middleware, handler HandlerFunc) HandlerFunc
-
API设计,通过封装好的App实现RESTFUL风格
// 用栈保存app和middleware中间件 type Api struct { stack []Middleware app App } // 创建一个api并返回 func NewApi() *Api //插入api的元素 func (api *Api) Use(middlewares ...Middleware) // MakeHandler 对api进行解码,返回以一个http.Handler func (api *Api) MakeHandler() http.Handler //定义中间件,实现api界面友好功能 var DefaultDevStack = []Middleware{ &AccessLogApacheMiddleware{}, }
-
路由表结构设计
Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串,一般有以下性质。
- 根节点不包含字符,其它每个节点都只包含一个字符
- 从根节点到某个节点,路径上的字符连接起来,对于当前节点对应的字符串
- 每个节点的所有子节点包含的字符都不相同
从上面的描述可以看出,trie树的结构与URL中的路径信息的结构相一致,因此我们直接借用goRestful的设计。
//树的节点信息 type node struct { HttpMethodToRoute map[string]interface{} //子节点 Children map[string]*node ChildrenKeyLen int // '/path'或'/.path'类型的节点 ParamChild *node ParamName string // ‘/#path'类型的节点 RelaxedChild *node RelaxedName string // 其它类型的节点 SplatChild *node SplatName string } //Trie结构 type Trie struct { root *node } //将路由保存到路由表中 func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParams []string) error //信息解码 func splitParam(remaining string) (string, string) //松弛信息解码 func splitRelaxed(remaining string) (string, string) // 路由信息压缩 func (n *node) compress() //路由参数匹配 type paramMatch struct { name string value string } //参数上下文匹配 type findContext struct { paramStack []paramMatch matchFunc func(httpMethod, path string, node *node) } //参数映射 func (fc *findContext) paramsAsMap() map[string]string
-
路由与URL是否匹配
//http请求中的参数和路由表中保存的路径匹配的 type Match struct { // Same Route as in AddRoute Route interface{} // map of params matched for this result Params map[string]string } //查找路由 func (n *node) find(httpMethod, path string, context *findContext) //
4. 功能测试:
该过程只是对路由器的功能进行一小部分展示,在第六小节性能测试中我们会把所有的githubApi保存到一个struct结构中进行整体测试。
-
//获取用户信息 curl -i -X GET http://127.0.0.1:8080/users/1834
-
//获取所有用户 curl -i -X GET http://127.0.0.1:8080/users
-
//修改用户信息 curl -i -X PUT http://127.0.0.1:8080/users/1834 \-d '{"Code":"1834","Name":"bbb"}'
-
//增加一个用户 curl -i -X POST http://127.0.0.1:8080/users \-d '{"Code":"1534","Name":"bbb"}'
-
//删除一个用户 curl -i -X DELETE http://127.0.0.1:8080/users/1734
删除操作之和再次获取所有用户信息:可以看到code为1734的User被删除。
-
//访问外部网址github.com curl -i -X GET http://127.0.0.1:8080/link/github.com
-
//获取信息 curl -i -X GET http://127.0.0.1:8080/message
-
//以流方式获取信息 curl -i -X GET http://127.0.0.1:8080/stream
-
//读取txt文本信息 curl -i -X GET http://127.0.0.1:8080/message.txt
5. 集成测试
测试定义的所有方法:go test -v
6. 性能测试:借助go-http-routing-benchmark包的方法来实现
-
测试该路由器是否可以访问所有的GithubApi
对所有的GithubApi进行封装,下面展示一部分。
利用以下方法进行所有api的测试
func TestRouters(t *testing.T) { loadTestHandler = true //routers为封装的所有路由 for _, router := range routers { req, _ := http.NewRequest("GET", "/", nil) u := req.URL rq := u.RawQuery for _, api := range apis { r := router.load(api.routes) for _, route := range api.routes { w := httptest.NewRecorder() req.Method = route.method req.RequestURI = route.path u.Path = route.path u.RawQuery = rq r.ServeHTTP(w, req) if w.Code != 200 || w.Body.String() != route.path { t.Errorf( "%s in API %s: %d - %s; expected %s %s\n", router.name, api.name, w.Code, w.Body.String(), route.method, route.path, ) } } } } loadTestHandler = false }
测试结果:
-
对不同长度,不同类型的Api进行多次测试,测试路由器的性能
const fiveBrace = "/{a}/{b}/{c}/{d}/{e}" const fiveRoute = "/test/test/test/test/test" func benchRoutes(b *testing.B, router http.Handler, routes []route) func BenchmarkGoJsonRest_Param(b *testing.B) const twentyColon = "/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j/:k/:l/:m/:n/:o/:p/:q/:r/:s/:t" const twentyBrace = "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}" const twentyRoute = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t" func BenchmarkGoJsonRest_Param20(b *testing.B) func BenchmarkGoJsonRest_ParamWrite(b *testing.B)
测试结果:
下面为用golang实现的其它路由器的测试结果:
Router | GithubApi |
---|---|
Beego | 407248 B |
Goji | 95736 B |
Gorilla Mux | 1557216 B |
HttpRouter | 44344 B |
对比可以看到,本次实现的路由器有着较好的性能。
7. 实验结论:
RESTFUL 风格的Api 按照表示层需要的数据组织api,相比其它类型的更加友好和直观,在我们设计路由器时也可以分块设计并且根据需求设计。这将会成为 web 应用间互操作的主流。