分享
  1. 首页
  2. 文章

Golang 路由匹配浅析[1]

buptztd · · 3231 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

在本文中以及下篇文章中,我们会研习Golang 的源码来探究Golang 是如何实现HTTP URL 匹配的,并对比 mux的实现。
本人水平有限,如有疏漏和不正确的地方,还请各位不吝赐教,多谢!

Golang 源码基于1.9.2

正文

我们有这样一个HTTP 服务器程序:

func main() {
 http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "Hello")
 })
 http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "World")
 })
 http.ListenAndServe(":8080", nil)
}

我们启动这样一个程序,并在浏览器输入 http://localhost:8080/bar,会看到页面打印出Hello,当我们将URL 换成 http://localhost:8080/foo时候,页面会打印出World。正是HTTP server 根据/bar/foo找到了相应的handler来server 这个request。我们跟随Golang 的源码来探究这个匹配的过程。

注册

跟随几步代码进去,会发现Golang 定义了这样一个结构

type ServeMux struct {
 mu sync.RWMutex
 m map[string]muxEntry
 hosts bool // whether any patterns contain hostnames
}

muxEntry是这样定义的

type muxEntry struct {
 explicit bool
 h Handler
 pattern string
}

看到这里,我们可以大致猜到m这个结构是URL 匹配的关键。它以URL Path作为key,而包含相应的Handler的muxEntry 作为Value。这样,当收到一个HTTP 请求时候,将URL Path 解析出来后,只要在m 中找到对应的handler就可以server 这个request 了。下面我们具体看下handler 的注册过程

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
 mux.mu.Lock()
 defer mux.mu.Unlock()
 if pattern == "" {
 panic("http: invalid pattern " + pattern)
 }
 if handler == nil {
 panic("http: nil handler")
 }
 if mux.m[pattern].explicit {
 panic("http: multiple registrations for " + pattern)
 }
 if mux.m == nil {
 mux.m = make(map[string]muxEntry)
 }
 mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
 if pattern[0] != '/' {
 mux.hosts = true
 }
 // Helpful behavior:
 // If pattern is /tree/, insert an implicit permanent redirect for /tree.
 // It can be overridden by an explicit registration.
 n := len(pattern)
 if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
 // If pattern contains a host name, strip it and use remaining
 // path for redirect.
 path := pattern
 if pattern[0] != '/' {
 // In pattern, at least the last character is a '/', so
 // strings.Index can't be -1.
 path = pattern[strings.Index(pattern, "/"):]
 }
 url := &url.URL{Path: path}
 mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
 }
}

Helpful behavior前面的代码显而易见,如果这个pattern 没有注册,会把handler 注册到这个pattern 上面。而 Helpful behavior 后面的代码会做这样的事情:假如我注册了/bar/这样一个pattern,mux 会默认帮我注册/bar这个pattern,而/bar的handler会将/bar的请求redirect到/bar/。我们修改一下我们的main 函数:

func main() {
 http.HandleFunc("/bar/", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "Hello")
 })
 http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "World")
 })
 http.ListenAndServe(":8080", nil)
}

当我们在浏览器输入http://localhost:8080/bar时,会看到浏览器的URL变成了http://localhost:8080/bar/而且页面打印出了Hello。实际上,这是两个http请求:

Request URL: http://127.0.0.1:8080/bar
Request Method: GET
Status Code: 301 Moved Permanently
Remote Address: 127.0.0.1:8080
Request URL: http://localhost:8080/bar/
Request Method: GET
Status Code: 200 OK (from disk cache)
Remote Address: [::1]:8080

这正是server 对/bar做了redirect请求。
注册一个handler 到一个pattern看起来比较简单,那么Golang 的HTTP server 是如何serve 一个HTTP request 的呢?

匹配

我们都知道HTTP 协议是基于TCP 实现的,我们先来看一个TCP echo 服务器

func main() {
 fmt.Println("Launching server...")
 // listen on all interfaces
 ln, _ := net.Listen("tcp", ":8081")
 for {
 // accept connection on port
 conn, _ := ln.Accept()
 // will listen for message to process ending in newline (\n)
 message, _ := bufio.NewReader(conn).ReadString('\n')
 // output message received
 fmt.Print("Message Received:", string(message))
 // sample process for string received
 newmessage := strings.ToUpper(message)
 // send new string back to client
 conn.Write([]byte(newmessage + "\n"))
 }
}

Golang 里面的net.Listen 封装了socket()bind()的过程,拿到一个listener之后,通过调用Accept()函数阻塞等待新的连接,每次Accept()函数返回时候,会得到一个TCP 连接。
Golang 里面的HTTP 服务也是这么做的:

func (srv *Server) Serve(l net.Listener) error {
 defer l.Close()
 if fn := testHookServerServe; fn != nil {
 fn(srv, l)
 }
 var tempDelay time.Duration // how long to sleep on accept failure
 if err := srv.setupHTTP2_Serve(); err != nil {
 return err
 }
 srv.trackListener(l, true)
 defer srv.trackListener(l, false)
 baseCtx := context.Background() // base is always background, per Issue 16220
 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
 rw, e := l.Accept()
 if e != nil {
 select {
 case <-srv.getDoneChan():
 return ErrServerClosed
 default:
 }
 if ne, ok := e.(net.Error); ok && ne.Temporary() {
 if tempDelay == 0 {
 tempDelay = 5 * time.Millisecond
 } else {
 tempDelay *= 2
 }
 if max := 1 * time.Second; tempDelay > max {
 tempDelay = max
 }
 srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
 time.Sleep(tempDelay)
 continue
 }
 return e
 }
 tempDelay = 0
 c := srv.newConn(rw)
 c.setState(c.rwc, StateNew) // before Serve can return
 go c.serve(ctx)
 }
}

从这也可以看到,对于每一个HTTP 请求,服务端都会起一个goroutine 来serve.
跟随者源码一路追溯下去,发现调用了这样一个函数:

// parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
 s1 := strings.Index(line, " ")
 s2 := strings.Index(line[s1+1:], " ")
 if s1 < 0 || s2 < 0 {
 return
 }
 s2 += s1 + 1
 return line[:s1], line[s1+1 : s2], line[s2+1:], true
}

对连接发送的内容进行HTTP 协议解析,得到 HTTP 方法和URI。我们略过其他协议解析和验证的部分,直接看serve request 的函数:

serverHandler{c.server}.ServeHTTP(w, w.req)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
 handler := sh.srv.Handler
 if handler == nil {
 handler = DefaultServeMux
 }
 if req.RequestURI == "*" && req.Method == "OPTIONS" {
 handler = globalOptionsHandler{}
 }
 handler.ServeHTTP(rw, req)
}

我们看到当handlernil时候,会使用package 的默认handlerDefaultServeMux。再回到我们的main.go:

http.ListenAndServe(":8080", nil)

我们在监听服务的时候,传入的handler 确实是nil,所以使用了DefaultServeMux,而当我们调用http.HandleFunc时,正是向DefaultServeMux 注册了pattern 和相应的handler。DefaultServeMuxServeHTTP方法如下:

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 if r.RequestURI == "*" {
 if r.ProtoAtLeast(1, 1) {
 w.Header().Set("Connection", "close")
 }
 w.WriteHeader(StatusBadRequest)
 return
 }
 h, _ := mux.Handler(r)
 h.ServeHTTP(w, r)
}

mux.Handler(r)方法通过request 找到对应的handler:

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
 mux.mu.RLock()
 defer mux.mu.RUnlock()
 // Host-specific pattern takes precedence over generic ones
 if mux.hosts {
 h, pattern = mux.match(host + path)
 }
 if h == nil {
 h, pattern = mux.match(path)
 }
 if h == nil {
 h, pattern = NotFoundHandler(), ""
 }
 return
}
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
 // Check for exact match first.
 v, ok := mux.m[path]
 if ok {
 return v.h, v.pattern
 }
 // Check for longest valid match.
 var n = 0
 for k, v := range mux.m {
 if !pathMatch(k, path) {
 continue
 }
 if h == nil || len(k) > n {
 n = len(k)
 h = v.h
 pattern = v.pattern
 }
 }
 return
}
// Does path match pattern?
func pathMatch(pattern, path string) bool {
 if len(pattern) == 0 {
 // should not happen
 return false
 }
 n := len(pattern)
 if pattern[n-1] != '/' {
 return pattern == path
 }
 return len(path) >= n && path[0:n] == pattern
}

match 函数中首先检查精确匹配,如果匹配到,直接返回相应的handler。如果没有匹配,遍历所有注册path,进行pathMatch 检查,满足pathMatch的最长的path胜出。举例说明,main 函数如下:

func main() {
 http.HandleFunc("/bar/", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "Hello")
 })
 http.HandleFunc("/bar/bbb/", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "bbb")
 })
 http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "World")
 })
 http.ListenAndServe(":8080", nil)
}

此时在浏览器中输入http://localhost:8080/foo/aaa,会返回404 page not found ,而输入http://localhost:8080/bar/aaa,会返回Hello。输入http://localhost:8080/bar/bbb/ccc时,/bar//bar/bbb/都会被匹配到,但是/bar/bbb/这个pattern 更长,浏览器会打印出bbb

总结

至此,我们浅析了Golang的路由匹配过程,注册过程将pattern 和相应handler 注册到一个map中,匹配时先检查是否有pattern 和path 完全匹配,如果没有,再检查最长匹配。
整个过程看起来比较简单,直接,但是不能支持正则的路由匹配。
下一篇文章中,将分析mux的源码,学习它的路由匹配方式。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:buptztd

查看原文:Golang 路由匹配浅析[1]

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3231 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏