Golang 路由匹配淺析[1]

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

前言

在本文中以及下篇文章中,我們會研習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/barRequest Method: GETStatus Code: 301 Moved PermanentlyRemote Address: 127.0.0.1:8080
Request URL: http://localhost:8080/bar/Request Method: GETStatus 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的源碼,學習它的路由匹配方式。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.