Golang http server 代碼原理

撥雲見霧

Go 中要實現一個簡單的 Web server 非常的簡單:

package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", HelloServer)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello world!\n")
}

上面這個程序運行後,我們在瀏覽器中訪問http://127.0.0.1:8080就會輸出 "hello world!"。那麼第一個問題來了:http.ListenAndServe做了什麼,它是怎麼和http.HandleFunc關聯起來的?OK,我們先來看一下http.ListenAndServe這個函數:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

這個函數比較簡單,它就利用參數構造了一個Server類型的變量 server,然後調用了這個類型的成員函數ListenAndServe()Server的結構如下:

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
    Addr         string        // TCP address to listen on, ":http" if empty
    Handler      Handler       // handler to invoke, http.DefaultServeMux if nil
    ReadTimeout  time.Duration // maximum duration before timing out read of the request
    WriteTimeout time.Duration // maximum duration before timing out write of the response
    TLSConfig    *tls.Config   // optional TLS config, used by ListenAndServeTLS

    // MaxHeaderBytes controls the maximum number of bytes the
    // server will read parsing the request header's keys and
    // values, including the request line. It does not limit the
    // size of the request body.
    // If zero, DefaultMaxHeaderBytes is used.
    MaxHeaderBytes int

    // TLSNextProto optionally specifies a function to take over
    // ownership of the provided TLS connection when an NPN/ALPN
    // protocol upgrade has occurred. The map key is the protocol
    // name negotiated. The Handler argument should be used to
    // handle HTTP requests and will initialize the Request's TLS
    // and RemoteAddr if not already set. The connection is
    // automatically closed when the function returns.
    // If TLSNextProto is nil, HTTP/2 support is enabled automatically.
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    // ConnState specifies an optional callback function that is
    // called when a client connection changes state. See the
    // ConnState type and associated constants for details.
    ConnState func(net.Conn, ConnState)

    // ErrorLog specifies an optional logger for errors accepting
    // connections and unexpected behavior from handlers.
    // If nil, logging goes to os.Stderr via the log package's
    // standard logger.
    ErrorLog *log.Logger

    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used
}

註釋寫的很明確,Server類型定義了一個 HTTP 服務器的一些參數。另外,而且它還有 4 個成員函數:

func (srv *Server) ListenAndServe() error
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
func (srv *Server) Serve(l net.Listener) error
func (srv *Server) SetKeepAlivesEnabled(v bool)

成員函數ListenAndServe的作用就是監聽srv.Addr指定的 TCP 網絡,並且調用srv.Serve()函數來處理收到的請求:

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
// If srv.Addr is blank, ":http" is used.
// ListenAndServe always returns a non-nil error.
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

再來看這個srv.Serve

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.
//
// Serve always returns a non-nil error.
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
    }

    // TODO: allow changing base context? can't imagine concrete
    // use cases yet.
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        rw, e := l.Accept()
        if e != nil {
            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)
    }
}

可以看到這裏是一個死循環,每收到一個請求,就創建一個 goroutine 去處理這個請求。這裏又出來了一個新的結構conn。代碼中srv.newConn(rw)返回的變量c就是這個類型。我們看下這個結構:

// A conn represents the server side of an HTTP connection.
type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // rwc is the underlying network connection.
    // This is never wrapped by other types and is the value given out
    // to CloseNotifier callers. It is usually of type *net.TCPConn or
    // *tls.Conn.
    rwc net.Conn

    // remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
    // inside the Listener's Accept goroutine, as some implementations block.
    // It is populated immediately inside the (*conn).serve goroutine.
    // This is the value of a Handler'(*Request).RemoteAddr.
    remoteAddr string

    // tlsState is the TLS connection state when using TLS.
    // nil means not TLS.
    tlsState *tls.ConnectionState

    // werr is set to the first write error to rwc.
    // It is set via checkConnErrorWriter{w}, where bufw writes.
    werr error

    // r is bufr's read source. It's a wrapper around rwc that provides
    // io.LimitedReader-style limiting (while reading request headers)
    // and functionality to support CloseNotifier. See *connReader docs.
    r *connReader

    // bufr reads from r.
    // Users of bufr must hold mu.
    bufr *bufio.Reader

    // bufw writes to checkConnErrorWriter{c}, which populates werr on error.
    bufw *bufio.Writer

    // lastMethod is the method of the most recent request
    // on this connection, if any.
    lastMethod string

    // mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
    mu sync.Mutex

    // hijackedv is whether this connection has been hijacked
    // by a Handler with the Hijacker interface.
    // It is guarded by mu.
    hijackedv bool
}

如註釋所示,這個結構描述 / 代表了服務端的一個 HTTP 連接。這個類型也有很多方法,這裏我們只介紹上面調用到的方法:func (c *conn) serve(ctx context.Context),因爲每個 goroutine 執行的就是這個方法。這個內容有點多,我們只保留對我們分析有用的部分:

func (c *conn) serve(ctx context.Context) {
    ...

    serverHandler{c.server}.ServeHTTP(w, w.req)

    ...
}

這裏又涉及到了一個serverHandler以及它的一個方法ServeHTTP

// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
    srv *Server
}

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

serverHandler定義了一個 HTTP 服務,上面c.serve方法中使用c.server初始化了這個 HTTP 服務,然後調用了其ServeHTTP方法。而這個ServeHTTP方法裏面會去c.server裏面找其Handler,如果該Handler不爲 nil,就調用其ServeHTTP方法;如果爲 nil,就用一個全局變量DefaultServeMux來初始化這個Handler,再調用其ServeHTTP方法。也就是說,最終都調用了HandlerServeHTTP方法。讓我們來看看這個Handler

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

這個Handler竟然是個接口,而且只定義了一個ServeHTTP方法。那我們接下來的任務就是去找看誰實現了這個接口。在這之前,我們先總結一下前面分析的東西。

  1. 我們從http.ListenAndServe開始,先是找到了Server這個類型,它用來描述一個運行 HTTP 服務的 Server。而http.ListenAndServe就是調用了這個它的方法ListenAndServe,這個方法又調用了Serve這個方法。在Serve這個方法中,我們看到對於每個請求,都會創建一個 goroutine 去執行conn類型的serve方法。

  2. 然後我們又分析了conn類型,它描述了服務端的一個 HTTP 連接。它的serve方法裏面調用了Handler接口的ServeHTTP方法。

上面的分析基本是根據函數調用來分析的,雖然有點亂,但是還是比較簡單的,而且其實主要就涉及到了Serverconn兩個類型和一個Handler接口。

接下來我們就分析 Go HTTP 中最重要的角色ServeMux

撥雲見日

上面的分析中我們注意到Server結構中的Handler變量有一個默認值DefaultServeMux。它是一個包級別的全局變量,類型是ServeMux,因爲它可以調用ServeHTTP,所以它應該實現了Handler接口。答案是肯定的!

ServeHTTP是 Go 中的 HTTP 請求分發器(HTTP request multiplexer),負責將特定 URL 來的請求分發給特定的處理函數。匹配的規則我摘抄一些 Golang 的文檔,就不翻譯了,基本就是正常思路:

Patterns name fixed, rooted paths, like "/favicon.ico", or rooted subtrees, like "/images/" (note the trailing slash). Longer patterns take precedence over shorter ones, so that if there are handlers registered for both "/images/" and "/images/thumbnails/", the latter handler will be called for paths beginning "/images/thumbnails/" and the former will receive requests for any other paths in the "/images/" subtree.

Note that since a pattern ending in a slash names a rooted subtree, the pattern "/" matches all paths not matched by other registered patterns, not just the URL with Path == "/".

If a subtree has been registered and a request is received naming the subtree root without its trailing slash, ServeMux redirects that request to the subtree root (adding the trailing slash). This behavior can be overridden with a separate registration for the path without the trailing slash. For example, registering "/images/" causes ServeMux to redirect a request for "/images" to "/images/", unless "/images" has been registered separately.

Patterns may optionally begin with a host name, restricting matches to URLs on that host only. Host-specific patterns take precedence over general patterns, so that a handler might register for the two patterns "/codesearch" and "codesearch.google.com/" without also taking over requests for "http://www.google.com/".

ServeMux also takes care of sanitizing the URL request path, redirecting any request containing . or .. elements or repeated slashes to an equivalent, cleaner URL.

然後回到剛開始的那個程序,http.ListenAndServe的第二個參數是 nil,根據前面的分析,它就會使用默認ServeMux類型的變量DefaultServeMux作爲 Handler。然後我們看http.HandleFunc是如何將HelloServer註冊給DefaultServeMux的?

http.HandleFunc函數體如下:

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

可見它還是調用了ServeMuxHandleFunc方法。所以我們還是先來看看這個ServeMux吧:

type ServeMux struct {
    mu    sync.RWMutex //鎖,由於請求涉及到併發處理,因此這裏需要一個鎖機制
    m     map[string]muxEntry  // 路由規則,一個string對應一個mux實體,這裏的string就是註冊的路由表達式
    hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
    explicit bool // 是否精確匹配
    h        Handler // 這個路由表達式對應哪個handler
    pattern  string //匹配字符串
}

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

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

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

上面的代碼塊中,我列了ServeMux結構的定義以及它的幾個重要的方法 (有的方法的實現內容後面講到時再貼)。至此,我們可以看到調用關係了:http.HandleFunc-->func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))-->func (mux *ServeMux) Handle(pattern string, handler Handler)。也就是說最終調用的是ServeMuxHandle方法。有時我們也用http.Handle註冊請求處理函數,其內部也調用的是func (mux *ServeMux) Handle(pattern string, handler Handler)

這裏還有個小細節需要注意:在ServeMuxHandle方法的第二個參數是Handler類型,它是個接口。而func (mux *ServeMux) HandleFunc(handler func(ResponseWriter, *Request))方法的第二個參數是func(ResponseWriter, *Request)類型的,對於我們上面的例子就是HelloServer函數,但是這個函數並沒有實現Handler接口中定義的ServeHTTP(ResponseWriter, *Request)方法,所以它是不能直接傳給Handle方法的的。我們可以看到func (mux *ServeMux) HandleFunc(handler func(ResponseWriter, *Request))函數體是這樣的:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

可以看到,這裏用HandlerFunchandler包裝了一下,就可以作爲Handler接口類型傳給Handle方法了。那這裏的包裝是什麼呢?初步看起來好像是函數調用,但其實是一個類型轉換。沒錯,HandlerFunc是一個類型:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

而且HandlerFunc類型還實現了ServeHTTP方法,也就是說它實現了Handler接口。又因爲我們的處理函數的簽名與它的一致,所以可以強轉。所以說HandlerFunc其實就是一個適配器,它使得的我們可以將普通的函數可以作爲 HTTP 的處理函數,只要這個函數的簽名是func(ResponseWriter, *Request)這樣子的。這也就是爲什麼我們註冊的 HTTP 請求處理函數的簽名都必須寫成這個樣子。不得不說,這也是 Go 中一個非常巧妙的用法。

OK,現在讓我們來看看Handle方法是如何註冊處理函數的:

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

可以看到註冊的過程其實就是構造map[string]muxEntry這個 map 或者往已有的裏面添加值,key 是 url,value 是處理函數以及其他一些必要信息。這樣,註冊過程就算明白了。我們再來看下,不同的請求來了以後,是如何選擇到事先註冊的處理函數的?

回想前面介紹的,每個請求來了以後會創建一個 goroutine 去爲這個請求服務,goroutine 裏面最終執行的是Server結構裏面Handler成員(類型是Handler接口類型)的ServeHTTP方法。這裏的這個Handler就是DefaultServeMuxServeMux類型),所以也就執行的是ServeMuxServeHTTP方法,我們來看一下:

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

這裏如果收到的請求是*,則關閉連接並返回StatusBadRequest。否則執行mux.Handler,我們看下這個函數:

// Handler returns the handler to use for the given request,
// consulting r.Method, r.Host, and r.URL.Path. It always returns
// a non-nil handler. If the path is not in its canonical form, the
// handler will be an internally-generated handler that redirects
// to the canonical path.
//
// Handler also returns the registered pattern that matches the
// request or, in the case of internally-generated redirects,
// the pattern that will match after following the redirect.
//
// If there is no registered handler that applies to the request,
// Handler returns a ``page not found'' handler and an empty pattern.
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            url := *r.URL
            url.Path = p
            return RedirectHandler(url.String(), StatusMovedPermanently), pattern
        }
    }

    return mux.handler(r.Host, r.URL.Path)
}

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

可以看到,函數的核心功能就是根據請求的 url 去之前註冊時構造的 map 裏面查找對應的請求處理函數,並返回這個而處理函數。得到這個處理函數後,就接着上面的執行h.ServeHTTP(w, r)。我們註冊時將我們自定義的請求處理函數強制轉換爲了HandlerFunc類型,所以從 map 裏面取出來的還是這個類型,所以這裏調用的就是這個類型的ServeHTTP方法:

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

可以看到,其實就是執行我們註冊的自定義的請求處理函數。

到這裏就解析完了,但是感覺好亂有木有,好吧,這裏我借用以下 astaxie 在《build-web-application-with-golang》一書中對這個流程的總結吧。對於剛開始的那段代碼,整個執行的流程是這樣的:

  1. 調用了DefaultServeMuxHandleFunc方法

  2. 調用了DefaultServeMuxHandle方法

  3. DefaultServeMuxmap[string]muxEntry中增加對應的 handler 和路由規則

  1. 實例化Server

  2. 調用ServerListenAndServe方法

  3. 調用net.Listen("tcp", addr)監聽端口

  4. 啓動一個 for 循環,在循環體中 Accept 請求

  5. 對每個請求實例化一個Conn,並且開啓一個 goroutine 爲這個請求進行服務go c.serve(ctx)

  6. 讀取每個請求的內容w, err := c.readRequest()

  7. 判斷handler是否爲空,如果沒有設置handler(這個例子就沒有設置 handler),handler 就設置爲DefaultServeMux

  8. 調用handlerServeHTTP

  9. 在這個例子中,下面就進入到DefaultServeMux.ServeHTTP

  10. 根據 request 選擇 handler,並且進入到這個 handler 的ServeHTTP

  11. 選擇 handler:

番外篇

從上面的分析可以看到,之所以我們能夠用 Go 非常容易的寫一個簡單的 Web Server 程序是因爲 Go 不光提供了機制和接口,還爲我們實現了一個版本。比如 Go 實現了一個ServeMux,並內置了一個全局變量DefaultServeMux,還實現了一系列諸如對於HandleFunc之類的函數和方法,使得我們可以非常容易的去註冊請求處理函數,去分發請求。

當然,我們可以不使用 Go 內部實現的ServeMux,而使用我們自己的。一方面是更加有靈活性(當然需要我們自己做更多的編碼工作),另一方面有些人也認爲標準庫中內置一個全局的變量不是一個好的設計與實踐。比如對於剛開始的程序,我們定義自己的 "ServeMux" 來實現:

package main

import (
    "io"
    "log"
    "net/http"
)

type MyMux struct{}

func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        HelloServer(w, r)
        return
    }

    http.NotFound(w, r)
    return
}

func main() {
    mux := &MyMux{}

    log.Fatal(http.ListenAndServe(":8080", mux))
}

func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello world!\n")
}

可以看到,實現自己的 "ServeMux" 其實就是實現Handler接口。當然這裏只是一個示例,實際中,如何進行路由分發纔是大的工作量。所以我覺得內部實現的那個挺好,至少可以減少開發者很多工作量...

除了這個ServeMux可以自定義外,如果我們想對Server進行更多更精細的控制,也可以自定義Server

s := &http.Server{
    Addr:           ":8080",
    Handler:        myHandler,
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

轉自:

niyanchun.com/go-http-server-learning.html

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P7AWbEpDeICWJe2gWw76fw