【Golang 源碼包分析】01 net/http包

快速搭建 Web 服務(wù)器

在Golang僅需要幾行代碼,便可以建立一個(gè)簡(jiǎn)單的 Web 服務(wù):

package main

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

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "Hello, world!\n")
    }

    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":9999", nil))
}

編譯運(yùn)行,如果不出意外的話,訪問(wèn) http://localhost:9999/hello,你將會(huì)在瀏覽器上看到Hello, world!

當(dāng)時(shí)剛接觸到Go的時(shí)候,我是很震驚的?。。∫郧皩?xiě) PHP,要配 web 服務(wù),還得搭個(gè) Nginx或Apache 服務(wù)器呢,配置下監(jiān)聽(tīng)的接口轉(zhuǎn)發(fā)到fastcgi進(jìn)行處理。Go這么流氓嗎,直接幾行代碼就完事了?是的,Go 就真的很流氓,它直接監(jiān)聽(tīng)了 TCP 端口。

Web 服務(wù)器運(yùn)轉(zhuǎn)機(jī)制

以使用TCP協(xié)議通訊的socket為例,socket通信交互流程大概是這樣子的

file

我們來(lái)看一段代碼 net 包開(kāi)頭的一段示例代碼

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    // handle error
}
for {
    conn, err := ln.Accept()
    if err != nil {
        // handle error
    }
    go handleConnection(conn)
}

創(chuàng)建 Listen Socket,監(jiān)聽(tīng)指定端口8080,等待客戶(hù)端的請(qǐng)求,創(chuàng)建 goroutine 處理請(qǐng)求。

實(shí)際上 Go 的 web server 也就是這么個(gè)處理邏輯,只是在這多了一些處理邏輯:請(qǐng)求的協(xié)議解析,讀取客戶(hù)端提交的數(shù)據(jù),路由分配(根據(jù)請(qǐng)求分配對(duì)應(yīng)的 handler 處理請(qǐng)求),封裝響應(yīng)數(shù)據(jù)。

http 包源碼分析

還記得快速搭建 web 服務(wù)器的時(shí)候有行代碼:http.ListenAndServe(":9999", nil),Go 就是通過(guò)這個(gè)方法來(lái)處理上面 端口監(jiān)聽(tīng)、請(qǐng)求解析、路由分配、響應(yīng)處理。

1. 端口監(jiān)聽(tīng)

讓我們追進(jìn)去看看,實(shí)際上,初始化一個(gè)server對(duì)象,調(diào)用了 server 的 ListenAndServe 方法。

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

再追進(jìn)去看看,瞧,這不是嘛 ln, err := net.Listen("tcp", addr) ,用TCP協(xié)議搭建了一個(gè)服務(wù),監(jiān)聽(tīng)著我們?cè)O(shè)置的端口呢。

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

2. 接受請(qǐng)求

是不是有點(diǎn)小興奮了,讓我們?cè)倮^續(xù)追進(jìn)去看看 srv.Serve(ln),核心代碼片段:

    for {
        // 接受監(jiān)聽(tīng)器listener的請(qǐng)求
        rw, e := l.Accept()
        if e != nil {
            // 監(jiān)聽(tīng)是否關(guān)閉信號(hào)
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
        }
            ……
        // 創(chuàng)建新連接
        c := srv.newConn(rw)
        // 再返回之前,設(shè)置連接狀態(tài)
        c.setState(c.rwc, StateNew) // before Serve can return
        // 創(chuàng)建goroutine,真正處理連接
        go c.serve(ctx)
    }

可以看到,這里用了一個(gè)死循環(huán) for{} 等待。

  1. 首先通過(guò) listener.Accept 接受請(qǐng)求,是不是跟上面提到的socket的運(yùn)行機(jī)制很像了。
  2. 接著用接受到的請(qǐng)求創(chuàng)建一個(gè)新的 Conn,并設(shè)置為New狀態(tài)。
  3. 最后創(chuàng)建 goroutine,真正的處理連接。

知識(shí)點(diǎn):每個(gè)請(qǐng)求都會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的goroutine去處理,所以各個(gè)請(qǐng)求之間是相互不影響的,同時(shí)提高并發(fā)性能。

3. 讀取請(qǐng)求并解析

來(lái),我們?cè)龠M(jìn)去 go c.serve(ctx) 看看,忽略一些細(xì)節(jié),重點(diǎn)關(guān)注關(guān)鍵的流程

for {
        ……
    
        // 讀request請(qǐng)求
        w, err := c.readRequest(ctx)
        
        ……
        // 調(diào)用業(yè)務(wù)層定義的路由
        serverHandler{c.server}.ServeHTTP(w, w.req)
        
        ……
        // flush刷io buffer的數(shù)據(jù)
        w.finishRequest()

readRequest 便是讀取數(shù)據(jù),解析請(qǐng)求的地方,包括解析請(qǐng)求的header、body,和一些基本的校驗(yàn),比如header頭信息,請(qǐng)求method等。

最后將請(qǐng)求的數(shù)據(jù)賦值到Request,并初始化Response對(duì)象,供業(yè)務(wù)層調(diào)用。

4. 路由分配handler

上面關(guān)鍵流程已經(jīng)看到了serverHandler{c.server}.ServeHTTP(w, w.req),這個(gè)實(shí)際上就是調(diào)用最開(kāi)始在main函數(shù)定義的handler,并將處理好的Request、Response對(duì)象作為參數(shù)傳入。

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

還記得嗎,我們最上面是這樣調(diào)用的 http.ListenAndServe(":9999", nil),第二個(gè)參數(shù)是nil。

所以,你看在ServeHTTP中,handler = DefaultServeMux,我們使用了默認(rèn)的路由器,如果 ListenAndServe 不是傳nil的話,那就會(huì)使用你自己定義的路由器。

我們來(lái)看看httprouter的例子,你就會(huì)明白了:

package main

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

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)

    log.Fatal(http.ListenAndServe(":8080", router))
}

看到了吧,上面router便是自定義的路由器,在走到上面ServerHTTP的時(shí)候,便是使用指定的路由器進(jìn)行路由。

5. 路由分配handler

好了,我們知道了使用默認(rèn)的路由器(DefaultServeMux),再看看它是怎么根據(jù)路徑找對(duì)應(yīng)handler的吧~~~

我想你看到這已經(jīng)累了,請(qǐng)不要輕易離開(kāi),學(xué)海無(wú)涯苦作舟啊~~

路由的過(guò)程里面我們只要弄懂下面的三個(gè)問(wèn)題,就知道 Go 自帶的路由是怎么運(yùn)行的了:

  1. 什么時(shí)候注冊(cè)的路由?
  2. 如何根據(jù)注冊(cè)的路由找對(duì)應(yīng)的handler?
  3. 如果沒(méi)注冊(cè)路由訪問(wèn)會(huì)返回什么?

好,我們首先來(lái)看看默認(rèn)路由器(DefaultServeMux)的結(jié)構(gòu)定義:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry 
    hosts bool
}

其中 m 是一個(gè)map,用來(lái)存儲(chǔ)路由pattern與handler的關(guān)系;es 是一個(gè)slice,將路由按長(zhǎng)度從大到小排序存儲(chǔ)起來(lái)。

匹配規(guī)則:首先精確匹配 m 中的pattern;如果在 m 不能精確匹配路徑時(shí),會(huì)在 es 中找到最接近的路由規(guī)則:比如注冊(cè)了兩個(gè)路徑 /a/b/ /a/ ,當(dāng)請(qǐng)求URL是 /a/b/c時(shí),會(huì)匹配到 /a/b/ 而不是 /a/

1. 什么時(shí)候注冊(cè)的路由?

還記得吧,在 ListenAndServe 之前,有這么一行代碼,http.HandleFunc("/hello", helloHandler),這個(gè)便是在注冊(cè)路由。來(lái)我們把一些檢查代碼去掉,看看關(guān)鍵代碼:

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

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

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    ……
    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.mServeMux.es 寫(xiě)的全過(guò)程

2. 如何根據(jù)注冊(cè)的路由找對(duì)應(yīng)的handler?

其實(shí)知道怎么寫(xiě)路由表,大概也能猜到是怎么進(jìn)行找了,無(wú)非就是從上面的 m,es 進(jìn)行匹配。

我們上面從端口監(jiān)聽(tīng),接受請(qǐng)求,讀取請(qǐng)求并解析,再到路由分配handler,一路追到 handler.ServeHTTP(rw, req),現(xiàn)在再看看這段代碼的實(shí)現(xiàn)

// 根據(jù)預(yù)設(shè)的pattern,將request分配最匹配的handler處理
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    ……
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

……

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // 優(yōu)先查找m表
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // 未精確匹配成功,查詢(xún)es(已排序),路徑長(zhǎng)的優(yōu)先匹配
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

3. 如果沒(méi)注冊(cè)路由訪問(wèn)會(huì)返回什么?

這個(gè)問(wèn)題其實(shí)上面代碼已經(jīng)看到了,Go 內(nèi)置了一個(gè) NotFoundHandler(),返回 "404 page not found"

好了,我想你看完這個(gè),應(yīng)該對(duì) Go 如何用幾行代碼搭建 Web 服務(wù)器有了基本了解了吧。

結(jié)尾

個(gè)人建議,看完這個(gè)文章后,自己點(diǎn)進(jìn)去 Go 的源碼包里,我想你會(huì)有新收獲的。


file

如果有任何疑問(wèn)或錯(cuò)誤可直接留言,我看到會(huì)回復(fù)的~~

公眾號(hào)『知否專(zhuān)欄』,歡迎關(guān)注。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容