Golang Web入門(1):自頂向下理解Http伺服器
摘要
由於Golang優秀的併發處理,很多公司使用Golang編寫微服務。對於Golang來說,只需要短短几行程式碼就可以實現一個簡單的Http伺服器。加上Golang的協程,這個伺服器可以擁有極高的效能。然而,正是因為程式碼過於簡單,我們才應該去研究他的底層實現,做到會用,也知道為什麼這麼用。
在本文中,會以自頂向下的方式,從如何使用,到如何實現,一點點的分析Golang中net/http這個包中關於Http伺服器的實現方式。內容可能會越來越難理解,作者會盡量把這些原始碼講的更清楚一些,希望對各位有所幫助。
1 建立
首先,我們以怎麼用為起點。
畢竟,知道了怎麼用,才能一步一步的深入挖掘為什麼這麼用。
先來看第一種最簡單的建立方式(省略了導包):
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World !")
}
func main() {
http.HandleFunc("/", helloWorldHandler)
http.ListenAndServe(":8000", nil)
}
其實在這一部分中,程式碼應該很容易理解。就是先做一個對映,把需要訪問的地址,和訪問後執行的函式,寫在一起。然後再加上監聽的埠,就可以了。
如果你是一個Java程式設計師,你應該能發覺這個和Java中的Servlet很相似。也是建立一個個的Servlet,然後註冊。
再來看看第二種建立方式,也一樣省略了導包:
type helloWorldHandler struct { content string } func (handler *helloWorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, handler.content) } func main() { http.Handle("/", &helloWorldHandler{content: "Hello World!"}) http.ListenAndServe(":8000", nil) }
在這裡,我們能發現相較於第一種方法,有些許的改動。
我們定義了一個結構體,然後又給這個結構體編寫了一個方法。根據我們之前對於介面的概念:要實現一個介面必須要實現這個介面的所有方法。
那麼我們是不是可以推測:存在這麼一個介面A,裡面有一個名為ServeHTTP的方法,而我們所編寫的這個結構體,他已經實現了這個介面A了,他現在是屬於這個A型別的一個結構體了。
type A interface{
ServeHTTP()
}
並且,在main函式中關於對映URI和方法的引數部分,需要呼叫實現了這個介面A的一個物件。
帶著這個問題,我們可以繼續往下。
2 註冊
在第一部分,我們提到了兩種註冊方式,一種是傳入一個函式,一種是傳入一個結構體指標。
http.HandleFunc("/", helloWorldHandler)
http.Handle("/", &helloWorldHandler{content: "Hello World!"})
我們來看看http包內的原始碼:
package http
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
先看一下這裡的程式碼,他們被稱為註冊函式。
首先研究一下HandleFunc
這個函式。在main
函式中,呼叫了這個具有func(pattern string, handler func(ResponseWriter, *Request))
簽名的函式,這裡的pattern
是string
型別的,指的是匹配的URI,這個很容易理解。第二個引數是一個具有func(ResponseWriter, *Request)
簽名的函式。
然後我們繼續看,在這個函式中,呼叫了這個方法:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
我們可以看到,最終是呼叫了DefaultServeMux
物件的Handle方法。
好,先到這裡,我們再看一看剛剛提到的簽名為func (pattern string, handler Handler)
另外一個函式。在這個函式裡面,同樣是呼叫了DefaultServeMux
物件的Handle方法。
也就是說,無論我們使用哪種註冊函式,最終呼叫的都是這個函式:
func (mux *ServeMux) Handle(pattern string, handler Handler)
這裡涉及到了兩種物件,第一是ServeMux
物件,第二是Handler
物件。
ServeMux
物件我們一會再聊,先聊聊Handler
物件。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
在Golang中,Handler是一種介面型別,只要實現了ServeHTTP
這個方法,那個就可以稱這個結構體是Handler
型別的。
注意到,在前面有一行程式碼是這樣的:
mux.Handle(pattern, HandlerFunc(handler))
有人可能會想,HandlerFunc func(ResponseWriter, *Request)
這個函式,是輸入一個函式,返回一個Handler
型別的物件,其實這是不對的。我們來看看這個函式的原始碼:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
我們可以發現,這個函式,他是一個結構體型別,而且這個結構體也是實現了ServeHTTP
方法的,也就是說,這個結構體也是一個Handler
型別。所以,這個方法其實並不是輸入一組引數,返回一個Handler
型別,而是他本身就是一個Handler
型別,可以直接呼叫ServeHTTP
方法。
這裡比較繞,但是相信當你理解了之後,會感覺妙啊。
說完了Handler
,我們再來聊聊ServeMux
。先來看看他的結構:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
我們先關注一下這個結構裡面的m
欄位。這個欄位是一個map
型別,key是URI
,value是muxEntry
型別。而這個muxEntry
型別,裡面包含了一個Handler
和URI
。也就是說,通過這個m
欄位,我們可以用URI
找到對應的Handler
物件。
繼續說回上面提到的func (mux *ServeMux) Handle(pattern string, handler Handler)
方法。我們已經知道了呼叫這個方法的物件是ServeMux
,也知道了這個方法的引數中的Handler
是什麼,下面讓我們來看看這個方法的詳細實現:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
在這個方法中,我們可以看到,Handle方法會先判斷傳入的URI
和handler
是否合法,然後判斷這個URI
對應的處理器是否已經註冊,然後將這個URI
和handler
對應的map
寫入ServeMux
物件中。
注意,這裡還有一個步驟。如果這個URI
是以/
結尾的,將會被送入es陣列
中,按長度排序。至於為什麼會這麼做,我們在後面的內容將會提到。
說完了這些,我們應該可以猜到這個ServeMux
物件的作用了。他可以儲存我們註冊的URI
和Handler
,以實現當有請求進來的時候,可以委派給相對應的Handler
的功能。
考慮到這個功能,那麼我們也可以推斷出,這個ServeMux
也是一個Handler
,只不過他和其他的Handler
不同。其他的Handler
處理的是具體的請求,而這個ServeMux
處理的是請求的分配。
所以,ServeMux也實現了ServeHTTP方法,他也是一個Handler。而對於他是怎麼實現ServeHTTP方法的,我們也在後面的內容提到。
3 監聽
現在,讓我們來聊聊main函式中的第二行:
http.ListenAndServe(":8000", nil)
按照慣例,我們來看一看這個方法的實現:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
這裡的Server,是一個複雜的結構體,裡面包含了設定伺服器的很多引數,但是這裡我們只聊Addr
和Handler
這兩個屬性。
Addr
很容易理解,就是這個伺服器所監聽的地址。
Handler
是處理器,負責把請求分配給各個對應的handler
。在這裡留空,則使用Golang預設的處理器,也就是上文中我們提到的實現了ServeHTTP
方法的ServeMux
。
知道了這些,我們繼續往下看server.ListenAndServe()
的實現:
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
這裡比較重要的有兩行,第一是ln, err := net.Listen("tcp", addr)
,也就是說,開始監聽addr
這個地址的tcp連線。
然後,呼叫srv.Serve(ln)
,我們來看看程式碼(省略部分,只保留與本文有關的邏輯):
func (srv *Server) Serve(l net.Listener) error {
...
for{
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
簡單來講,在這個方法中,有一個死迴圈,他不斷接收新的連線,然後啟動一個協程,處理這個連線。我們來看看c.serve(connCtx)
的具體實現:
func (c *conn) serve(ctx context.Context) {
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
省略其他所有的細節,最關鍵的就是這一行程式碼了,然後我們再看看這個ServeHTTP
方法。注意,這裡的c.server
,還是指的是最開始的那個Server結構體。堅持一下下,馬上就到最關鍵的地方啦:
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)
}
這裡的ServeHTTP
方法邏輯很容易看出,如果最開始沒有定義一個全域性處理的Handler
,則會使用Golang的預設handler
:DefaultServeMux
。
假設,我們這裡使用的是DefaultServeMux
,執行ServeHTTP
方法。說到這裡你是否有印象,我們在上一個章節裡提到的:
所以,ServeMux也實現了ServeHTTP方法,他也是一個Handler。而對於他是怎麼實現ServeHTTP方法的,我們也在後面的內容提到。
就是這裡,對於ServeMux
來說,他就是一個處理請求分發的Handler
。
如果你學過Java,我跟你說他和ServletDispatcher
很相似,你應該能理解吧。
4 處理
到了這裡,就是最後一步了,我們來看看這裡處理請求分發的ServeHTTP
方法具體實現:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
在省去其他細節之後我們應該可以推斷,這個mux.Handler(r)
方法返回的h
,應該是所請求的URI
所對應的Handler
。然後,執行這個Handler
所對應的ServeHTTP
方法。我們來看看mux.Handler(r)
這個方法:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
...
return mux.handler(host, r.URL.Path)
}
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
}
到了這裡,程式碼就變得簡潔明瞭了。重點就是這個mux.match
方法,會根據地址,來返回對應的Handler。我們來看看這個方法:
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. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
這段程式碼也應該很容易理解。如果在ServeMux
中儲存了key
為這個URI的路由規則的對映,則直接返回這個URI
對應的Handler
。
否則,就去匹配es陣列
。還記得嗎,這個陣列是之前註冊路由的時候提到的,如果URI
是以/
結尾的,就會把這個路由對映新增到es陣列中
,並由長到短進行排序。
這樣的作用是,可以優先匹配到最長的URI
,以達到近似匹配的時候能夠匹配到最合適的路由的目的。
至此,返回對應的Handler
,然後執行,就成功的實現了處理相對應的請求了。
寫在最後
首先,謝謝你能看到這裡!
不知道你有沒有理解我所說的內容,希望這篇文章可以給你一些幫助。
其實寫這篇文章的目的是這樣的,學完了Golang的基礎之後作者準備開始研究Golang Web。但是查詢各種資料後發現,並沒有找到一條很合適的學習路線。然後本來作者打算去直接研究一個框架,如MeeGo,Gin等。但是又考慮到,框架只是用來解決問題的,學會了框架卻不知道基礎內容,有種知其然不知其所以然的感覺。
所以,作者打算從Golang的net/http包的原始碼開始,慢慢去了解怎麼用原生的Go語言去建立一個HTTP伺服器,然後去了解一下怎麼進行快取,做持久化等,這也是作者思考之後決定的一條學習路線。當能夠把這些內容都研究明白之後,再去研究框架,去看這些框架是怎麼解決問題的,可能才是比較合適的。
當然了,作者也是剛入門。所以,可能會有很多的疏漏。如果在閱讀的過程中,有哪些解釋不到位,或者理解出現了偏差,也請你留言指正。
再次感