Realize the web framework Gee from scratch in seven days - 3

Previously, we used a very simple map structure to store the routing table, using the map to store key-value pairs, the index is very efficient, but there is a drawback, the key-value pair storage method can only be used to index static routes. So what if we want to support dynamic routing like /hello/:name? The so-called dynamic routing means that a routing rule can match a certain type instead of a fixed route. For example, /hello/:name can match /hello/geektutu, hello/jack, etc.

So today our task is to:

  • Dynamic route parsing using Tire tree
  • Two modes are supported: name and *filepath

Next, the dynamic routing we implement has the following two functions:

  • Parameter matching ":", such as /p/:lang/doc, can match /p/c/doc and /p/go/doc
  • Wildcard "*", such as /static/*filepath, can match /static/fav.ico, or /static/js/jQuery.js, this pattern is often used in static servers, and can recursively match subpaths

Tire tree implementation

First of all, we need to design the information that should be stored on the tree node, where pattern indicates the route that the current node can match; part indicates the content of a certain part of the route corresponding to the current node; children stores the child nodes of the current node, that is, the subsequent matching route; isWild It indicates whether the current node is a fuzzy match. Different from ordinary trees, isWildthis parameter is added in order to realize dynamic route matching. That is, when we match /p/go/doc/this route, the first-level nodes pare matched accurately p, and the second-level nodes goare fuzzy matched :lang, then langthis parameter will be assigned a value to gocontinue to the next level of matching. We wrap the matching logic as a helper function.

type node struct {
	pattern  string // 待匹配路由,例如 /p/:lang
	part     string // 路由中的一部分,例如 :lang
	children []*node // 子节点,例如 [doc, tutorial, intro]
	isWild   bool // 是否精确匹配,part 含有 : 或 * 时为true
}

The next step is to define the method corresponding to the node object, first of all, two matching methods

  • Find the first match in the subtree: Traverse all child nodes of the current node object, and return when the first part part of the matching node is encountered
// 找到匹配的子节点,场景是用在插入时使用,找到1个匹配的就立即返回
func (n *node) matchChild(part string) *node {
	// 遍历n节点的所有子节点,看是否能找到匹配的子节点,将其返回
	for _, child := range n.children {
		// 如果有模糊匹配的也会成功匹配上
		if child.part == part || child.isWild {
			return child
		}
	}
	return nil
}
  • Find all possible matches in the subtree: traverse all child nodes of the current node object, put all matching nodes into slice, and return slice after traversing all nodes
// 找到匹配的子节点,场景是用在插入时使用,找到1个匹配的就立即返回
func (n *node) matchChild(part string) *node {
	// 遍历n节点的所有子节点,看是否能找到匹配的子节点,将其返回
	for _, child := range n.children {
		// 如果有模糊匹配的也会成功匹配上
		if child.part == part || child.isWild {
			return child
		}
	}
	return nil
}

For routing, the most important thing is of course registration and matching. When developing services, register routing rules and map handlers; when accessing, match routing rules and find the corresponding handlers.

Therefore, the Trie tree needs to support node insertion and query. The insert function corresponds to the registration process in the route, and finds the first matching node in the subtree of the current node according to the content of the target route. If the matching node does not exist, build a new node according to the target route content, and insert the new node into the subtree of the current node, and then insert the next route content according to the complete route, recursively complete the entire route content Node insertion is to complete the registration of the route. One thing to note is that the pattern will be set to /p/:lang/doc only when /p/:lang/doc traverses to the third layer node, that is, the doc node. The pattern attribute of the p and :lang nodes are both empty. Therefore, when the match ends, we can use n.pattern == "" to determine whether the routing rule matches successfully. For example, although /p/python can successfully match :lang, but the pattern value of :lang is empty, so the matching fails.

// 一边匹配一边插入的方法
//r.roots[method].insert(pattern, parts, 0)
//parts = [] parts = [hello] parts = [hello :name]  parts = [assets *filepath]
//pattren= / ```/hello ```/hello/:name ```/assets/*filepath
func (n *node) insert(pattern string, parts []string, height int) {
	if len(parts) == height {
		// 如果已经匹配完了,那么将pattern赋值给该node,表示它是一个完整的url
		// 这是递归的终止条件
		n.pattern = pattern
		return
	}

	part := parts[height]
	child := n.matchChild(part)
	if child == nil {
		// 没有匹配上,那么进行生成,放到n节点的子列表中
		child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
		n.children = append(n.children, child)
	}
	// 接着插入下一个part节点
	child.insert(pattern, parts, height+1)
}

The query function corresponds to the registration process in the route, and searches for the tree node corresponding to the given route according to the existing trie tree. When the last node is found or the current node contains the * wildcard, it proves that the route is successfully matched, and the current node is returned. Otherwise, find all matching nodes in the subtree, traverse these nodes, and recursively check whether the next layer matches, until you find a leaf node that completely matches the route, and return the leaf node.

//n := root.search(searchParts, 0)
//[]   [hello] [hello :name] [assets *filepath]
func (n *node) search(parts []string, height int) *node {
	if len(parts) == height || strings.HasPrefix(n.part, "*") {
		// 递归终止条件,找到末尾了或者通配符
		if n.pattern == "" {
			// pattern为空字符串表示它不是一个完整的url,匹配失败
			return nil
		}
		return n
	}

	part := parts[height]
	// 获取所有可能的子路径
	children := n.matchChildren(part)

	for _, child := range children {
		// 对于每条路径接着用下一part去查找
		result := child.search(parts, height+1)
		if result != nil {
			// 找到了即返回
			return result
		}
	}

	return nil
}
// 查找所有完整的url,保存到列表中
func (n *node) travel(list *([]*node)) {
	if n.pattern != "" {
		// 递归终止条件
		*list = append(*list, n)
	}
	for _, child := range n.children {
		// 一层一层的递归找pattern是非空的节点
		child.travel(list)
	}
}

Router routing implementation

  • data structure

Trie After the tree insertion and search are successfully implemented, next we put the Tire tree into router.go, where roots is the root node of the tree corresponding to the requested route, which is used to determine whether the route matches. The initial tree node is GET Request type nodes such as /POST (key eg, roots['GET'] roots['POST']); handlers store the corresponding response processing function, and the key value is usually "request type-complete route" For example, handlers['GET-/ p/:lang/doc'], handlers['POST-/p/book'].

Change the router structure and newRouter method in router.go to the following

type router struct {
	roots    map[string]*node       // 请求的路由对应的树根节点,用于判断路由是否匹配(key eg, roots['GET'] roots['POST'])
	handlers map[string]HandlerFunc // 对应的处理函数(key eg, handlers['GET-/p/:lang/doc'], handlers['POST-/p/book'])
}
 
// ---构造函数
func newRouter() *router {
	return &router{
		roots:    make(map[string]*node),
		handlers: make(map[string]HandlerFunc),
	}
}
  • route resolution

Divide the route according to "/" and split it into various parts that can be used as node.part. Note: there can only be one * in the reason, and all content after * is regarded as a file path. In the function, the parameters of the two matchers  getRoute are also parsed , and one is returned  . For example, if it matches , the analysis result is: , if it matches , the analysis result is .:*map/p/go/doc/p/:lang/doc{lang: "go"}/static/css/geektutu.css/static/*filepath{filepath: "css/geektutu.css"}

func parsePattern(pattern string) []string {
	vs := strings.Split(pattern, "/") // 使用 ’/‘ 对字符串进行分割
 
	parts := make([]string, 0) // 初始化路由的各个部分
	for _, item := range vs {  // 遍历路由中每一部分
		if item != "" { // 如果该部分不为空
			parts = append(parts, item) // 添加路由
			if item[0] == '*' {
				break
			}
		}
	}
	return parts
}
  • The method corresponding to the Router object

The first is to register the route, and build a matching trie tree according to the given request type and route. Split the route and insert it into the subtree corresponding to the request type in turn, and store the corresponding response method in the handlers.

func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
	//parts = [] parts = [hello] parts = [hello :name]  parts = [assets *filepath]
	parts := parsePattern(pattern) // 完成对路由的分析,获取其中的各个部分
	//key= GET-/ key= GET-/hello key= GET-/hello/:name key= GET-/assets/*filepath
	key := method + "-" + pattern // 构建router中handlers的注册路由
	//method=/  以/为root节点
	_, ok := r.roots[method]
	if !ok { // 该方法还没有树根节点,添加一个空节点便于插入
		r.roots[method] = &node{}
	}
	//pattren= / ```/hello ```/hello/:name ```/assets/*filepath
	r.roots[method].insert(pattern, parts, 0) // 像树中添加该路由的各个节点

	//把key= GET-/ key= GET-/hello key= GET-/hello/:name key= GET-/assets/*filepath 与回调绑定
	r.handlers[key] = handler
}

Then there is the matching route, according to the request type and the corresponding route, return the matching node and the corresponding parameter list (the content of the fuzzy matching part)

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
	searchParts := parsePattern(path) // 获取待查找路由的各个部分
	params := make(map[string]string) // 模糊匹配对应的匹配内容
	root, ok := r.roots[method]       // 获取该类型请求对应的树节点
 
	if !ok {
		return nil, nil
	} // 不存在该类型请求的路由,直接返回空
 
	n := root.search(searchParts, 0) // 查找是否存在匹配的路由节点
 
	if n != nil { //节点匹配
		parts := parsePattern(n.pattern) // 解析当前找到的节点的路由
		for index, part := range parts { // 遍历路由的各个部分
			if part[0] == ':' { // 遇到模糊匹配:
				params[part[1:]] = searchParts[index] // key:除匹配符(:)的其余字符,value:待匹配路由的对应位置内容
			}
			if part[0] == '*' && len(part) > 1 { // 遇到模糊匹配*
				params[part[1:]] = strings.Join(searchParts[index:], "/") // key:除匹配符(*)的其余字符,value:待匹配路由之后的内容
				break                                                     // 后续可不再匹配
			}
		}
		return n, params // 返回匹配的节点,以及模糊匹配对应的内容
	}
	return nil, nil // 没有匹配的节点,直接返回空
}

Finally, a response is given according to the route. After finding a matching node according to the type of request and the route, the corresponding response content is returned. The more important point is that handlerthe parsed routing parameters are assigned to before calling the matched one c.Params. In this way, you can access the specific value handlerthrough the object inContext

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)
	}
}

Context modification

In HandlerFunc, you want to be able to access the parsed parameters, so you need to  Context add a property and method to the object to provide access to the routing parameters. We store the parsed parameters in Params, and c.Param("lang")get the corresponding values ​​through the method. Therefore, modify the context structure as follows:

  • Add Params to the structure: make(map[string]string), Params stores the content corresponding to fuzzy matching (for example, param[":lang"]="go"), and the corresponding fuzzy matching parameters can be obtained through Param when necessary
  • The constructor increases the initialization of Params
  • A new method to return the matching content in the corresponding route according to the given fuzzy matching content
func (c *Context) Param(key string) string {
	value, _ := c.Params[key]
	return value
}

Guess you like

Origin blog.csdn.net/qq_47431008/article/details/130659513