首页 » Web前端 » php出生技巧_GO WEB开拓简介router 请求路由

php出生技巧_GO WEB开拓简介router 请求路由

访客 2024-11-21 0

扫一扫用手机浏览

文章目录 [+]

GET /card/:idPOST /card/:idDELTE /card/:idGET /card/:id/name...GET /card/:id/relations

可见是否利用框架还是要详细问题详细剖析的。

Go的Web框架大致可以分为这么两类:

php出生技巧_GO WEB开拓简介router 请求路由

Router框架MVC类框架

在框架的选择上,大多数情形下都是依照个人的喜好和公司的技能栈。
例如公司有很多技能职员是PHP出身,那么他们一定会非常喜好像beego这样的框架,但如果公司有很多C程序员,那么他们的想法可能是越大略越好。
比如很多大厂的C程序员乃至可能都会去用C措辞去写很小的CGI程序,他们可能本身并没有什么意愿去学习MVC或者更繁芜的Web框架,他们须要的只是一个非常大略的路由(乃至连路由都不须要,只须要一个根本的HTTP协议处理库来帮他省却没什么意思的体力劳动)。

php出生技巧_GO WEB开拓简介router 请求路由
(图片来自网络侵删)

Go的net/http包供应的便是这样的根本功能,写一个大略的http echo server只须要30s。

//brief_intro/echo.gopackage mainimport (...)func echo(wr http.ResponseWriter, r http.Request) { msg, err := ioutil.ReadAll(r.Body) if err != nil { wr.Write([]byte("echo error")) return } writeLen, err := wr.Write(msg) if err != nil || writeLen != len(msg) { log.Println(err, "write len:", writeLen) }}func main() { http.HandleFunc("/", echo) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal(err) }}

如果你过了30s还没有完成这个程序,请检讨一下你自己的打字速率是不是慢了(开个玩笑 :D)。
这个例子是为相识释在Go中写一个HTTP协议的小程序有多么大略。
如果你面临的情形比较繁芜,例如几十个接口的企业级运用,直接用net/http库就显得不太得当了。

我们来看看开源社区中一个Kafka监控项目中的做法:

//Burrow: http_server.gofunc NewHttpServer(app ApplicationContext) (HttpServer, error) { ... server.mux.HandleFunc("/", handleDefault) server.mux.HandleFunc("/burrow/admin", handleAdmin) server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList}) server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka}) server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList}) ...}

上面这段代码来自大名鼎鼎的linkedin公司的Kafka监控项目Burrow,没有利用任何router框架,只利用了net/http。
只看上面这段代码彷佛非常优雅,我们的项目里大概只有这五个大略的URI,以是我们供应的做事便是下面这个样子:

//burrow/admin/v2/kafka/v2/kafka//v2/zookeeper

如果你确实这么想的话就被骗了。
我们再进handleKafka()这个函数一探究竟:

func handleKafka(app ApplicationContext, w http.ResponseWriter, r http.Request) (int, string) { pathParts := strings.Split(r.URL.Path[1:], "/") if _, ok := app.Config.Kafka[pathParts[2]]; !ok { return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r) } if pathParts[2] == "" { // Allow a trailing / on requests return handleClusterList(app, w, r) } if (len(pathParts) == 3) || (pathParts[3] == "") { return handleClusterDetail(app, w, r, pathParts[2]) } switch pathParts[3] { case "consumer": switch { case r.Method == "DELETE": switch { case (len(pathParts) == 5) || (pathParts[5] == ""): return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4]) default: return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) } case r.Method == "GET": switch { case (len(pathParts) == 4) || (pathParts[4] == ""): return handleConsumerList(app, w, r, pathParts[2]) case (len(pathParts) == 5) || (pathParts[5] == ""): // Consumer detail - list of consumer streams/hosts? Can be config info later return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r) case pathParts[5] == "topic": switch { case (len(pathParts) == 6) || (pathParts[6] == ""): return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4]) case (len(pathParts) == 7) || (pathParts[7] == ""): return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6]) } case pathParts[5] == "status": return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false) case pathParts[5] == "lag": return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true) } default: return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) } case "topic": switch { case r.Method != "GET": return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) case (len(pathParts) == 4) || (pathParts[4] == ""): return handleBrokerTopicList(app, w, r, pathParts[2]) case (len(pathParts) == 5) || (pathParts[5] == ""): return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4]) } case "offsets": // Reserving this endpoint to implement later return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r) } // If we fell through, return a 404 return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)}

由于默认的net/http包中的mux不支持带参数的路由,以是Burrow这个项目利用了非常蹩脚的字符串Split和乱七八糟的 switch case来达到自己的目的,但却让本来该当很集中的路由管理逻辑变得繁芜,散落在系统的各处,难以掩护和管理。
如果读者细心地看过这些代码之后,可能会创造其它的几个handler函数逻辑上较大略,最繁芜的也便是这个handleKafka()。
而我们的系统总是从这样微不足道的混乱开始集腋成裘,终极变得难以整顿。

根据我们的履历,大略地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就只管即便不要利用net/http中默认的路由。
在Go开源界运用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。
关于httpRouter路由的事理,会在本章节的router一节中进行详细的阐释。

再来回顾一下文章开头说的,开源界有这么几种框架,第一种是对httpRouter进行大略的封装,然后供应定制的中间件和一些大略的小工具集成比如gin,主打轻量,易学,高性能。
第二种是借鉴其它措辞的编程风格的一些MVC类框架,例如beego,方便从其它措辞迁移过来的程序员快速上手,快速开拓。
还有一些框架功能更为强大,除了数据库schema设计,大部分代码直接天生,例如goa。
不管哪种框架,适宜开拓者背景的便是最好的。

router 要求路由

在常见的Web框架中,router是必备的组件。
Go措辞圈子里router也时常被称为http的multiplexer。
在上一节中我们通过对Burrow代码的大略学习,已经知道如何用http标准库中内置的mux来完成大略的路由功能了。
如果开拓Web系统对路径中带参数没什么兴趣的话,用http标准库中的mux就可以。

RESTful是几年前刮起的API设计风潮,在RESTful中除了GET和POST之外,还利用了HTTP协议定义的几种其它的标准化语义。
详细包括:

const ( MethodGet = "GET" MethodHead = "HEAD" MethodPost = "POST" MethodPut = "PUT" MethodPatch = "PATCH" // RFC 5789 MethodDelete = "DELETE" MethodConnect = "CONNECT" MethodOptions = "OPTIONS" MethodTrace = "TRACE")

来看看RESTful中常见的要求路径:

GET /repos/:owner/:repo/comments/:id/reactionsPOST /projects/:project_id/columnsPUT /user/starred/:owner/:repoDELETE /user/starred/:owner/:repo

相信聪明的你已经猜出来了,这是Github官方文档中挑出来的几个API设计。
RESTful风格的API重度依赖要求路径。
会将很多参数放在要求URI中。
除此之外还会利用很多并不那么常见的HTTP状态码,不过本节只谈论路由,以是先略过不谈。

如果我们的系统也想要这样的URI设计,利用标准库的mux显然就力不从心了。

httprouter

较盛行的开源go Web框架大多利用httprouter,或是基于httprouter的变种对路由进行支持。
前面提到的github的参数式路由在httprouter中都是可以支持的。

由于httprouter中利用的是显式匹配,以是在设计路由的时候须要规避一些会导致路由冲突的情形,例如:

conflict:GET /user/info/:nameGET /user/:idno conflict:GET /user/info/:namePOST /user/:id

大略来讲的话,如果两个路由拥有同等的http方法(指 GET/POST/PUT/DELETE)和要求路径前缀,且在某个位置涌现了A路由是wildcard(指:id这种形式)参数,B路由则是普通字符串,那么就会发生路由冲突。
路由冲突会在初始化阶段直接panic:

panic: wildcard route ':id' conflicts with existing children in path '/user/:id'goroutine 1 [running]:github.com/cch123/httprouter.(node).insertChild(0xc4200801e0, 0xc42004fc01, 0x126b177, 0x3, 0x126b171, 0x9, 0x127b668) /Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:256 +0x841github.com/cch123/httprouter.(node).addRoute(0xc4200801e0, 0x126b171, 0x9, 0x127b668) /Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:221 +0x22agithub.com/cch123/httprouter.(Router).Handle(0xc42004ff38, 0x126a39b, 0x3, 0x126b171, 0x9, 0x127b668) /Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:262 +0xc3github.com/cch123/httprouter.(Router).GET(0xc42004ff38, 0x126b171, 0x9, 0x127b668) /Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:193 +0x5emain.main() /Users/caochunhui/test/go_web/httprouter_learn2.go:18 +0xafexit status 2

还有一点须要把稳,由于httprouter考虑到字典树的深度,在初始化时会对参数的数量进行限定,以是在路由中的参数数目不能超过255,否则会导致httprouter无法识别后续的参数。
不过这一点上也不用考虑太多,毕竟URI是人设计且给人来看的,相信没有长得夸年夜的URI能在一条路径中带有200个以上的参数。

除支持路径中的wildcard参数之外,httprouter还可以支持号来进行通配,不过号开头的参数只能放在路由的结尾,例如下面这样:

Pattern: /src/filepath /src/ filepath = "" /src/somefile.go filepath = "somefile.go" /src/subdir/somefile.go filepath = "subdir/somefile.go"

这种设计在RESTful中可能不太常见,紧张是为了能够利用httprouter来做大略的HTTP静态文件做事器。

除了正常情形下的路由支持,httprouter也支持对一些分外情形下的回调函数进行定制,例如404的时候:

r := httprouter.New()r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r http.Request) { w.Write([]byte("oh no, not found"))})

或者内部panic的时候:

r.PanicHandler = func(w http.ResponseWriter, r http.Request, c interface{}) { log.Printf("Recovering from panic, Reason: %#v", c.(error)) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(c.(error).Error()))}

目前开源界最为盛行(star数最多)的Web框架gin利用的便是httprouter的变种。

事理

httprouter和浩瀚衍生router利用的数据构造被称为压缩字典树(Radix Tree)。
读者可能没有打仗过压缩字典树,但对字典树(Trie Tree)该当有所耳闻。
图 5-1是一个范例的字典树构造:

字典树

字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。
对付目标字符串,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经涌现过,韶光繁芜度为O(n),n可以认为是目标字符串的长度。
为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串比拟的韶光繁芜度取决于字符串长度。
如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,韶光繁芜度只高不低。
可认为字典树是一种空间换韶光的范例做法。

普通的字典树有一个比较明显的缺陷,便是每个字母都须要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺陷。
是范例的压缩字典树构造:

压缩字典树

每个节点上不但存储一个字母了,这也是压缩字典树中“压缩”的紧张含义。
利用压缩字典树可以减少树的层数,同时由于每个节点上数据存储也比常日的字典树要多,以是程序的局部性较好(一个节点的path加载到cache即可进行多个字符的比拟),从而对CPU缓存友好。

压缩字典树创建过程

我们来跟踪一下httprouter中,一个范例的压缩字典树的创建过程,路由设定如下:

PUT /user/installations/:installation_id/repositories/:repository_idGET /marketplace_listing/plans/GET /marketplace_listing/plans/:id/accountsGET /searchGET /statusGET /support补充路由:GET /marketplace_listing/plans/ohyes

末了一条补充路由是我们臆想的,除此之外所有API路由均来自于api.github.com。

root 节点创建

httprouter的Router构造体中存储压缩字典树利用的是下述数据构造:

// 略去了其它部分的 Router structtype Router struct { // ... trees map[string]node // ...}

trees中的key即为HTTP 1.1的RFC中定义的各种方法,详细有:

GETHEADOPTIONSPOSTPUTPATCHDELETE

每一种方法对应的都是一棵独立的压缩字典树,这些树彼此之间不共享数据。
详细到我们上面用到的路由,PUT和GET是两棵树而非一棵。

大略来讲,某个方法第一次插入的路由就会导致对应字典树的根节点被创建,我们按顺序,先是一个PUT:

r := httprouter.New()r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello)

这样PUT对应的根节点就会被创建出来。
把这棵PUT的树画出来:

图 5-3 插入路由之后的压缩字典树

radix的节点类型为httprouter.node,为相识释方便,我们留下了目前关心的几个字段:

path: 当前节点对应的路径中的字符串wildChild: 子节点是否为参数节点,即 wildcard node,或者说 :id 这种类型的节点nType: 当前节点类型,有四个列举值: 分别为 static/root/param/catchAll。
static // 非根节点的普通字符串节点 root // 根节点 param // 参数节点,例如 :id catchAll // 通配符节点,例如 anywayindices:子节点索引,当子节点为非参数类型,即本节点的wildChild为false时,会将每个子节点的首字母放在该索引数组。
说是数组,实际上是个string。

当然,PUT路由只有唯一的一条路径。
接下来,我们往后续的多条GET路径为例,讲解子节点的插入过程。

子节点插入

当插入GET /marketplace_listing/plans时,类似前面PUT的过程,GET树的构造如图 5-4

插入第一个节点的压缩字典树

由于第一个路由没有参数,path都被存储到根节点上了。
以是只有一个节点。

然后插入GET /marketplace_listing/plans/:id/accounts,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很大略,插入后的树构造见

插入第二个节点的压缩字典树

由于:id这个节点只有一个字符串的普通子节点,以是indices还依然不须要处理。

上面这种情形比较大略,新的路由可以直接作为原路由的子节点进行插入。
实际情形不会这么美好。

边分裂

接下来我们插入GET /search,这时会导致树的边分裂,见图 5-6

插入第三个节点,导致边分裂

原有路径和新的路径在初始的/位置发生分裂,这样须要把原有的root节点内容下移,再将新路由 search同样作为子节点挂在root节点之下。
这时候由于子节点涌现多个,root节点的indices供应子节点索引,这时候该字段就须要派上用场了。
"ms"代表子节点的首字母分别为m(marketplace)和s(search)。

我们一口作气,把GET /status和GET /support也插入到树中。
这时候会导致在search节点上再次发生分裂,终极结果见

子节点冲突处理

在路由本身只有字符串的情形下,不会发生任何冲突。
只有当路由中含有wildcard(类似 :id)或者catchAll的情形下才可能冲突。
这一点在前面已经提到了。

子节点的冲突处理很大略,分几种情形:

在插入wildcard节点时,父节点的children数组非空且wildChild被设置为false。
例如:GET /user/getAll和GET /user/:id/getAddr,或者GET /user/aaa和GET /user/:id。
在插入wildcard节点时,父节点的children数组非空且wildChild被设置为true,但该父节点的wildcard子节点要插入的wildcard名字不一样。
例如:GET /user/:id/info和GET /user/:name/info。
在插入catchAll节点时,父节点的children非空。
例如:GET /src/abc和GET /src/filename,或者GET /src/:id和GET /src/filename。
在插入static节点时,父节点的wildChild字段被设置为true。
在插入static节点时,父节点的children非空,且子节点nType为catchAll。

只要发生冲突,都会在初始化的时候panic。
例如,在插入我们臆想的路由GET /marketplace_listing/plans/ohyes时,涌现第4种冲突情形:它的父节点marketplace_listing/plans/的wildChild字段为true。

标签:

相关文章