Go語言入門【六】:源碼學(xué)習(xí)-net/http

package net/http是Go語言的主要應(yīng)用場景之一web應(yīng)用的基礎(chǔ),從中可以學(xué)習(xí)到大量前文提到的io,以及沒有提到的sync包等一系列基礎(chǔ)包的知識,代碼量也相對較多,是一個源碼學(xué)習(xí)的寶庫。本文主要從一個http server開始,講解Go是如何實現(xiàn)一個http協(xié)議服務(wù)器的。

主要涉及以下源碼文件:
net/net.go
net/server.go
net/http.go
net/transfer.go
sync/pool.go
sync/mutex.go

0.引子:從最簡單的http server說起

func main() {
    http.HandleFunc("/hi", hi)
    http.ListenAndServe(":9999", nil)
    fmt.Printf("hello, world\n")
}

func hi(res http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(res, "hi")
}

以上就是最簡單的服務(wù)器代碼,運行后監(jiān)聽本機(jī)的9999端口,在瀏覽器中打開http://localhost:9999可以看到返回的hi,接下來就從此入手,開始分析net/http模塊。

1.Handler: 從路由開始上路

先來分析http.HandleFunc("/hi", hi) 這一句,查看源碼發(fā)現(xiàn):

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

首先我們了解到handler的定義是這樣的func(ResponseWriter, *Request)。這個定義很關(guān)鍵,先提一下。
然后看到了DefaultServeMux,這個類是來自于ServeMux結(jié)構(gòu)的一個實例,而后者是一個『路由器』的角色,在后面講到的請求處理過程中,ServeMux用來匹配請求的地址,分配適合的handler來完成業(yè)務(wù)邏輯。
完整的來講,我們應(yīng)該先定義一個自己的ServeMux,并向他分配路由,像這樣:

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page!")
})
http.ListenAndServe(":9999", mux)

1.生成一個路由器
2.向路由器注冊路由
3.由路由器以及服務(wù)地址建立底層連接并提供服務(wù)

而之前的簡寫方式只是省略了建立路由的過程,實際上用了系統(tǒng)自帶的DefaultServeMux作為路由器而已。

2.向net包匆匆一瞥:一切的基礎(chǔ)在net.Conn

接下來看到http.ListenAndServe(":9999", nil)這句代碼的源碼。

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

首先生成了一個server對象,并調(diào)用了它的ListenAndServe方法。Server對象顧名思義,封裝了有關(guān)提供web服務(wù)相關(guān)的所有信息,是一個比較重要的類。

// 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
    ReadTimeout  time.Duration // maximum duration before timing out read of the request
    WriteTimeout time.Duration // maximum duration before timing out write of the response
    TLSConfig    *tls.Config   // optional TLS config, used by ListenAndServeTLS

    MaxHeaderBytes int

    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    ConnState func(net.Conn, ConnState)

    ErrorLog *log.Logger

    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used
}

1.handler即路由器(實際上路由器本身作為handler,其中有注冊了很多handler),見Handler定義:

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

和之前注冊的函數(shù)幾乎一樣。
2.ErrorLog默認(rèn)以stdErr作為輸出,也可以提供其他的logger形式。
3.其他的是一些配置以及https,http2的相關(guān)支持,暫擱一邊。

初始化一個Server必須要的是地址(端口)以及路由,其他都可以按照默認(rèn)值。生成好Server之后,進(jìn)入ListenAndServe,源碼主要有:

ln, err := net.Listen("tcp", addr)
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})

重要的有兩句,首先調(diào)用底層的net模塊對地址實現(xiàn)監(jiān)聽,返回的ln是一個Listener類型,這個類型有三個方法:

  • Accept() (Conn, error)
  • Close() error
  • Addr() Addr

我們先不碰net模塊,只要知道ln可以通過accept()返回一個net.Conn就夠了,獲取一個連接的上下文意味著和客戶端建立了通道,可以獲取數(shù)據(jù),并把處理的結(jié)果返回給客戶端了。接下來srv.Serve()方法接受了ln,在這里程序被分為了兩層:ln負(fù)責(zé)連接的底層建立,讀寫,關(guān)閉;Server負(fù)責(zé)數(shù)據(jù)的處理。

補(bǔ)充說明一下net.Conn,這個Conn區(qū)別于后文要講的server.conn,是比較底層的,有

  • Read(b []byte) (n int, err error)
  • Write(b []byte) (n int, err error)

兩個方法,也意味著實現(xiàn)了io.Reader, io.Writer接口。

3.回到server:建立一個服務(wù)器,用goroutine 優(yōu)雅處理并發(fā)

接著前面說,建立好ln之后,用tcpKeepAliveListener類型簡單包裝,作為參數(shù)傳給srv.Serve()方法,該方法十分重要,值得放出全部代碼:

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.
//
// Serve always returns a non-nil error.
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    // TODO: allow changing base context? can't imagine concrete
    // use cases yet.
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        rw, e := l.Accept()
        if e != nil {
            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)
    }
}

分析一下:

a) 首先是context這個類型

這個類型比較奇葩,其作用就是一個map,以key,value的形式設(shè)置一些背景變量,使用方法是context.WithValue(parentCtx,key,value)

b) 然后進(jìn)入一個for無限循環(huán),

l.Accept()阻塞直到獲取到一個net.Conn,之后通過srv.newConn(rw)建立一個server.conn(屬于私有變量,不對外暴露),并設(shè)置狀態(tài)為StateNew

c) 啟動一個goroutine來處理這個連接

調(diào)用go c.serve(ctx)。從這里可以看出,go語言的并發(fā)模型不同于nodejs的單線程回調(diào)模型,也不同于Java的多線程方案,采用原生的goroutine來處理既有隔離性,又兼顧了性能。因為這樣不會發(fā)生nodejs中因為異常處理問題經(jīng)常讓服務(wù)器掛掉的現(xiàn)象。同時,goroutine的創(chuàng)建代價遠(yuǎn)遠(yuǎn)低于創(chuàng)建線程,當(dāng)然能在同一臺機(jī)器比Java服務(wù)器達(dá)到更大的并發(fā)量了。

4. 從server到conn:一次請求所有的精華都在conn

前面提到了server.conn,來看一下源碼:

// A conn represents the server side of an HTTP connection.
type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // rwc is the underlying network connection.
    // This is never wrapped by other types and is the value given out
    // to CloseNotifier callers. It is usually of type *net.TCPConn or
    // *tls.Conn.
    rwc net.Conn

    // remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
    // inside the Listener's Accept goroutine, as some implementations block.
    // It is populated immediately inside the (*conn).serve goroutine.
    // This is the value of a Handler's (*Request).RemoteAddr.
    remoteAddr string

    // tlsState is the TLS connection state when using TLS.
    // nil means not TLS.
    tlsState *tls.ConnectionState

    // werr is set to the first write error to rwc.
    // It is set via checkConnErrorWriter{w}, where bufw writes.
    werr error

    // r is bufr's read source. It's a wrapper around rwc that provides
    // io.LimitedReader-style limiting (while reading request headers)
    // and functionality to support CloseNotifier. See *connReader docs.
    r *connReader

    // bufr reads from r.
    // Users of bufr must hold mu.
    bufr *bufio.Reader

    // bufw writes to checkConnErrorWriter{c}, which populates werr on error.
    bufw *bufio.Writer

    // lastMethod is the method of the most recent request
    // on this connection, if any.
    lastMethod string

    // mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
    mu sync.Mutex

    // hijackedv is whether this connection has been hijacked
    // by a Handler with the Hijacker interface.
    // It is guarded by mu.
    hijackedv bool
}

解釋一下:
首先,持有server的引用;持有對原始net.Conn引用;持有一個reader,封裝自底層讀取接口,可以從連接中讀取數(shù)據(jù),以及一個bufr(還是前面的reader,加了緩沖)。以及一個對應(yīng)的同步鎖,鎖定對本身的參數(shù)修改,防止同步更新出錯。
然后,這里的mu類型是sync.Mutex這個類型的作用有點像Java中的synchronized塊(有關(guān)于Java的Synchronized,可以參考本人另一篇拙作《Java多線程你只需要看著一篇就夠了》),mu就是持有對象鎖的那個實例。我們可以看到conn的hijackedv屬性就是通過mu來進(jìn)行維護(hù)的,目的是防止同步更新問題。參考conn.hijackLocked(),不再展開。

繼續(xù)看serv.Serve()方法,接著前面的3點:

d) setState(state)

實際上state被維護(hù)在Server里,只不過通過conn來調(diào)用了。一共有StateNew, StateActive, StateIdle, StateHijacked, StateClosed五個狀態(tài)。從new開始,當(dāng)讀取了一個字節(jié)之后進(jìn)入active,讀取完了并發(fā)送response之后,進(jìn)入idle。終結(jié)有兩種,主動終結(jié)closed以及被接管: Hijack讓調(diào)用者接管連接,在調(diào)用Hijack()后,http server庫將不再對該連接進(jìn)行處理,對于該連接的管理和關(guān)閉責(zé)任將由調(diào)用者接管。參考interface Hijacker

e) c.serve(ctx)

讓我們先來看conn.serve()源碼:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    defer func() {
        if err := recover(); err != nil {
            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 {
        if d := c.server.ReadTimeout; d != 0 {
            c.rwc.SetReadDeadline(time.Now().Add(d))
        }
        if d := c.server.WriteTimeout; d != 0 {
            c.rwc.SetWriteDeadline(time.Now().Add(d))
        }
        if err := tlsConn.Handshake(); err != nil {
            c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
            return
        }
        c.tlsState = new(tls.ConnectionState)
        *c.tlsState = tlsConn.ConnectionState()
        if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
            if fn := c.server.TLSNextProto[proto]; fn != nil {
                h := initNPNRequest{tlsConn, serverHandler{c.server}}
                fn(c.server, tlsConn, h)
            }
            return
        }
    }

    // HTTP/1.x from here on.

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

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

    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)
        }
        if err != nil {
            if err == errTooLarge {
                // Their HTTP client may or may not be
                // able to read this if we're
                // responding to them and hanging up
                // while they're still writing their
                // request. Undefined behavior.
                io.WriteString(c.rwc, "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n431 Request Header Fields Too Large")
                c.closeWriteAndWait()
                return
            }
            if err == io.EOF {
                return // don't reply
            }
            if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
                return // don't reply
            }
            var publicErr string
            if v, ok := err.(badRequestError); ok {
                publicErr = ": " + string(v)
            }
            io.WriteString(c.rwc, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n400 Bad Request"+publicErr)
            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
        }

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        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)
    }
}

5.從conn到conn.Serve:http協(xié)議的處理實現(xiàn)之處,conn變成Request和Response

上文的conn.Serve(),我們只關(guān)注主要邏輯:

1.初始化bufr和bufw。
...
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
...

這兩個是讀寫的切入點,從效率考慮,是加了一層緩沖的。值得注意的是bufw和bufr還加了一層sync.Pool的封裝,這是來源于sync包的對象池,目的是為了重用,不需要每次都執(zhí)行new分配內(nèi)存。

2.接下來重要的是,從底層讀取客戶端發(fā)送的數(shù)據(jù):
...
w, err := c.readRequest(ctx)
...

我們看到readRequest定義:

func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error)
返回的是 (w *response, err error),而response又是server.go中的一個重要對象,它是conn的更高一層封裝,包括了req,conn,以及一個writer,當(dāng)然這個write操作實際上還是由conn,進(jìn)而由更底層的net.Conn來執(zhí)行的。對于開發(fā)者而言,面對的基本上就是這個response,可以說是一個設(shè)計模式中的門面模式。

另外,注意到readRequest執(zhí)行的時候也調(diào)用了mu.Lock()

3.最重要的,調(diào)用用戶的handler
...
serverHandler{c.server}.ServeHTTP(w, w.req)

首先serverHandler只是一個包裝,這句實際上調(diào)用的是c.server.Handler.ServeHTTP()。而在前面講到的server的初始化中,Handler就是DefaultServeMux或者用戶指定的ServeMux,我們稱之為路由器。在路由器中,根據(jù)用戶定義路由規(guī)則,來具體調(diào)用用戶的業(yè)務(wù)邏輯方法。

路由器可以看做一個Map,以路由規(guī)則(string)作為key,以業(yè)務(wù)方法(func類型)作為value。

ServeHttp傳入了最重要的兩個高層封裝response對象和Request對象(嚴(yán)格來講這里response是私有類型,暴露在外的是ResponseWriter,但從http的本質(zhì)來理解,還是稱之為response)。

從層次來看,這兩個封裝對象中間封裝的是底層的conn,客戶端發(fā)送來的數(shù)據(jù)(req.body),以及讀寫的接口reader,writer。

然后,用戶的業(yè)務(wù)邏輯就接受數(shù)據(jù),進(jìn)行處理,進(jìn)而返回數(shù)據(jù)。返回數(shù)據(jù)一般直接寫入到這個w,即ResponseWriter中。這樣,一個http請求的完整流程就完成了。

4.最后做一些處理工作

主要包括:異常處理,資源回收,狀態(tài)更新。我們了解即可,重點還是放在主要流程上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373

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