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's (*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
方法。也就是說,最終都調用了Handler
的ServeHTTP
方法。讓我們來看看這個Handler
:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
這個Handler
竟然是個接口,而且只定義了一個ServeHTTP
方法。那我們接下來的任務就是去找看誰實現了這個接口。在這之前,我們先總結一下前面分析的東西。
-
我們從
http.ListenAndServe
開始,先是找到了Server
這個類型,它用來描述一個運行 HTTP 服務的 Server。而http.ListenAndServe
就是調用了這個它的方法ListenAndServe
,這個方法又調用了Serve
這個方法。在Serve
這個方法中,我們看到對於每個請求,都會創建一個 goroutine 去執行conn
類型的serve
方法。 -
然後我們又分析了
conn
類型,它描述了服務端的一個 HTTP 連接。它的serve
方法裏面調用了Handler
接口的ServeHTTP
方法。
上面的分析基本是根據函數調用來分析的,雖然有點亂,但是還是比較簡單的,而且其實主要就涉及到了Server
和conn
兩個類型和一個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) }
可見它還是調用了ServeMux
的HandleFunc
方法。所以我們還是先來看看這個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)
。也就是說最終調用的是ServeMux
的Handle
方法。有時我們也用http.Handle
註冊請求處理函數,其內部也調用的是func (mux *ServeMux) Handle(pattern string, handler Handler)
。
這裏還有個小細節需要注意:在ServeMux
的Handle
方法的第二個參數是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))
}
可以看到,這裏用HandlerFunc
對handler
包裝了一下,就可以作爲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
就是DefaultServeMux
(ServeMux
類型),所以也就執行的是ServeMux
的ServeHTTP
方法,我們來看一下:
// 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》一書中對這個流程的總結吧。對於剛開始的那段代碼,整個執行的流程是這樣的:
- 首先調用
http.HandleFunc
,然後內部按順序做了以下事情:
-
調用了
DefaultServeMux
的HandleFunc
方法 -
調用了
DefaultServeMux
的Handle
方法 -
往
DefaultServeMux
的map[string]muxEntry
中增加對應的 handler 和路由規則
-
其次調用
http.ListenAndServe(":8080", nil)
,依次做了以下事情 -
判斷是否有路由能滿足這個 request(循環遍歷
ServerMux
的muxEntry
) -
如果有路由滿足,調用這個路由
handler
的ServeHTTP
-
如果沒有路由滿足,調用
NotFoundHandler
的ServeHTTP
-
實例化
Server
-
調用
Server
的ListenAndServe
方法 -
調用
net.Listen("tcp", addr)
監聽端口 -
啓動一個 for 循環,在循環體中 Accept 請求
-
對每個請求實例化一個
Conn
,並且開啓一個 goroutine 爲這個請求進行服務go c.serve(ctx)
-
讀取每個請求的內容
w, err := c.readRequest()
-
判斷
handler
是否爲空,如果沒有設置handler
(這個例子就沒有設置 handler),handler 就設置爲DefaultServeMux
-
調用
handler
的ServeHTTP
-
在這個例子中,下面就進入到
DefaultServeMux.ServeHTTP
-
根據 request 選擇 handler,並且進入到這個 handler 的
ServeHTTP
-
選擇 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