blog icon indicating copy to clipboard operation
blog copied to clipboard

Go语言动手写Web框架 - Gee第三天 路由Router | 极客兔兔

Open geektutu opened this issue 4 years ago • 88 comments

https://geektutu.com/post/gee-day3.html

7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架。本文介绍了如何用 Trie 前缀树实现路由。支持简单的参数解析和通配符的场景。

geektutu avatar Aug 27 '19 17:08 geektutu

very good!

demo007x avatar Feb 17 '20 10:02 demo007x

it's cool

zaneyang123 avatar Mar 14 '20 09:03 zaneyang123

开始看不懂了。。。。

ghost avatar Mar 25 '20 05:03 ghost

这里有问题, getRoutes 的时候有用到的 root.travel 并没有说出来 ,测试的时候有点懵逼,后来看github源码才发现

ReviveKwan avatar Mar 27 '20 01:03 ReviveKwan

只有第二个参数接受:匹配吗

Arbusz avatar Mar 28 '20 15:03 Arbusz

这一节难度大了不止一点半点 建议分两段 放后面讲

zjfsdnu avatar Jun 01 '20 01:06 zjfsdnu

@Arbusz @zjfsdnu 为了降低代码量,这一节实现的路由算法很粗糙。因为整个系列的核心还是想介绍一个框架的最基础的构成,所以都只能是浅尝辄止,不过7天就打不住了。

geektutu avatar Jun 08 '20 07:06 geektutu

Trie树有点懵了,那两个mathch函数和interst、search函数注释能多点就好了,看起来太费劲了。

haochen233 avatar Jun 18 '20 08:06 haochen233

这章确实看的懵逼了,注释太少,跨度太大。在慢慢研究了。

6613974 avatar Jul 24 '20 03:07 6613974

大佬写的太棒了,very cool

jianghui568 avatar Aug 12 '20 08:08 jianghui568

感谢大佬让我 如沐春风 如淋甘露 飘飘欲仙 让我走出人生低谷 让我让重新开始热爱生活

wilgx0 avatar Aug 28 '20 07:08 wilgx0

兔兔你好,发现了两个小bug

第一个 bug

trie.go 里的

func (n *node) insert(pattern string, parts []string, height int) {
	if len(parts) == height {
		n.pattern = pattern
		return
	}
	part := parts[height]
	child := n.matchChild(part)
	if child == nil {
		child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
		n.children = append(n.children, child)
	}
	child.insert(pattern, parts, height+1)
}

如果第一次插入的pattern/:agemethodGEThandlefunchandleAge()

那么会生成一个这样的 node

nodeAge = node{
    pattern:  ":age",
    part:     ":age",
    children: nil,
    isWild:   true,
}
handlers["GET-:age"] = handleAge

第二次插入的 pattern/18 ,method 与第一次相同,仍然为 GEThandlefunchandle18(),此时并不会修改之前的 node,而是修改了之前 nodeAgepattern

nodeAge = node{
    pattern:  "18",
    part:     ":age",
    children: nil,
    isWild:   true,
}
handlers["GET-:age"] = handleAge
handlers["GET-18"] =  handle18

接下来看看handle()函数

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
		c.Params = params
		key := c.Method + "-" + n.pattern
		r.handlers[key](c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

当有一个 /19 的请求到来时,将会匹配到 nodeAge,但是由于 nodeAgepattern 变成了 18,因此将会被 handle18() 处理,这不太合适。

GIN的做法是将冲突的路由直接panic了。

第二个bug

routerhandle 方法貌似不是协程安全的

假设此时只有一个/:agerouter

nodeAge = node{
    pattern:  ":age",
    part:     ":age",
    children: nil,
    isWild:   true,
}
handlers["GET-:age"] = handleAge

此时有两个请求地址分别为 /18/19的请求到达。

handle() 内的 getRoute() 函数得到的 params 分别为 key:age value:18key:age value:19,它们会对同一个 context 进行写入,因此不太安全。

http 包内的 server.go 文件中的 Serve 函数内,有这样一段注释

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error

可见,http.ListenAndServe() 是有可能开启多个协程的。

liweiforeveryoung avatar Sep 16 '20 02:09 liweiforeveryoung

@liweiforeveryoung 非常感谢,指出了非常关键的问题。

第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用 ServeHTTP ,这个方法中,context 每次都创建新的,不会对同一个context进行写入。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

geektutu avatar Sep 16 '20 09:09 geektutu

@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。

第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用 ServeHTTP ,这个方法中,context 每次都创建新的,不会对同一个context进行写入。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

嗯,我刚刚看发现了ServeHTTP每次都创建了一个新的context,刚准备修改评论的,结果看见你回复了。

liweiforeveryoung avatar Sep 16 '20 12:09 liweiforeveryoung

@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。

第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用 ServeHTTP ,这个方法中,context 每次都创建新的,不会对同一个context进行写入。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。

第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用 ServeHTTP ,这个方法中,context 每次都创建新的,不会对同一个context进行写入。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

非常感谢作者的付出!!

liweiforeveryoung avatar Sep 16 '20 12:09 liweiforeveryoung

问一个比较蠢的问题。search函数中调用了matchChildren,matchChildren什么情况下会有多个匹配返回呢?

yufeifly avatar Sep 19 '20 04:09 yufeifly

建议这一章先把代码写一遍 大概清楚每一个方法是做什么的,然后按照 addRouter -> getRouter -> handle 在看一遍代码 走一遍流程就明了多了, 说白了就是 用前缀树结构存 前缀树结构取, 用GET /a/asd/c || GET a/s/c 匹配到路由(GET-/a/:param/c)对应的HandlerFunc 并把asd || s 存在context的Params里.

感谢楼主

givetimetolife avatar Oct 21 '20 08:10 givetimetolife

func (n *node) travel(list *([]*node)) {
	if n.pattern != "" {
		*list = append(*list, n)
	}
	for _, child := range n.children {
		child.travel(list)
	}
}

func (n *node) String() string {
	return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}

以上两个方法,在文章中没有提到,但是用到了,看了GitHub源码,才知道,建议后期这种不重要的代码可以提一下,要不会以为自己哪里漏了。

BigYoungs avatar Oct 29 '20 03:10 BigYoungs

@BigYoungs 感谢建议~

geektutu avatar Oct 29 '20 05:10 geektutu

其实自己在关键的地方多加点fmt就看懂怎么回事儿了 不过还是建议作者多讲讲 毕竟是从零实现系列。。。

limaoxiaoer avatar Nov 12 '20 08:11 limaoxiaoer

感谢兔兔,从这里开始的4个方法matchChild,matchChildren,insert,search,设计方法和应用场景能简单说明下就更好了

xlban163 avatar Nov 18 '20 02:11 xlban163

@limaoxiaoer @xlban163 感谢二位的建议,这里可能假定了大家是计算机专业,学习过二叉树、B+树等常用的数据结构了。以后的文章细节会多写一些。

geektutu avatar Nov 22 '20 14:11 geektutu

没其他语言开发基础的, 估计已经懵了, 建议小白们, 多看几遍, 每个方法仔细推敲一下, 也很好理解, 不必完全明白, 懂流程就行了, 继续往前看

wushh avatar Dec 03 '20 03:12 wushh

isWild为true时表示的是通配符匹配吧?所以isWild为true时应该是模糊匹配吧 通配符匹配==精准匹配吗

Howard0o0 avatar Dec 20 '20 01:12 Howard0o0

写的太好了,十分感谢大佬的付出!

wangzukang avatar Dec 23 '20 08:12 wangzukang

@Howard0o0 通配符匹是文中的模糊匹配,精准匹配是指每个字符都一致,而不是用通配符。

写的太好了,十分感谢大佬的付出!

@wushh @wangzukang 感谢支持,笔芯~

geektutu avatar Dec 23 '20 15:12 geektutu

受益匪浅,感谢前辈的付出!

jeffmingup avatar Dec 26 '20 08:12 jeffmingup

谢谢博主,我有小问题, 在Demo例子里 curl "http://localhost:9999/hello/geektutu/a/b", 匹配不了/hello/geektutu,但可以匹配到/hello 所有是否可以修改为:

func (n *node) search(parts []string, height int) *node {
	if len(parts) == height || strings.HasPrefix(n.part, "*") {
		if n.pattern == "" {
			return nil
		}
		return n
	}

	part := parts[height]
	children := n.matchChildren(part)

	for _, child := range children {
		result := child.search(parts, height+1)
		if result != nil {
			return result
		}
	}

   //应该新增, 判断当前节点是否满足其前缀
   
  if  n.pattern != nil {
       return n
 }

	return nil
}

feixintianxia avatar Dec 28 '20 03:12 feixintianxia

很棒的学习资料!

有一个问题 get /a/:b get /a/c

/a/x 也是去到 /a/c


get /a/c get /a/:b 和 get /a/:b get /a/c 顺序不一样, 结果是两个不同的树

GaloisZhou avatar Dec 29 '20 11:12 GaloisZhou

@GaloisZhou 嗯,前面评论提到了,这一块路由有冲突时没有提示,直接覆盖了。gin 的做法是直接 panic 报失败。

geektutu avatar Dec 31 '20 14:12 geektutu