ServeMux詳解

在 Go 語言中并闲,創(chuàng)建一個 HTTP 服務(wù)很簡單,只需要幾行代碼就可以創(chuàng)建一個可用的 HTTP 服務(wù)谷羞,這是因?yàn)?Go 原生幫我們實(shí)現(xiàn)了一個默認(rèn)的 HTTP 服務(wù)帝火,就是 ServeMux,在這篇文章中湃缎,我們來詳細(xì)看一下 ServeMux 的具體實(shí)現(xiàn)犀填。

1. 創(chuàng)建一個 HTTP 服務(wù)

在 Go 語言中,創(chuàng)建一個 HTTP 服務(wù)只需要寫下面幾行代碼就可以了嗓违。

func main() {

    http.HandleFunc("/index", func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte("Hello go web"))
    })

    http.ListenAndServe(":8080", nil)
}

我們定義了一個路由九巡,然后啟動 HTTP 服務(wù),就可以在瀏覽器中通過 http://127.0.0.1:8080/index 來訪問服務(wù)靠瞎,服務(wù)端會返回 Hello go web比庄。

這樣,一個簡單的 HTTP 服務(wù)就創(chuàng)建完成了乏盐。

2. HTTP 服務(wù)如何運(yùn)行

上面的代碼有兩部分佳窑,一部分是定義 HTTP 服務(wù)的路由,在服務(wù)啟動之后父能,我們訪問相應(yīng)的路由神凑,就能得到服務(wù)端的響應(yīng)。

在 http 包中,有一個接口 http.Handler 溉委,這個接口是 HTTP 服務(wù)的核心:

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

對于 HTTP 服務(wù)來說鹃唯,一定發(fā)起請求的客服端和處理請求的服務(wù)端,客戶端發(fā)起一個請求瓣喊,然后服務(wù)端給出相應(yīng)的輸出坡慌。這個 Handler 接口就把這個整個過程抽象為 ServeHTTP 方法。ResponseWriter 表示服務(wù)端的輸出藻三,Request 表示來自客戶端的請求洪橘。

在 http 包中提供了一個 Handler 的實(shí)現(xiàn) ServeMux,這個Handler 做的事情也很簡單棵帽,就是來維護(hù) URL 和 Handler 之間的關(guān)系熄求,根據(jù) URL 判斷應(yīng)該把請求轉(zhuǎn)發(fā)到哪個 Handler,沒錯這里的 Handler 也是 http.Handler逗概,是同一個接口弟晚。用 http.Handler 來管理 http.Handler,我覺得這是一個非常優(yōu)雅的設(shè)計(jì)逾苫。

2.1 定義路由

路由可以使用兩種方式來定義卿城,一種是實(shí)現(xiàn) Handler,還有一種是使用 HandleFunc隶垮,這兩個概念在上一篇文章中我們已經(jīng)詳細(xì)討論過了藻雪,這里就不多說。

示例代碼中的路由使用 HandleFunc 來定義狸吞,這里我們來看一下路由具體是如何被定義的勉耀。我們進(jìn)入 HandleFunc 的源碼:

// server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

發(fā)現(xiàn)實(shí)際上是調(diào)用了 DefaultServerMux 的 HandleFunc 方法,DefaultServerMux 實(shí)際上就是 ServeMux蹋偏,是 Go 的 HTTP 服務(wù)的默認(rèn)實(shí)現(xiàn)便斥。然后 DefaultServerMux 調(diào)用 Handle 方法來處理路由:

// server.go
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
    }
}

路由由兩部分組成,一個是匹配 HTTP url 的 pattern威始,每個pattern 都代表著一類 HTTP 請求枢纠,都需要一個對應(yīng)的 handler 來處理。

DefaultServerMux 的 Handle 方法其實(shí)就做了一件事黎棠,在判斷路由和對應(yīng)的 handler 實(shí)現(xiàn)都沒問題晋渺,并且該路由沒有重復(fù)定義之后,就把這些路由都存到 map 中脓斩。所以 HTTP 服務(wù)的路由表其實(shí)就是一個 map木西。

完整流程如下,DefaultServerMux 簡稱為 DSM :

2.2 啟動 HTTP 服務(wù)

路由定義完成之后随静,就需要啟動服務(wù)了八千,就是下面這行代碼:

http.ListenAndServe(":8080", nil)

通常吗讶,第二個參數(shù)都會設(shè)置為 nil,設(shè)置為 nil 的時候恋捆,就會使用 Go 語言默認(rèn)的 HTTP 服務(wù)實(shí)現(xiàn)照皆。我們跟進(jìn)代碼的實(shí)現(xiàn):

// server.go
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

默認(rèn)情況下,會創(chuàng)建一個 Server沸停,Server 就是表示一個服務(wù)端膜毁, 其中定義了運(yùn)行一個 HTTP 服務(wù)所需要的全部參數(shù)以及必要的方法,如果不給 Server 傳入?yún)?shù) 愤钾,那么 Server 就使用默認(rèn)的參數(shù)運(yùn)行爽茴。

type Server struct {
    // 服務(wù)端的 host 和 端口號
    Addr string

    Handler Handler // 默認(rèn)為 ServeMux
  // 省略其他參數(shù)
}

然后 server 會調(diào)用 ListenAndServe 來啟動端口監(jiān)聽和請求接收,使用 net.Listen 來啟動監(jiān)聽端口:

// server.go
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)
}

然后調(diào)用 Server 的 Serve 方法來接收請求和處理請求绰垂。在 Serve 方法中,最關(guān)鍵的是下面這段代碼火焰,這里是一個 for 循環(huán)劲装,沒有結(jié)束條件,除非發(fā)生錯誤或者主動結(jié)束服務(wù)昌简,否則會一直處在接收請求的狀態(tài)占业。

// server.go#Serve
for {
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            //......
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(connCtx)
    }

接收到請之后,會為每一個請求創(chuàng)建一個 conn 實(shí)例纯赎,conn 表示服務(wù)端的一個 HTTP 連接谦疾,并啟動一個新的 goroutine 來處理這個請求。

然后就進(jìn)入到 conn 的 serve 方法犬金。

因?yàn)樵谝粋€ HTTP 請求中念恍,有可能會出現(xiàn)多次請求的收發(fā),所以這里依然啟動了 for 的循環(huán)來接收請求數(shù)據(jù)晚顷。

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)
        }
    // .. 去掉無關(guān)代碼
        serverHandler{c.server}.ServeHTTP(w, w.req)
    // .. 去掉無關(guān)代碼    
    }

在 conn 的 serve 方法中峰伙,其實(shí)就只做了兩件事,一件事讀取請求中的數(shù)據(jù)该默。然后是調(diào)用 ServeHTTP 方法瞳氓,進(jìn)入到 ServeHTTP 方法中:

// server.go
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)
}

判斷 handler 是否為 nil,如果為 nil栓袖,然后就直接使用 DefaultServeMux 的 ServeHTTP 方法來處理 HTTP 請求匣摘。

// server.go
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)
}

我們發(fā)現(xiàn)其實(shí)這個方法也只做了一件事,就是去上面路由表 muxEntry 中匹配路由裹刮,然后使用路由 的 Handler 調(diào)用 ServerHTTP 來真正的處理請求音榜。

完整的流程如下:

3. 小結(jié)

Go 服務(wù)默認(rèn)的 HTTP 處理流程其實(shí)不難理解,最難的地方在于滿屏都是 Handler 接口和 ServeHTTP 方法必指,理解 Handler 接口是理解整個流程的關(guān)鍵囊咏。

所有的 HTTP 請求都需要經(jīng)過 ServeHTTP 方法處理。而 Go 語言中的 ServeMux 實(shí)現(xiàn)了 Handler 接口,通過 url 找到對應(yīng)的路由梅割,然后在 ServeHTTP 中調(diào)用路由實(shí)現(xiàn)的 ServeHTTP 方法去真正處理對應(yīng)的請求霜第。

Go 語言對 HTTP 服務(wù)的抽象非常好,通過一個接口就把整個流程串起來了户辞。

文 / Rayjun

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泌类,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子底燎,更是在濱河造成了極大的恐慌刃榨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件双仍,死亡現(xiàn)場離奇詭異枢希,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)朱沃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門苞轿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逗物,你說我怎么就攤上這事搬卒。” “怎么了翎卓?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵契邀,是天一觀的道長。 經(jīng)常有香客問我失暴,道長坯门,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任锐帜,我火速辦了婚禮田盈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缴阎。我一直安慰自己允瞧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布蛮拔。 她就那樣靜靜地躺著述暂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪建炫。 梳的紋絲不亂的頭發(fā)上畦韭,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音肛跌,去河邊找鬼艺配。 笑死察郁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的转唉。 我是一名探鬼主播皮钠,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赠法!你這毒婦竟也來了麦轰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砖织,失蹤者是張志新(化名)和其女友劉穎款侵,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侧纯,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡新锈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了眶熬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壕鹉。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖聋涨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情负乡,我是刑警寧澤牍白,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站抖棘,受9級特大地震影響茂腥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜切省,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一最岗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朝捆,春花似錦般渡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至儒老,卻和暖如春蝴乔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驮樊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工薇正, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留片酝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓挖腰,卻偏偏與公主長得像雕沿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子曙聂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容