Golang 路由匹配淺析[1]

前言

在本文中以及下篇文章中,咱們會研習Golang 的源碼來探究Golang 是如何實現HTTP URL 匹配的,並對比 mux的實現。
本人水平有限,若有疏漏和不正確的地方,還請各位不吝賜教,多謝!git

Golang 源碼基於1.9.2

正文

咱們有這樣一個HTTP 服務器程序:github

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是這樣定義的app

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 的註冊過程socket

// 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 函數:tcp

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 服務器url

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的源碼,學習它的路由匹配方式。

相關文章
相關標籤/搜索