golang http server原始碼解讀
1. 初識
http 是典型的 C/S 架構,客戶端向服務端傳送請求(request),服務端做出應答(response)。
golang 的標準庫 net/http
提供了 http 程式設計有關的介面,封裝了內部TCP連線和報文解析的複雜瑣碎的細節,使用者只需要和 http.request
和 http.ResponseWriter
兩個物件互動就行。也就是說,我們只要寫一個
handler,請求會通過引數傳遞進來,而它要做的就是根據請求的資料做處理,把結果寫到 Response 中。廢話不多說,來看看 hello world 程式有多簡單吧!
package main
import (
"io"
"net/http"
)
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}
func main() {
http.Handle("/", &helloHandler{})
http.ListenAndServe(":12345", nil)
}
執行 go run hello_server.go
,我們的伺服器就會監聽在本地的 12345
hello,
world!
:
正如上面程式展示的那樣,我們只要實現的一個 Handler,它的 介面原型 是(也就是說只要實現了 ServeHTTP
方法的物件都可以作為
Handler):
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
然後,註冊到對應的路由路徑上就 OK 了。
http.HandleFunc
接受兩個引數:第一個引數是字串表示的 url 路徑,第二個引數是該 url 實際的處理物件。
http.ListenAndServe
監聽在某個埠,啟動服務,準備接受客戶端的請求(第二個引數這裡設定為 nil
http.Request
,呼叫對應的
handler 的 ServeHTTP
方法,然後把操作後的 http.ResponseWriter
解析,返回到客戶端。
2. 封裝
上面的程式碼沒有什麼問題,但是有一個不便:每次寫 Handler 的時候,都要定義一個型別,然後編寫對應的 ServeHTTP
方法,這個步驟對於所有 Handler
都是一樣的。重複的工作總是可以抽象出來, net/http
也正這麼做了,它提供了 http.HandleFunc
方法,允許直接把特定型別的函式作為
handler。上面的程式碼可以改成:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!\n")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":12345", nil)
}
其實, HandleFunc
只是一個介面卡,
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
自動給 f
函式添加了 HandlerFunc
這個殼,最終呼叫的還是 ServerHTTP
,只不過會直接使用 f(w,
r)
。這樣封裝的好處是:使用者可以專注於業務邏輯的編寫,省去了很多重複的程式碼處理邏輯。如果只是簡單的 Handler,會直接使用函式;如果是需要傳遞更多資訊或者有複雜的操作,會使用上部分的方法。
如果需要我們自己寫的話,是這樣的:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!\n")
}
func main() {
// 通過 HandlerFunc 把函式轉換成 Handler 介面的實現物件
hh := http.HandlerFunc(helloHandler)
http.Handle("/", hh)
http.ListenAndServe(":12345", nil)
}
3. 預設
大部分的伺服器邏輯都需要使用者編寫對應的 Handler,不過有些 Handler 使用頻繁,因此 net/http
提供了它們的實現。比如負責檔案 hosting
的 FileServer
、負責 404 的 NotFoundHandler
和
負責重定向的 RedirectHandler
。下面這個簡單的例子,把當前目錄所有檔案 host 到服務端:
package main
import (
"net/http"
)
func main() {
http.ListenAndServe(":12345", http.FileServer(http.Dir(".")))
}
強大吧!只要一行邏輯程式碼就能實現一個簡單的靜態檔案伺服器。從這裡可以看出一件事: http.ListenAndServe
第二個引數就是一個 Handler 函式(請記住這一點,後面有些內容依賴於這個)。
執行這個程式,在瀏覽器中開啟 http://127.0.0.1:12345
,可以看到所有的檔案,點選對應的檔案還能看到它的內容。
其他兩個 Handler,這裡就不再舉例子了,讀者可以自行參考文件。
4. 路由
雖然上面的程式碼已經工作,並且能實現很多功能,但是實際開發中,HTTP 介面會有許多的 URL 和對應的 Handler。這裡就要講 net/http
的另外一個重要的概念: ServeMux
。 Mux
是 multiplexor
的縮寫,就是多路傳輸的意思(請求傳過來,根據某種判斷,分流到後端多個不同的地方)。 ServeMux
可以註冊多了
URL 和 handler 的對應關係,並自動把請求轉發到對應的 handler 進行處理。我們還是來看例子吧:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.URL.Path)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/", echoHandler)
http.ListenAndServe(":12345", mux)
}
這個伺服器的功能也很簡單:如果在請求的 URL 是 /hello
,就返回 hello,
world!
;否則就返回 URL 的路徑,路徑是從請求物件 http.Requests
中提取的。
這段程式碼和之前的程式碼有兩點區別:
- 通過
NewServeMux
生成了ServerMux
結構,URL 和 handler 是通過它註冊的 http.ListenAndServe
方法第二個引數變成了上面的mux
變數
還記得我們之前說過, http.ListenAndServe
第二個引數應該是 Handler 型別的變數嗎?這裡為什麼能傳過來 ServeMux
?嗯,估計你也猜到啦: ServeMux
也是是 Handler
介面的實現,也就是說它實現了 ServeHTTP
方法,我們來看一下:
type ServeMux struct {
// contains filtered or unexported fields
}
func NewServeMux() *ServeMux
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
哈!果然,這裡的方法我們大都很熟悉,除了 Handler()
返回某個請求的 Handler。 Handle
和 HandleFunc
這兩個方法 net/http
也提供了,後面我們會說明它們之間的關係。而 ServeHTTP
就是 ServeMux
的核心處理邏輯: 根據傳遞過來的
Request,匹配之前註冊的 URL 和處理函式,找到最匹配的項,進行處理。 可以說 ServeMux
是個特殊的 Handler,它負責路由和呼叫其他後端
Handler 的處理方法。
關於 ServeMux
,有幾點要說明:
- URL 分為兩種,末尾是
/
:表示一個子樹,後面可以跟其他子路徑; 末尾不是/
,表示一個葉子,固定的路徑 - 以
/
結尾的 URL 可以匹配它的任何子路徑,比如/images
會匹配/images/cute-cat.jpg
- 它採用最長匹配原則,如果有多個匹配,一定採用匹配路徑最長的那個進行處理
- 如果沒有找到任何匹配項,會返回 404 錯誤
ServeMux
也會識別和處理.
和..
,正確轉換成對應的 URL 地址
你可能會有疑問?我們之間為什麼沒有使用 ServeMux
就能實現路徑功能?那是因為 net/http
在後臺預設建立使用了 DefaultServeMux
。
5. 深入
嗯,上面基本覆蓋了編寫 HTTP 服務端需要的所有內容。這部分就分析一下,它們的原始碼實現,加深理解,以後遇到疑惑也能通過原始碼來定位和解決。
Server
首先來看 http.ListenAndServe()
:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
這個函式其實也是一層封裝,建立了 Server
結構,並呼叫它的 ListenAndServe
方法,那我們就跟進去看看:
// 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
......
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections. If
// srv.Addr is blank, ":http" is used.
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)})
}
Server
儲存了執行 HTTP 服務需要的引數,呼叫 net.Listen
監聽在對應的
tcp 埠, tcpKeepAliveListener
設定了 TCP 的 KeepAlive
功能,最後呼叫 srv.Serve()
方法開始真正的迴圈邏輯。我們再跟進去看看 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.
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
// 迴圈邏輯,接受請求並處理
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
// 建立 Conn 連線
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
// 啟動新的 goroutine 進行處理
go c.serve()
}
}
最上面的註釋也說明了這個方法的主要功能:
- 接受
Listener l
傳遞過來的請求 - 為每個請求建立 goroutine 進行後臺處理
- goroutine 會讀取請求,呼叫
srv.Handler
func (c *conn) serve() {
origConn := c.rwc // copy it before it's set nil on Close or Hijack
...
for {
w, err := c.readRequest()
if c.lr.N != c.server.initialLimitedReaderSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
serverHandler{c.server}.ServeHTTP(w, w.req)
w.finishRequest()
if w.closeAfterReply {
if w.requestBodyLimitHit {
c.closeWriteAndWait()
}
break
}
c.setState(c.rwc, StateIdle)
}
}
看到上面這段程式碼 serverHandler{c.server}.ServeHTTP(w, w.req)
這一句了嗎?它會呼叫最早傳遞給 Server
的
Handler 函式:
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)
}
哇!這裡看到 DefaultServeMux
了嗎?如果沒有 handler 為空,就會使用它。 handler.ServeHTTP(rw,
req)
,Handler 介面都要實現 ServeHTTP
這個方法,因為這裡就要被呼叫啦。
也就是說,無論如何,最終都會用到 ServeMux
,也就是負責 URL 路由的傢伙。前面也已經說過,它的 ServeHTTP
方法就是根據請求的路徑,把它轉交給註冊的
handler 進行處理。這次,我們就在原始碼層面一探究竟。
ServeMux
我們已經知道, ServeMux
會以某種方式儲存 URL 和 Handlers 的對應關係,下面我們就從程式碼層面來解開這個祕密:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry // 存放路由資訊的字典!\(^o^)/
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
explicit bool
h Handler
pattern string
}
沒錯,資料結構也比較直觀,和我們想象的差不多,路由資訊儲存在字典中,接下來就看看幾個重要的操作:路由資訊是怎麼註冊的? ServeHTTP
方法到底是怎麼做的?路由查詢過程是怎樣的?
// 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)
}
// 建立 `muxEntry` 並新增到路由字典中
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
// 這是一個很有用的小技巧,如果註冊了 `/tree/`, `serveMux` 會自動新增一個 `/tree` 的路徑並重定向到 `/tree/`。當然這個 `/tree` 路徑會被使用者顯示的路由資訊覆蓋。
// 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, "/"):]
}
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
}
}
路由註冊沒有什麼特殊的地方,很簡單,也符合我們的預期,注意最後一段程式碼對類似 /tree
URL 重定向的處理。
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux)