golang http server 源碼閱讀

http 包怎么用

使用 golang 的 http 包可以很簡易的實現(xiàn)一個 web 服務悲柱,如下

main.go

package main

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

func foo(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hi! babe~"))
}

func echo(w http.ResponseWriter, r *http.Request) {
    s := fmt.Sprintf("gorotines count: %d", runtime.NumGoroutine())
    w.Write([]byte(s))
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/foo", foo)plainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplain
    mux.HandleFunc("/echo/goroutines", echo)

    log.Println("Listening...")
    http.ListenAndServe(":3000", mux)
}

那如果我想看看整個服務是怎么實現(xiàn)的捧请,該怎么辦呢凡涩?
ListenAndServe()接收一個地址和處理程序的參數(shù),此函數(shù)的定義如下

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

然后調用了

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

然后上述函數(shù)又調用了 Serve 函數(shù)

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    ...
    srv.trackListener(l, true)
    defer srv.trackListener(l, false)

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        // Accept等待并返回listener的下一個連接
        rw, e := l.Accept()
        if e != nil { ... } // 省略一些代碼
        tempDelay = 0
        // 使用rw創(chuàng)建一個新連接
        c := srv.newConn(rw)
        // 將鏈接置為激活狀態(tài)疹蛉,同時可指定在客戶端連接更改狀態(tài)時調用可選的回調函數(shù)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

從上面的go c.serve(ctx)可以看出活箕,http 包在 ctx 上下文組裝好之后交給了 gorotine 來處理這個請求。在繼續(xù)下一步之前可款,我們先看看這個 ctx 上下文育韩,Context 被定義為一個接口,它在 golang 中被運用的非常廣泛闺鲸。

type Context interface {
    // Deadline 設置了兩個參數(shù)deadline, ok
    // deadline 表示上下文被取消的截止時間
    // 如果沒有設置deadline筋讨,Deadline的ok參數(shù)會返回false。
    // 連續(xù)調用返回結果相同
    Deadline() (deadline time.Time, ok bool) 
 
    // 如果上下文被取消摸恍,Done會返回一個被關閉的chan
    // 如果上下文從沒被取消過悉罕,Done將返回nil
    // 連續(xù)調用返回結果相同
    Done() <-chan struct{}

    // Done 的 chan被關閉后,也就是上下文被取消時立镶,Err會返回非零的錯誤值壁袄。
   // 當 Done 的 chan被關閉后,連續(xù)調用返回結果相同
    Err() error

    // 也就是通過key去獲取該key上下文中的值谜慌,如果沒有則為nil然想,可見ctx是一個鍵值對。該值是線程安全的
    Value(key interface{}) interface{} 
}

好了介紹完 context 之后欣范,我們再來看看 Serve 函數(shù)中的baseCtx := context.Background()是干什么的变泄。

// Background返回一個非零的空Context令哟。它沒有值也沒有deadline,所以也不會被取消妨蛹,
// 它通常在main函數(shù)被用來初始化屏富,測試,以及作為請求傳入的頂級Context
func Background() Context {
    return background
}

嗯蛙卤,他其實就是初始化的一個作用狠半。

接下來又碰到了 WithValue 函數(shù),我們繼續(xù)看看 WithValue 的定義颤难。

// 生成一個綁定了一個鍵值對數(shù)據的Context神年,可以通過parent訪問到上一層的context,這個綁定的數(shù)據可以通過Context.Value方法訪問到
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

// 一個valueCtx結構帶有一個鍵值對行嗤。然后用來嵌套其他的Context已日。
type valueCtx struct {
    Context
    key, val interface{}
}

結合源碼,那么這個 context 定義結構就可以了解了

    // 下面定義了兩個context key栅屏,一個存儲了type *Server飘千,另一個存儲了type net.Addr
    ServerContextKey = &contextKey{"http-server"}
    LocalAddrContextKey = &contextKey{"local-addr"}
   
    // 頂層context
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
   // parent 為 頂層的context
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())

那么為什么這么定義呢? 思考思考栈雳,對 context 的作用和細節(jié)還沒系統(tǒng)了解過护奈,context 是一個很重要的功能 TODO

繼續(xù)往下看 go serve(ctx),可以看到這里用 gorotine 來處理每個鏈接來支撐并發(fā)哥纫,這也是支持并發(fā)的關鍵霉旗。

// 處理一個新鏈接
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()

    ...

    // HTTP/1.x from here on.
    // 這里又碰到WithCancel函數(shù),WithCancel返回帶有父context 的Done通道副本和一個cancelCtx函數(shù)磺箕。
    // 返回的上下文的Done通道在調用了返回的cancelCtx函數(shù)或父context的Done通道關閉時關閉奖慌,以先發(fā)生者為準。
    // 取消此上下文會釋放與其關聯(lián)的資源松靡,因此代碼應在此上下文中運行的操作完成后立即調用cancelCtx简僧。所以可以看到使用了defer去調用cancelCtx釋放資源
    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 {
        // 從鏈接中讀取請求
        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)
        }

        ...
        // 核心點,該處就是處理請求的hanler
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        
        ...

        c.rwc.SetReadDeadline(time.Time{})
    }
}

好了到這我們知道是用serverHandler{c.server}.ServeHTTP(w, w.req)來處理請求的雕欺。我們回過頭去看看岛马,路由和 handler 是怎么綁定到一起的

    // ServeMux是一個HTTP請求多路復用器,說白了就承擔了路由功能唄
    // 在ServeMux 的注釋中屠列,我們可以了解到整個路由的一些機制啦逆。
    // 模式名稱固定,帶根的路徑笛洛,如"/favicon.ico"夏志,或帶根的子樹,如"/images/"(請注意尾部斜杠)苛让。
    // 較長的模式優(yōu)先于較短的模式沟蔑,因此如果存在"/images/"和"/images/thumbnails/"注冊的handler湿诊,則"/images/thumbnails/"開頭的路徑將調用后者的handler,然后前者將接收"/images/"子樹中任何其他路徑的請求瘦材,比方說"/images/xxxx"等等厅须。

    mux := http.NewServeMux()

    // 往mux上綁定了兩個handler
    mux.HandleFunc("/foo", foo)
    mux.HandleFunc("/echo/goroutines", echo)

我們看到 mux 調用了 HandleFunc,來看看他們的定義

// HandleFunc為給定pattern注冊handler
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

// 如果pattern已經存在handler了食棕,將會panic
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    
    ...

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

    if pattern[0] != '/' {
        mux.hosts = true
    }

    // 如果pattern是/tree/朗和,則為/tree插入隱式永久重定向
    // 通過顯式注冊可以覆蓋
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // 如果pattern包含host name,將其刪除并使用剩余的路徑進行重定向簿晓。
        path := pattern
        if pattern[0] != '/' {
            // strings.Index 返回子串 sep "/" 在字符串 pattern 中第一次出現(xiàn)的位置
            // 如果找不到眶拉,則返回 -1,如果 sep 為空憔儿,則返回 0镀层。
            path = pattern[strings.Index(pattern, "/"):]
        }
        url := &url.URL{Path: path}
        // 在我們的例子中pattern為"/echo/gorotine/",則會為"/echo/gorotine" 添加一個重定向
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
    }
}

看完上面的定義皿曲,我們知道路由和 handler 是怎么存儲的了。

再看看 ServeMux 結構吴侦,m 是一個字典形式的屋休,當我們調用 HandlerFunc 會把 pattern 即"/echo/goroutines"作為 key,muxEntry 作為 value备韧,muxEntry 為一個包含 pattern劫樟,handler 和 explicit 的結構。

type ServeMux struct {
    mu    sync.RWMutex // 讀寫鎖
    m     map[string]muxEntry // 存儲結構
    hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
    explicit bool  // 該pattern是否完全匹配handler
    h        Handler
    pattern  string
}

好了织堂,上述把 handler 綁定到了 server 上叠艳。那么是如何通過 url 查找 handler 的呢?先看看 ServeHTTP

// ServeHTTP將請求分派給handler易阳,該handler的pattern與請求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
    }
    // 通過Handler找到最匹配的handler來處理該請求
    h, _ := mux.Handler(r)
    // 調用處理請求
    h.ServeHTTP(w, r)
}

從上面代碼可以看到,使用 handler 的 ServeHTTP 方法去處理請求潦俺,這里又有一個疑問了拒课,為什么 ServeHTTP 的 w 是值傳遞,而 r 是引用傳遞呢事示?
先看看 w早像,r 的定義,通過觀察 ResponseWriter肖爵,和 Request 的定義就知道為什么這么做了卢鹦。

// HTTP處理程序使用ResponseWriter接口來構造HTTP響應。
// Handler.ServeHTTP方法返回后劝堪,可能就無法使用ResponseWriter了冀自。
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(int)
}

// 請求表示由服務器接收或由客戶端發(fā)送的HTTP請求揉稚。
type Request struct {
    ...
}

可以看到,ResponseWriter 是一個接口凡纳,Request 是一個結構窃植。我們往回撥一下,看看這個接口是什么荐糜。

w, err := c.readRequest(ctx)
...
// 正是readRequest返回的
serverHandler{c.server}.ServeHTTP(w, w.req)

// 再看看 readRequest的函數(shù)簽名巷怜,其實也是一個指針來的。
func (c *conn) readRequest(ctx context.Context) (w *response, err error)
{
    ...
    w = &response{
        conn:          c,
        cancelCtx:     cancelCtx,
        req:           req,
        reqBody:       req.Body,
        handlerHeader: make(Header),
        contentLength: -1,
        closeNotifyCh: make(chan bool, 1),

        // We populate these ahead of time so we're not
        // reading from req.Header after their Handler starts
        // and maybe mutates it (Issue 14940)
        wants10KeepAlive: req.wantsHttp10KeepAlive(),
        wantsClose:       req.wantsClose(),
    }
    if isH2Upgrade {
        w.closeAfterReply = true
    }
    // 這里有個令人窒息的操作暴氏,對于vegetable的我來說有點難以理解延塑。w.cw.res的res其實也是一個response,w.w的第一個w是response結構答渔,第二個w是一個*bufio.Writer結構关带。
    w.cw.res = w
    // newBufioWriterSize返回一個Writer結構的指針,而w的Writer是一個方法沼撕,注意區(qū)分
    w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
    return w, nil
}

// response的Write方法正是掉用了第二個w結構的Write方法宋雏,把數(shù)據寫入了緩沖區(qū)
// 在main.go中向response里寫數(shù)據的方法 w.Write([]byte("hi! babe~"))
func (w *response) Write(data []byte) (n int, err error) {
    return w.write(len(data), data, "")
}

// Write的具體實現(xiàn)
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
    ...
    if dataB != nil {
        return w.w.Write(dataB)
    } else {
        return w.w.WriteString(dataS)
    }
}

// w.w 也就是 *bufio.Writer結構 的方法∥癫颍可以看到通過copy把p寫入了write結構磨总。
func (b *Writer) Write(p []byte) (nn int, err error) {
    ...
    n := copy(b.buf[b.n:], p)
    b.n += n
    nn += n
    return nn, nil
}

從上面的代碼可以看到 ResponseWriter 接口,其實也是傳入了一個 response 結構的指針笼沥,又解決一個疑問蚪燕,nice

// Handler返回用于給定請求的handler,返回依據參考r.Method奔浅,r.Host和r.URL.Path等參數(shù)馆纳。它總是返回一個非空的handler(如果沒有則返回NotFound的handler)。
// 如果路徑不規(guī)范汹桦,則處理程序將會走內部生成的handler鲁驶,重定向到它的規(guī)范路徑。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            url := *r.URL
            url.Path = p
            return RedirectHandler(url.String(), StatusMovedPermanently), pattern
        }
    }

    return mux.handler(r.Host, r.URL.Path)
}

// handler函數(shù)是Handler的主要實現(xiàn)舞骆,host參數(shù)傳入請求的r.Host, path參數(shù)傳入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
}

上面的調用鏈handler.ServeHTTP -> func (mux *ServeMux) Handler(r *Request) -> func (mux *ServeMux) handler(r *Request)

當?shù)竭_func (mux *ServeMux) handler的時候灵嫌,一切邏輯就清晰了起來。先拋一個問題葛作,"/echo/"和"/echo/goroutines/"這倆怎么區(qū)分 handler寿羞?從前面的注釋,我們知道"/echo/"會處理它所有的子樹赂蠢,而"/echo/goroutines/"就是它的子樹绪穆,匹配的時候會根據最長原則,也就是會先匹配"/echo/goroutines/"的 handler,那我們來看看這個具體實現(xiàn)玖院。

// 上層調用match函數(shù)path傳入r.URL.Path
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    // m里的存儲規(guī)則是 m['/echo/gorotines/'] = EntryMux{}
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        // 從pathMatch函數(shù)可以知道"/echo/"會匹配所有它的子樹菠红,也就是類似"/echo/xxx"這些,該函數(shù)都會返回true难菌。
        // 所以下面這段邏輯就是上面問題的答案试溯。即最長原則,如果滿足len(k) > n 的情況郊酒,h會被替換成更長path的那個handler遇绞。
        // 這段代碼的時間復雜度是O(n),其他的更高效的web框架會不會實現(xiàn)一個O(lgn)的算法呢燎窘?我們知道trie樹可以做的摹闽,下次看看其他的框架怎么實現(xiàn)的
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

// 可以看到這個pathMatch是拿pattern和path做比較,
func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
        // should not happen
        return false
    }
    n := len(pattern)
    // 如果pattern不以'/'結尾 直接比較
    if pattern[n-1] != '/' {
        return pattern == path
    }
    // 關鍵部位褐健,path比pattern要長
    // 截取path[0:n] 和 pattern匹配付鹿,也就是如果我們的path為"/echo/goroutines/" 我們注冊的handler只有"/echo/"的話,那么"/echo/goroutines/" 會匹配到"/echo/"
    return len(path) >= n && path[0:n] == pattern
}

帶著一些問題蚜迅,閱讀了整個 http 請求的一些源碼舵匾,其中確實很復雜,通過了解代碼能搞大概搞清楚怎么處理的谁不,當然作者為什么這么寫腦海里仍然有一個疑問纽匙,等姿勢水平再高一點再來探究。整篇分析到此結束拍谐,怎么把這些條理化展示的水平還有待提高。

回顧一下提出的幾個問題
  • "/echo/"和"/echo/goroutines/"這怎么區(qū)分匹配 handler馏段?
  • 為什么 ServeHTTP 的 w 是值傳遞轩拨,而 r 是引用傳遞呢?
  • http server 怎么處理并發(fā)請求院喜?
  • 了解 context 在golang中的應用亡蓉?TODO
參考:

傅小黑的這篇文章框架很清晰
http://fuxiaohei.me/2016/9/20/go-and-http-server.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市喷舀,隨后出現(xiàn)的幾起案子砍濒,更是在濱河造成了極大的恐慌,老刑警劉巖硫麻,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爸邢,死亡現(xiàn)場離奇詭異,居然都是意外死亡拿愧,警方通過查閱死者的電腦和手機杠河,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人券敌,你說我怎么就攤上這事唾戚。” “怎么了待诅?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵叹坦,是天一觀的道長。 經常有香客問我卑雁,道長募书,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任序厉,我火速辦了婚禮锐膜,結果婚禮上,老公的妹妹穿的比我還像新娘弛房。我一直安慰自己道盏,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布文捶。 她就那樣靜靜地躺著荷逞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粹排。 梳的紋絲不亂的頭發(fā)上种远,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音顽耳,去河邊找鬼坠敷。 笑死,一個胖子當著我的面吹牛射富,可吹牛的內容都是我干的膝迎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胰耗,長吁一口氣:“原來是場噩夢啊……” “哼限次!你這毒婦竟也來了?” 一聲冷哼從身側響起柴灯,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤卖漫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赠群,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羊始,經...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年查描,在試婚紗的時候發(fā)現(xiàn)自己被綠了店枣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片速警。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鸯两,靈堂內的尸體忽然破棺而出闷旧,到底是詐尸還是另有隱情,我是刑警寧澤钧唐,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布忙灼,位于F島的核電站,受9級特大地震影響钝侠,放射性物質發(fā)生泄漏该园。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一帅韧、第九天 我趴在偏房一處隱蔽的房頂上張望里初。 院中可真熱鬧,春花似錦忽舟、人聲如沸双妨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽像鸡。三九已至笋籽,卻和暖如春基显,著一層夾襖步出監(jiān)牢的瞬間分歇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工勒叠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兜挨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓眯分,卻偏偏與公主長得像拌汇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颗搂,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容