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))簽名的函數,這里的patternstring類型的,指的是匹配的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)
}

我們可以發現,這個函數,他是屬於一種叫HandlerFunc類型的函數。

在Golang中,這是一種很特別的特性。我們可以將函數設置為一種類型,或者你可以理解成變量,你可以用一個變量名去表示這個函數,可以把這個函數賦值給某一個變量。

fn := func(){
    fmt.Println("x is",x)
}
fn()

所以,在這里這個函數類型也是實現了ServeHTTP方法的,也就是說,這個名為HandlerFunc的函數類型,也是屬於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類型,里面包含了一個HandlerURI。也就是說,通過這個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方法會先判斷傳入的URIhandler是否合法,然后判斷這個URI對應的處理器是否已經注冊,然后將這個URIhandler對應的map寫入ServeMux對象中。

注意,這里還有一個步驟。如果這個URI是以/結尾的,將會被送入es數組中,按長度排序。至於為什么會這么做,我們在后面的內容將會提到。

說完了這些,我們應該可以猜到這個ServeMux對象的作用了。他可以存儲我們注冊的URIHandler,以實現當有請求進來的時候,可以委派給相對應的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,是一個復雜的結構體,里面包含了設置服務器的很多參數,但是這里我們只聊AddrHandler這兩個屬性。

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的默認handlerDefaultServeMux

假設,我們這里使用的是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服務器,然后去了解一下怎么進行緩存,做持久化等,這也是作者思考之后決定的一條學習路線。當能夠把這些內容都研究明白之后,再去研究框架,去看這些框架是怎么解決問題的,可能才是比較合適的。

當然了,作者也是剛入門。所以,可能會有很多的疏漏。如果在閱讀的過程中,有哪些解釋不到位,或者理解出現了偏差,也請你留言指正。

再次感謝~

PS:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM