深入剖析GoWeb服務器實現原理

NO IMAGE

1. 前言

對於Go語言來說,只需要短短几行代碼,就可以實現一個簡單的http server,加上協程的加持,Go實現的http server擁有非常優秀的性能。如下圖所示:

深入剖析GoWeb服務器實現原理

通過net/http標準庫,我們可以啟動一個http服務器,然後讓這個服務器接收請求並返回響應。net/http標準庫還提供了一個連接多路複用器(multiplexer)的接口以及一個默認的多路複用器。

本文會對Go標準庫net/http服務的實現原理進行較為深入的探究,幫助讀者學習網絡編程的設計範式和常見解決問題的思路。本文主要分為三部分:

  • 第一部分簡單介紹一下web服務器的代碼簡單實現。
  • 第二部分是web服務器的實現原理和使用技巧,是本文的重點,筆者會由淺入深的介紹Go語言實現web服務器各個環節所涉及到的概念和實現
  • 第三部分是源碼實現,讀者理解了第二部分的內容,會發現這部分內容不是特別的晦澀難懂。

2. Go Web服務器簡單實現

Go語言創建一個服務器的步驟非常簡單,只要調用ListenAndServe並傳入網絡地址以及負責處理請求的處理器(handler)作為參數就可以了。如果網絡地址為空字符串,那麼服務器默認使用80端口進行網絡連接;如果處理器參數為nil,那麼服務器將使用默認的多路複用器DefaultServeMux。代碼實現:

package main
import "net/http"
func main() {
http.ListenAndServe("", nil)
}

除了可以通過ListenAndServe的參數對服務器的網絡地址和處理器進行配置之外,還可以通過Server結構對服務器進行更詳細的配置,其中包括為請求讀取操作設置超時時間,為響應寫入操作設置超時時間、為Server結構設置錯誤日誌記錄器等。

package main
import "net/http"
func main() {
server := http.Server{
Addr:    "127.0.0.1:8080",
Handler: nil,
}
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
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
}

3. web服務器的實現原理和使用技巧

前邊我們啟動了一個web服務器,但是我們訪問這個服務器地址會得到:404 page not found 的信息。出現這一問題的原因是我們沒有為服務器編寫任何處理器,服務器的多路複用器在接收到請求之後找不到任何處理器來處理請求,因此它只能返回一個404響應。為了讓服務器能夠產生實際的響應結果,我們需要編寫處理器。

3.1 處理器和處理器函數

處理器和處理器函數是不同的,本節我們會詳細談談它們的定義。在Go語言中,一個處理器就是一個擁有ServeHTTP方法的接口,這個ServeHTTP方法需要接收兩個參數:第一個參數是一個ResponseWriter接口,而第二個參數則是一個指向Request結構的指針。換句話說,任何接口只要擁有一個ServeHTTP方法,並且該方法帶有以下簽名,那麼它就是一個處理器:

ServeHTTP(http.ResponseWriter, *http.Request)

不知道讀者有沒有想過一個問題: ServeHTTP為什麼要接受ResponseWriter接口和一個指向Request結構的指針作為參數呢?接收Request指針的原因很簡單:為了讓服務器能夠覺察到處理器對Request結構的修改,我們必須要以傳引用(pass by reference)而不是傳值(pass by value)的方式傳遞Request結構。但是另一方面,為什麼ServeHTTP卻是以傳值的方式接受ResponseWriter呢?難道服務器不需要知道處理器對ResponseWriter所做的修改嗎?對於這個問題,筆者深入探究了一下net/http庫的源碼,在這裡給出答案:ResonseWriter實際上就是response這個非導出結構的接口,而ResponseWriter在使用response結構時,傳遞的也是指向response 結構的指針,也就是說,ResponseWriter 是以傳引用而是傳值的方式在使用response結構。換句話說:實際上ServeHTTP函數的兩個參數傳遞的都是引用而不是值——雖然ResponseWriter看上去是一個值,但它卻是一個帶有結構指針的接口

為了搞清楚另外一個問題,我們不得不再離題一下。既然ListenAndServe第二個參數是一個處理器,那麼為何它的默認值卻是多路複用器DefaultServeMux呢?原來DefaultServeMux是ServeMux結構的一個實例,ServeMux結構也擁有ServeHTTP方法,並且這個方法的簽名與成為處理器所需要的簽名完全一致。所以有了結論:DefaultServeMux不僅是一個多路複用器,它還是一個處理器。不過DefaultServeMux處理器與其他的處理器不同,DefaultServeMux是一個特殊的處理器,它唯一要做的就是根據請求的URL將請求重定向到不同的處理器。

有了這個結論以後,我們就可以自行編寫一個處理器並使用它去代替默認的多路複用器,使服務器能夠對客戶端正常響應了,代碼實現:

package main
import (
"fmt"
"net/http"
)
type MyHandler struct {}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world!")
}
func main() {
handler := MyHandler{}
server := http.Server{
Addr:    "127.0.0.1:8080",
Handler: &handler,
}
server.ListenAndServe()
}

現在,我們再次通過瀏覽器訪問http://localhost:8080/得到的結果將是:hello world!

但是無論我們訪問http://localhost:8080/的任何路由得到的結果始終是hello world!,比如http://localhost:8080/get/list。造成這個問題的原因很明顯:我們自己創建了一個處理器並與服務器綁定,替換了原來使用的默認多路複用器(DefaultServeMux),也就是服務器不會再通過URL匹配將請求導向到不同的處理器,而是直接使用自己的處理程序處理所有的請求,這並不是我們想要的!我們繼續使用DefaultServeMux作為處理器好了,然後通過http.Handle函數將處理器綁定至DefaultServeMux。Handle函數是為了操作便利而創建的函數,調用它等同於調用了DefaultServeMux的Handle方法。代碼如下:

package main
import (
"fmt"
"net/http"
)
type HelloHandler struct {}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}
type WorldHandler struct {}
func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "world!")
}
func main() {
server := http.Server{
Addr:    "127.0.0.1:8080",
}
hello := HelloHandler{}
http.Handle("/hello", &hello)
world := WorldHandler{}
http.Handle("/world", &world)
server.ListenAndServe()
}

知道了什麼是處理器,我們再來看一下什麼是處理器函數:處理器函數實際上就是與處理器擁有相同行為的函數,這些函數與ServeHTTP方法擁有相同的簽名。我們將上邊的處理器方法簡化一下,代碼如下:

package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}
func world(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "world!")
}
func main() {
server := http.Server{
Addr:              "127.0.0.1:8080",
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/world", world)
server.ListenAndServe()
}

處理器函數的實現原理也很簡單:Go語言擁有一種HandlerFunc函數類型,它可以把一個帶有正確簽名的函數f轉換成一個帶有方法f的Handler。比如說hello函數,程序只需要執行以下代碼:

helloHandler := HandlerFunc(hello)

就可以把helloHandler設置成為一個Handler。後邊我們會通過源碼再來回顧以下HandleFunc的實現原理,我們現在只需要記住結論就好了:處理器函數只不過是創建處理器的一種便利的方法而已

3.2 ServeMux 和 DefaultServeMux

我們之前對ServeMux和DefaultServeMux做過介紹,ServeMux是一個HTTP請求多路複用器,它負責接收HTTP請求並根據請求中的URL將請求重定向到正確的處理器,ServeMux也實現了ServeHTTP方法,它也是一個處理器。當ServeMux的ServeHTTP方法接收到一個請求的時候,它會在結構的映射裡面找出與被請求URL最為匹配的URL,然後調用與之相對應的處理器的ServeHTTP方法,如圖所示:

深入剖析GoWeb服務器實現原理

因為ServeMux是一個結構而不是一個接口,所以DefaultServeMux並不是ServeMux的實現。DefaultServeMux實際上是ServeMux的一個實例,並且所有引入了net/http標準庫的程序都可以使用這個實例。當用戶沒有為Server結構指定處理器時,服務器就會使用DefaultServeMux作為ServeMux的默認實例。

上邊的例子中,請求URL /hello完美的匹配了多路複用器綁定的URL,但是如果我們使用瀏覽器訪問/hello/china呢?那麼服務器會返回什麼響應呢?

這個問題的答案跟我們綁定的URL方法相關:匹配不成功的URL會根據URL的層級逐層下降,直到最終落到根URL上。當瀏覽器訪問/hello/china時,會返回什麼結果呢?很多人會認為因為我們的程序已經綁定了/hello,所以響應結果應該是hello。相信試驗過後的讀者可能會有所失望,服務器並沒有這樣去做,因為程序在綁定url使用的是/hello而不是/hello/。如果被綁定的url不是以/結尾,那麼它只能會與完全相同的url匹配;但是如果被綁定的url以/結尾,那麼即使請求的url只有前綴部分與被綁定的url相同,ServeMux也會認為這兩個url是匹配的。(留心的讀者可能會發現,這和Nginx的策略是相同的)

ServeMux的一個缺陷是無法使用變量實現URL模式匹配,比如/get/post/123,我們的本意是獲得id為123的帖子內容,但是我們使用ServeMux並不能實現這樣的功能。所以才有了業界著名的HttpRouter包的出現。

3.3 優雅的實現Web框架中間件

我們上邊瞭解瞭如何Go語言如何實現一個服務器,知道了很多概念和原理,這很枯燥,本節我們來實戰一個例子:優雅的實現Web框架中間件。

中間件是什麼,在這裡無需多言,利用中間件技術可以將業務代碼和非業務代碼解耦。我們來看一下我們要實現中間件的用法:

r := router.NewRouter()
r.Use(timeMiddleware)
r.Add("/", http.HandlerFunc(hello))
r.Run(":8080")

相信沒有比這更簡潔、更靈活的中間件了,實現它卻非常簡單:

package router
import "net/http"
type middleware func(http.Handler) http.Handler
type Router struct {
middlewareChain [] middleware
mux map[string] http.Handler
}
func NewRouter() *Router{
return &Router{
[]middleware{},
make(map[string] http.Handler),
}
}
func (r *Router) Use(m middleware) {
r.middlewareChain = append(r.middlewareChain)
}
func (r *Router) Add(route string, h http.Handler) {
var mergeHandler = h
for i := len(r.middlewareChain) - 1; i >= 0; i-- {
mergeHandler = r.middlewareChain[i](mergeHandler)
}
r.mux[route] = mergeHandler
}
func (r *Router) Run(addr string) {
for route, handler := range r.mux {
http.Handle(route, handler)
}
http.ListenAndServe(addr, nil)
}

可以看到我們代碼實現的核心邏輯也只有40行的代碼,首先定義一個路由結構體,其內容一個是中間件的調用鏈條,我們用slice存放,另外一個是實現了路由及其handler的map:

type Router struct {
middlewareChain [] middleware
mux map[string] http.Handler
}

我們為路由結構體定義了三個方法,Use、Add、Run。Use的實現只是簡單的向中間件鏈條中添加了一個元素而已,使用append方法,middlerware並不是隨便寫的,我們也定義了一個結構體,接收一個http.Handler,返回值也是一個http.Handler,這樣我們才能在最後使用handle將路由url成功註冊到處理器上:

type middleware func(http.Handler) http.Handler
...
func (r *Router) Use(m middleware) {
r.middlewareChain = append(r.middlewareChain)
}

Add方法就是使用middleware對處理器函數進行包裝,需要注意的是代碼中middleware數組遍歷順序與用戶希望的調用順序是“相反”的,這個應該不難理解。

func (r *Router) Add(route string, h http.Handler) {
var mergeHandler = h
for i := len(r.middlewareChain) - 1; i >= 0; i-- {
mergeHandler = r.middlewareChain[i](mergeHandler)
}
r.mux[route] = mergeHandler
}

最後是Run函數,所做的工作就是將我們定義的路由解析,也就是將url綁定到指定的處理器上,我們默認只開發了addr可配置,使用DefaultServeMux:

func (r *Router) Run(addr string) {
for route, handler := range r.mux {
http.Handle(route, handler)
}
http.ListenAndServe(addr, nil)
}

接下來我們就可以在main函數中使用它了:

......
func hello(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello"))
}
func timeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
timeStart := time.Now()
//next handler
next.ServeHTTP(wr, r)
timeElapsed := time.Since(timeStart)
log.Println(timeElapsed)
})
}
func main() {
r := router.NewRouter()
r.Use(timeMiddleware)
r.Add("/", http.HandlerFunc(hello))
r.Run(":8080")
}

我們定義了一個timeMiddleware,用來統計接口的耗時,timeMiddleware符合middleware結構體的定義,注意在返回的匿名函數內調用了next.ServeHTTP(wr, r)方法。路由的Add方法中使用HandlerFunc對hello處理器函數做了封裝,使其轉化為了處理器。

這樣我們一個優雅的web中間件就實現了,我們可以拓展這個中間件的功能:比如在Run函數中添加更多的控制參數。給指定的處理器函數添加指定的中間件等等,這些有趣而實用的功能就交給感興趣的讀者完成吧。

4. Go Web服務器實現源碼解析

學習軟件設計的思想和深入理解架構理論最好的途徑是閱讀源碼,本節將帶領大家一步步閱讀Go Web服務器的實現邏輯,相信大家能收穫很多。

4.1 路由註冊:http.HandleFunc 和 http.Handle源碼

我們前邊看到,http.HandleFunc和http.Handle方法都是用於註冊路由,兩者的區別在於第二個參數,前者是一個具有 func(w http.ResponseWriter, r *http.Requests) 簽名的函數,而後者是一個結構體,該結構體實現了 func(w http.ResponseWriter, r *http.Requests) 簽名的方法。

http.HandleFunc 和 http.Handle源碼實現如下:

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
...
// 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) }

可以看到最終這兩個函數都是由多路複用器調用Handle方法完成路由的註冊。這裡我們遇到了兩種類型的對象:ServeMux 和 Handler,我們先來說Handler:

Handler是一個接口:

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

Handler 接聲明瞭名為 ServeHTTP 的函數簽名,也就是說任何結構只要實現了這個 ServeHTTP 方法,那麼這個結構體就是一個 Handler 對象。其實go的 http 服務都是基於 Handler 進行處理,而 Handler 對象的 ServeHTTP 方法也正是用以處理 request 並構建 response 的核心邏輯所在。

在上邊的代碼中,我們看到有這麼一段邏輯:

mux.Handle(pattern, HandlerFunc(handler))

這裡的HandlerFunc是個什麼呢?函數、結構體……,答案還要從源碼中尋找:

// 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 that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

讀到這裡,讀者應該明白了,HandlerFunc其實是一個類型,只不過是具有func(ResponseWriter, *Request)函數簽名的類型。HandlerFunc(handler)只不過是將handler方法做了類型轉化。並且這種類型自己實現了ServeHTTP函數,也就是說這個類型的函數其實就是一個 Handler 類型的對象。利用這種類型轉換,我們可以將一個 handler 函數轉換為一個
Handler 對象,而不需要定義一個結構體,再讓這個結構實現 ServeHTTP 方法。讀者可以認真體會一下這種設計思想。

4.2 路由解析: ServeMux和 DefaultServeMux源碼實現

前邊我們說到,Go語言中的路由基於多路複用器實現,多路複用器基本結構是:ServeMux。ServeMux是一個結構體,我們先來看一下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
}

ServeMux中的m是一個map,key是路由表達式,value是muxEntry結構體,muxEntry結構體的內容是路由表達式和handler(處理器)。

我們前邊說過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)
}

代碼很簡單,不做過多解釋,我們直到了什麼是handler和ServeMux,再回到原來的代碼邏輯:

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler)
}

我們知道:DefaultServeMux是Go語言默認提供的多路複用器(Multiplexer)。當我們沒有創建自定義的 Multiplexer ,則會自動使用一個默認的 Multiplexer 。它是ServeMux的一個實例。我們就來看一下ServeMux的Handle方法都做了些什麼工作吧,我們前邊其實也說過,它將url映射到了相關的處理器。源碼如下:

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

我們發現:ServeMux 的 map[string]muxEntry 增加給定的路由匹配規則;還將路由表達式以 ‘/’ 結尾對應的 muxEntry 對象加入到 []muxEntry 中,按照路由表達式長度排序。這就是我們上邊舉例子中為什麼/hello/註冊和/hello是不一樣的了,降級的匹配規則就是這樣實現的

4.3 開啟監聽服務ListenAndServe源碼實現

我們前邊代碼中多次使用到ListenAndServe,我們來看一下它的實現:

func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return 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(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

這裡先創建了一個 Server 對象,傳入了地址和 handler 參數,然後調用 Server 對象 ListenAndServe() 方法。這是http調用ListenAndServe的方式:

http.ListenAndServe(":8080", nil)

如果是nil,就使用默認的DefaultServeMux,它也是一個ServeMux實例。Server結構體我們前邊提到過,再來看一下:

type Server struct {
Addr    string  // TCP address to listen on, ":http" if empty
Handler Handler // handler to invoke, http.DefaultServeMux if nil
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ErrorLog *log.Logger
disableKeepAlives int32     // accessed atomically.
inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)
nextProtoOnce     sync.Once // guards setupHTTP2_* init
nextProtoErr      error     // result of http2.ConfigureServer if used
mu         sync.Mutex
listeners  map[*net.Listener]struct{}
activeConn map[*conn]struct{}
doneChan   chan struct{}
onShutdown []func()
}

前邊一部分是可變配置參數我們前邊提過了,後邊一部分首字母都是小寫的,我們無法對其進行配置。

在 Server 的 ListenAndServe 方法中,會初始化監聽地址 Addr ,同時調用 Listen 方法設置監聽。最後將監聽的TCP對象傳入 Serve 方法:

func (srv *Server) Serve(l net.Listener) error {
...
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept() // 等待新的連接建立
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx) // 創建新的協程處理請求
}
}

這裡隱去了一些細節,以便了解 Serve 方法的主要邏輯。首先創建一個上下文對象,然後調用 Listener 的 Accept() 等待新的連接建立;一旦有新的連接建立,則調用 Server 的 newConn() 創建新的連接對象,並將連接的狀態標誌為 StateNew ,然後開啟一個新的 goroutine 處理連接請求。

conn的Serve方法依然很長,不過我們從中能學習到很多東西:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
......
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle)
c.curReq.Store((*response)(nil))
......
c.rwc.SetReadDeadline(time.Time{})
}
}

當一個連接建立之後,該連接中所有的請求都將在這個協程中進行處理,直到連接被關閉。在 serve() 方法中會循環調用 readRequest() 方法讀取下一個請求進行處理。

c.setState(c.rwc, StateIdle)  //將連接狀態置為空閒
c.curReq.Store((*response)(nil)) //將當前請求置為空

其中最關鍵的邏輯就是一行代碼:

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

我們繼續在源碼中查看serverHandler:

// 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 的 ServeHTTP() 方法裡的 sh.srv.Handler 其實就是我們最初在 http.ListenAndServe() 中傳入的 Handler 對象,也就是我們自定義的 ServeMux 對象。如果該 Handler 對象為 nil ,則會使用默認的 DefaultServeMux 。最後調用 ServeMux 的 ServeHTTP() 方法匹配當前路由對應的 handler 方法。

接下來就是ServeMux的handler方法:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
return mux.handler(r.Host, r.URL.Path)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
return mux.handler(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
}
......
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
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的Handler方法就是根據url調用指定handler方法,handler方法的作用是調用match匹配路由。在 match 方法裡我們看到之前提到的 map[string]muxEntry 和 []muxEntry 。這個方法裡首先會利用進行精確匹配,在 map[string]muxEntry 中查找是否有對應的路由規則存在;如果沒有匹配的路由規則,則會進行近似匹配。

對於類似 /path1/path2/path3 這樣的路由,如果不能找到精確匹配的路由規則,那麼則會去匹配和當前路由最接近的已註冊的父路由,所以如果路由 /path1/path2/ 已註冊,那麼該路由會被匹配,否則繼續匹配父路由,直到根路由 / 。

ServeMux的Handler方法中找到要執行的handler之後,就調用handler的serveHTTP方法,至此,大家應該熟悉Go Web服務器的整個實現過程了。

5.小結

Go語言實現Web服務器的設計還是相當精巧的,簡單總結如下:

  • 一個方法只要實現了ServeHTTP的接口,它就是一個處理器。
ServeHTTP(http.ResponseWriter, *http.Request)
  • 處理器是處理用戶邏輯的核心所在,Go語言提供了handleFunc方法,將帶有func(http.ResponseWriter, *http.Request)函數簽名(處理器函數)的方法都能轉化為處理器,從而方便讀者實現業務邏輯。

  • Go提供了一個默認的多路複用器DefaultServeMux,它是ServeMux的一個實例,同樣實現了ServeHTTP方法,作用是將url綁定到具體的處理器。

我們還簡單實現了一個web中間件,幫助讀者瞭解了Go設計服務器接口的靈活性和可拓展性。

文章的最後我們還簡單分析了一下Go Web服務器實現的源碼,學習了一下網絡編程的設計思想。希望本文能給讀者帶來一些收穫,為今後學習Go語言在Web方面的編程打下基礎。

相關文章

「又是一年跳槽季」380+篇前端面試文章助你一臂之力🔥

[2020,步履不停|年度徵文]

Flutter終於有可視化編輯頁面了(HotUI)

用60行代碼實現一個高性能的聖誕抽抽樂H5小遊戲(含源碼)