Golang 路由匹配浅析[1]

前端之家收集整理的这篇文章主要介绍了Golang 路由匹配浅析[1]前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

前言

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

Golang 源码基于1.9.2

正文

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

  1. func main() {
  2. http.HandleFunc("/bar",func(w http.ResponseWriter,r *http.Request) {
  3. fmt.Fprintln(w,"Hello")
  4. })
  5.  
  6. http.HandleFunc("/foo","World")
  7. })
  8.  
  9. http.ListenAndServe(":8080",nil)
  10. }

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

注册

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

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

muxEntry是这样定义的

  1. type muxEntry struct {
  2. explicit bool
  3. h Handler
  4. pattern string
  5. }

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

  1. // Handle registers the handler for the given pattern.
  2. // If a handler already exists for pattern,Handle panics.
  3. func (mux *ServeMux) Handle(pattern string,handler Handler) {
  4. mux.mu.Lock()
  5. defer mux.mu.Unlock()
  6.  
  7. if pattern == "" {
  8. panic("http: invalid pattern " + pattern)
  9. }
  10. if handler == nil {
  11. panic("http: nil handler")
  12. }
  13. if mux.m[pattern].explicit {
  14. panic("http: multiple registrations for " + pattern)
  15. }
  16.  
  17. if mux.m == nil {
  18. mux.m = make(map[string]muxEntry)
  19. }
  20. mux.m[pattern] = muxEntry{explicit: true,h: handler,pattern: pattern}
  21.  
  22. if pattern[0] != '/' {
  23. mux.hosts = true
  24. }
  25.  
  26. // Helpful behavior:
  27. // If pattern is /tree/,insert an implicit permanent redirect for /tree.
  28. // It can be overridden by an explicit registration.
  29. n := len(pattern)
  30. if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
  31. // If pattern contains a host name,strip it and use remaining
  32. // path for redirect.
  33. path := pattern
  34. if pattern[0] != '/' {
  35. // In pattern,at least the last character is a '/',so
  36. // strings.Index can't be -1.
  37. path = pattern[strings.Index(pattern,"/"):]
  38. }
  39. url := &url.URL{Path: path}
  40. mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(),StatusMovedPermanently),pattern: pattern}
  41. }
  42. }

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

  1. func main() {
  2. http.HandleFunc("/bar/",nil)
  3. }

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

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

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

匹配

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

  1. func main() {
  2.  
  3. fmt.Println("Launching server...")
  4.  
  5. // listen on all interfaces
  6. ln,_ := net.Listen("tcp",":8081")
  7.  
  8. for {
  9. // accept connection on port
  10. conn,_ := ln.Accept()
  11.  
  12. // will listen for message to process ending in newline (\n)
  13. message,_ := bufio.NewReader(conn).ReadString('\n')
  14. // output message received
  15. fmt.Print("Message Received:",string(message))
  16. // sample process for string received
  17. newmessage := strings.ToUpper(message)
  18. // send new string back to client
  19. conn.Write([]byte(newmessage + "\n"))
  20. }
  21. }

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

  1. func (srv *Server) Serve(l net.Listener) error {
  2. defer l.Close()
  3. if fn := testHookServerServe; fn != nil {
  4. fn(srv,l)
  5. }
  6. var tempDelay time.Duration // how long to sleep on accept failure
  7.  
  8. if err := srv.setupHTTP2_Serve(); err != nil {
  9. return err
  10. }
  11.  
  12. srv.trackListener(l,true)
  13. defer srv.trackListener(l,false)
  14.  
  15. baseCtx := context.Background() // base is always background,per Issue 16220
  16. ctx := context.WithValue(baseCtx,ServerContextKey,srv)
  17. for {
  18. rw,e := l.Accept()
  19. if e != nil {
  20. select {
  21. case <-srv.getDoneChan():
  22. return ErrServerClosed
  23. default:
  24. }
  25. if ne,ok := e.(net.Error); ok && ne.Temporary() {
  26. if tempDelay == 0 {
  27. tempDelay = 5 * time.Millisecond
  28. } else {
  29. tempDelay *= 2
  30. }
  31. if max := 1 * time.Second; tempDelay > max {
  32. tempDelay = max
  33. }
  34. srv.logf("http: Accept error: %v; retrying in %v",e,tempDelay)
  35. time.Sleep(tempDelay)
  36. continue
  37. }
  38. return e
  39. }
  40. tempDelay = 0
  41. c := srv.newConn(rw)
  42. c.setState(c.rwc,StateNew) // before Serve can return
  43. go c.serve(ctx)
  44. }
  45. }

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

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

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

  1. serverHandler{c.server}.ServeHTTP(w,w.req)
  2.  
  3. func (sh serverHandler) ServeHTTP(rw ResponseWriter,req *Request) {
  4. handler := sh.srv.Handler
  5. if handler == nil {
  6. handler = DefaultServeMux
  7. }
  8. if req.RequestURI == "*" && req.Method == "OPTIONS" {
  9. handler = globalOptionsHandler{}
  10. }
  11. handler.ServeHTTP(rw,req)
  12. }

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

  1. http.ListenAndServe(":8080",nil)

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

  1. // ServeHTTP dispatches the request to the handler whose
  2. // pattern most closely matches the request URL.
  3. func (mux *ServeMux) ServeHTTP(w ResponseWriter,r *Request) {
  4. if r.RequestURI == "*" {
  5. if r.ProtoAtLeast(1,1) {
  6. w.Header().Set("Connection","close")
  7. }
  8. w.WriteHeader(StatusBadRequest)
  9. return
  10. }
  11. h,_ := mux.Handler(r)
  12. h.ServeHTTP(w,r)
  13. }

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

  1. // handler is the main implementation of Handler.
  2. // The path is known to be in canonical form,except for CONNECT methods.
  3. func (mux *ServeMux) handler(host,path string) (h Handler,pattern string) {
  4. mux.mu.RLock()
  5. defer mux.mu.RUnlock()
  6.  
  7. // Host-specific pattern takes precedence over generic ones
  8. if mux.hosts {
  9. h,pattern = mux.match(host + path)
  10. }
  11. if h == nil {
  12. h,pattern = mux.match(path)
  13. }
  14. if h == nil {
  15. h,pattern = NotFoundHandler(),""
  16. }
  17. return
  18. }
  19.  
  20. // Find a handler on a handler map given a path string.
  21. // Most-specific (longest) pattern wins.
  22. func (mux *ServeMux) match(path string) (h Handler,pattern string) {
  23. // Check for exact match first.
  24. v,ok := mux.m[path]
  25. if ok {
  26. return v.h,v.pattern
  27. }
  28.  
  29. // Check for longest valid match.
  30. var n = 0
  31. for k,v := range mux.m {
  32. if !pathMatch(k,path) {
  33. continue
  34. }
  35. if h == nil || len(k) > n {
  36. n = len(k)
  37. h = v.h
  38. pattern = v.pattern
  39. }
  40. }
  41. return
  42. }
  43.  
  44. // Does path match pattern?
  45. func pathMatch(pattern,path string) bool {
  46. if len(pattern) == 0 {
  47. // should not happen
  48. return false
  49. }
  50. n := len(pattern)
  51. if pattern[n-1] != '/' {
  52. return pattern == path
  53. }
  54. return len(path) >= n && path[0:n] == pattern
  55. }

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

  1. func main() {
  2. http.HandleFunc("/bar/","Hello")
  3. })
  4.  
  5. http.HandleFunc("/bar/bbb/","bbb")
  6. })
  7.  
  8. http.HandleFunc("/foo",nil)
  9. }

此时在浏览器中输入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的源码,学习它的路由匹配方式。

猜你在找的Go相关文章