Go http server (I) 源碼閱讀

這個系列會寫三到四篇文章容燕,第一篇是 go sdk 里 net/http/server.go 的閱讀筆記,之后會寫一下如何利用 server.go 的接口自定義一個簡易通用的 HTTP server 框架译蒂。

example

先從一個簡單的例子開始吧:

package main

import (
    "net/http"
    "fmt"
    "log"
)

//開啟web服務(wù)
func test() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9090", nil) // 注意這里第二個參數(shù)為 nil
    if err != nil {
        log.Fatal("ListenAndServer:", err)
    }
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello Guest!")
}

func main() {
    test()
}

運行代碼,此時瀏覽器訪問localhost:9090就會看到輸出 “Hello Guest!”盛杰,其實訪問localhost:9090/+任意字符串禽捆,都能得到結(jié)果。這段代碼先用http.HandleFunc注冊了一個處理函數(shù)洲炊,然后調(diào)用http.ListenAndServe監(jiān)聽端口感局,當(dāng)有請求到來時,會根據(jù)訪問路徑找到并執(zhí)行對應(yīng)的處理函數(shù)暂衡。

我們通常還能看到另一種寫法:

package main

import (
    "net/http"
    "fmt"
    "log"
)

//開啟web服務(wù)
func test() {
  http.Handle("/", &handler{})
    err := http.ListenAndServe(":9090", nil) //
    if err != nil {
        log.Fatal("ListenAndServer:", err)
    }
}

func sayHello(w http.ResponseWriter, r *http.Request) {...}

func main() {
    test()
}

type handler struct{}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    sayHello(w, r)
}

這段代碼效果一樣询微。區(qū)別就是http.HandleFunchttp.Handle需要的第二個參數(shù),前者要一個func (w http.ResponseWriter, r *http.Request)函數(shù)狂巢,后者要一個實現(xiàn)了該函數(shù)的結(jié)構(gòu)體撑毛。

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

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

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

可以看到,兩個函數(shù)都會調(diào)用mux.handle

func (mux *ServeMux) Handle(pattern string, handler Handler)

第二個參數(shù)是Handler唧领,是一個接口:

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

現(xiàn)在回到上面的HandleFunc,注意這個:HandlerFunc(handler),這里很容易讓人誤以為HandlerFunc是一個函數(shù)并且包裝了傳入的handler藻雌,再返回一個Handler類型雌续。而實際上這里是類型轉(zhuǎn)換,來看HandlerFunc的定義:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

雖然HandlerFunc的類型是一個函數(shù)胯杭,但它是一種類型驯杜,因為是以type來定義而不是func,并且實現(xiàn)了ServeHTTP(w ResponseWriter, r *Request)做个,在這個函數(shù)里鸽心,它又調(diào)用了自身。這個細節(jié)是十分重要的居暖,因為這一步關(guān)乎到當(dāng)路由規(guī)則匹配時顽频,相應(yīng)的響應(yīng)方法是否會被調(diào)用的問題!這里的類型轉(zhuǎn)換用法使一個函數(shù)自身實現(xiàn)了一個接口膝但,就不用每次都要先寫一個本身無用結(jié)構(gòu)體冲九,再用結(jié)構(gòu)體實現(xiàn)接口。請仔細體會這種技巧跟束!

。丑孩。冀宴。有點扯偏了,這里記住 Handler 這個接口是 go 語言 HTTP 服務(wù)最最最重要的接口温学,官方庫和第三方庫都按照這個接口來擴展略贮。

Server

來看一下 Server 這個結(jié)構(gòu)體吧, 這里我只列出了幾個核心的域:

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 // optional TLS config, used by ServeTLS and ListenAndServeTLS

    listeners  map[net.Listener]struct{}
    onShutdown []func()
}

handler

這里主要關(guān)注 Handler,這個 Handler 就是剛剛的那個接口仗岖,可以在創(chuàng)建 Server 時傳入逃延,也可以在調(diào)用 Server.ListenAndServe 時傳入:

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

這個 handler 是在建立連接后收到客戶端請求時用到:

func (c *conn) serve(ctx context.Context) { // conn 指當(dāng)前連接
    ...
    for {
        w, err := c.readRequest(ctx)
        ...
        serverHandler{c.server}.ServeHTTP(w, w.req)
        ...
    }
}

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ù)可以看到,當(dāng) server.handler==nil 時轧拄,使用內(nèi)部全局變量揽祥,也就是前面提到過的 DefaultServeMux。也就是說檩电,我們在收到請求時通過這個 handler 來執(zhí)行自己的邏輯代碼拄丰,所以這個 handler 必須包含路由功能,并且能夠執(zhí)行路由對應(yīng)的處理函數(shù)俐末。同時我們用的第三方 HTTP server 框架(echo料按、beego…)也是通過自定義 handler 來實現(xiàn)功能擴展。這也是 Handler 這個接口是最最重要的接口的原因卓箫。

關(guān)于 DefaultServeMux 和 自定義的 handler载矿,會在之后詳細討論。接下來回到 Server 本身烹卒。

Server.Serve

在主函數(shù)中可以調(diào)用 http.ListenAndServe 或者 http.Serve 來開始 HTTP 服務(wù)闷盔, 原理都一樣:

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

仔細看下 srv.Serve 的實現(xiàn):

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    
    var tempDelay time.Duration // how long to sleep on accept failure

    if err := srv.setupHTTP2_Serve(); err != nil {// 如果設(shè)置了 http2魂挂,就使用 http2 服務(wù),
        return err
    }

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept() // 這里會等待新的連接的建立馁筐,會阻塞在這里涂召。
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            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
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

這里要詳細解釋一下的就是 Accept 返回的 error 了。有以下幾種可能:

  • Accept 的時候 Server 由于某種原因停止了
  • 收到系統(tǒng)信號產(chǎn)生中斷敏沉,當(dāng)然如果 返回的是 EINTR 表示可以重新調(diào)用
  • 之前斷掉的連接在短時間被重用了果正,此時該連接處于 TIME_WAIT 狀態(tài),新連接暫時不可用盟迟∏镉荆可參考這里

對于暫時性的錯誤,可以稍等一會兒攒菠,所以會出現(xiàn) sleep迫皱。如果成功拿到 conn,先標(biāo)記連接狀態(tài)辖众,然后創(chuàng)建新 goroutine 開始對連接服務(wù)卓起。

conn.serve

func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    defer func() {
        if err := recover(); err != nil && err != ErrAbortHandler {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
        }
        if !c.hijacked() {
            c.close()
            c.setState(c.rwc, StateClosed)
        }
    }()

    if tlsConn, ok := c.rwc.(*tls.Conn); ok {
        // 處理 https
    }

    // HTTP/1.x from here on.

    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for { // 同一個連接有多個請求,循環(huán)處理
        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)
        }
        if err != nil {
            // handle error
            return
        }

        // Expect 100 Continue support
        req := w.req
        if req.expectsContinue() {
            if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
                // Wrap the Body reader with one that replies on the connection
                req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
            }
        } else if req.Header.get("Expect") != "" {
            w.sendExpectationFailed()
            return
        }

        c.curReq.Store(w)

        if requestBodyRemains(req.Body) { // 支持管線化戏阅,處理當(dāng)前請求時可能還在接收請求
            registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
        } else {
            if w.conn.bufr.Buffered() > 0 {
                w.conn.r.closeNotifyFromPipelinedRequest()
            }
            w.conn.r.startBackgroundRead()
        }

        serverHandler{c.server}.ServeHTTP(w, w.req) // 這里就是之前提到的,自定義處理的入口
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest() // 把數(shù)據(jù) flush 到網(wǎng)絡(luò)層啤它,此次請求在應(yīng)用層結(jié)束
        if !w.shouldReuseConnection() {
            if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
                c.closeWriteAndWait() // 發(fā)送 TCP FIN 奕筐,關(guān)閉連接
            }
            return
        }
        
        ...
        
        if d := c.server.idleTimeout(); d != 0 { // 設(shè)置空閑超時,超時后關(guān)閉連接
            c.rwc.SetReadDeadline(time.Now().Add(d))
            if _, err := c.bufr.Peek(4); err != nil {
                return
            }
        }
        c.rwc.SetReadDeadline(time.Time{})
    }
}

這里代碼比較復(fù)雜变骡,包含了比較完整的 HTTP离赫、HTTPs、HTTP2 協(xié)議的實現(xiàn)塌碌,建議了解了協(xié)議的內(nèi)容再來看具體實現(xiàn)渊胸。代碼協(xié)議的細節(jié)部分代碼就不詳細談了,我們需要理解的是 創(chuàng)建 listener誊爹,從 Accept 拿到連接蹬刷,等待并讀取到 request,用 handler 處理 request 并把結(jié)果或錯誤信息寫到 response 的過程频丘。

需要注意的是办成,我們所討論的是 go 語言官方庫的 HTTP 的實現(xiàn),這里的發(fā)送和接收數(shù)據(jù)都是指的發(fā)給下層傳輸層和從傳輸層接收搂漠,也就是調(diào)用 socket 接口迂卢,一定要分清楚各個層次。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市而克,隨后出現(xiàn)的幾起案子靶壮,更是在濱河造成了極大的恐慌,老刑警劉巖员萍,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腾降,死亡現(xiàn)場離奇詭異,居然都是意外死亡碎绎,警方通過查閱死者的電腦和手機螃壤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筋帖,“玉大人奸晴,你說我怎么就攤上這事∪蒸铮” “怎么了寄啼?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長代箭。 經(jīng)常有香客問我墩划,道長,這世上最難降的妖魔是什么梢卸? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任走诞,我火速辦了婚禮,結(jié)果婚禮上蛤高,老公的妹妹穿的比我還像新娘。我一直安慰自己碑幅,他們只是感情好戴陡,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沟涨,像睡著了一般恤批。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裹赴,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天喜庞,我揣著相機與錄音,去河邊找鬼棋返。 笑死延都,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睛竣。 我是一名探鬼主播晰房,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了殊者?” 一聲冷哼從身側(cè)響起与境,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猖吴,沒想到半個月后摔刁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡海蔽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年共屈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片准潭。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡趁俊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刑然,到底是詐尸還是另有隱情寺擂,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布泼掠,位于F島的核電站怔软,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏择镇。R本人自食惡果不足惜挡逼,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腻豌。 院中可真熱鬧家坎,春花似錦、人聲如沸吝梅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苏携。三九已至做瞪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間右冻,已是汗流浹背装蓬。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纱扭,地道東北人牍帚。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像跪但,于是被迫代替她去往敵國和親履羞。 傳聞我的和親對象是個殘疾皇子峦萎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

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