一文帶你吃透Golang中net/http標準庫服務(wù)端
前言
今天分享下Go語言net/http標準庫的實現(xiàn)邏輯,文章將從客戶端(Client)--服務(wù)端(Server)兩個方向作為切入點,進而一步步分析http標準庫內(nèi)部是如何運作的。
由于會涉及到不少的代碼流程的走讀,寫完后覺得放在一篇文章中會過于長,可能在閱讀感受上會不算很好,因此分為【Server--Client兩個篇文章】進行發(fā)布。
本文內(nèi)容是【服務(wù)端Server部分】,文章代碼版本是Golang 1.19,文中會涉及較多的代碼,需要耐心閱讀,不過我會在盡量將注釋也邏輯闡述清楚。先看下所有內(nèi)容的大綱:
Go 語言的 net/http 中同時封裝好了 HTTP 客戶端和服務(wù)端的實現(xiàn),這里分別舉一個簡單的使用示例。
Server啟動示例
Server和Client端的代碼實現(xiàn)來自net/http標準庫的文檔,都是簡單的使用,而且用很少的代碼就可以啟動一個服務(wù)!
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "xiaoxu code") }) http.ListenAndServe(":8080", nil)
上面代碼中:
HandleFunc 方法注冊了一個請求路徑 /hello 的 handler 函數(shù)
ListenAndServe指定了8080端口進行監(jiān)聽和啟動一個HTTP服務(wù)端
Client發(fā)送請求示例
HTTP 包一樣可以發(fā)送請求,我們以Get方法來發(fā)起請求,這里同樣也舉一個簡單例子:
resp, err := http.Get("http://example.com/") if err != nil { fmt.Println(err) return } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body))
是不是感覺使用起來還是很簡單的,短短幾行代碼就完成了http服務(wù)的啟動和發(fā)送http請求,其背后是如何進行封裝的,在接下的章節(jié)會講清楚!
服務(wù)端 Server
我們先預覽下圖過程,對整個服務(wù)端做的事情有個了解
從圖中大致可以看出主要有這些流程:
1. 注冊handler到map中,map的key是鍵值路由
2. handler注冊完之后就開啟循環(huán)監(jiān)聽,監(jiān)聽到一個連接就會異步創(chuàng)建一個 Goroutine
3. 在創(chuàng)建好的 Goroutine 內(nèi)部會循環(huán)的等待接收請求數(shù)據(jù)
4. 接受到請求后,根據(jù)請求的地址去處理器路由表map中匹配對應(yīng)的handler,然后執(zhí)行handler
Server結(jié)構(gòu)體
type Server struct { Addr string Handler Handler mu sync.Mutex ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration TLSConfig *tls.Config ConnState func(net.Conn, ConnState) activeConn map[*conn]struct{} doneChan chan struct{} listeners map[*net.Listener]struct{} ... }
我們在下圖中解釋了部分字段代表的意思
ServeMux結(jié)構(gòu)體
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool }
字段說明:
• sync.RWMutex:這是讀寫互斥鎖,允許goroutine 并發(fā)讀取路由表,在修改路由map時獨占
• map[string]muxEntry:map結(jié)構(gòu)維護pattern (路由) 到 handler (處理函數(shù)) 的映射關(guān)系,精準匹配
• []muxEntry:存儲 "/" 結(jié)尾的路由,切片內(nèi)按從最長到最短的順序排列,用作模糊匹配patter的muxEntry
• hosts:是否有任何模式包含主機名
Mux是【多路復用器】的意思,ServeMux就是服務(wù)端路由http請求的多路復用器。
作用: 管理和處理程序來處理傳入的HTTP請求
原理:內(nèi)部通過一個 map類型 維護了從 pattern (路由) 到 handler (處理函數(shù)) 的映射關(guān)系,收到請求后根據(jù)路徑匹配找到對應(yīng)的處理函數(shù)handler,處理函數(shù)進行邏輯處理。
路由注冊
通過對HandleFunc的調(diào)用追蹤,內(nèi)部的調(diào)用核心實現(xiàn)如下:
了解完流程之后接下來繼續(xù)追函數(shù)看代碼
var DefaultServeMux = &defaultServeMux // 默認的ServeMux var defaultServeMux ServeMux // HandleFunc注冊函數(shù) func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
DefaultServeMux是ServeMux的默認實例。
//接口 type Handler interface { ServeHTTP(ResponseWriter, *Request) } //HandlerFunc為函數(shù)類型 type HandlerFunc func(ResponseWriter, *Request) //實現(xiàn)了Handler接口 func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { ... // handler是真正處理請求的函數(shù) mux.Handle(pattern, HandlerFunc(handler)) }
HandlerFunc函數(shù)類型是一個適配器,是Handler接口的具體實現(xiàn)類型,因為它實現(xiàn)了ServeHTTP方法。
HandlerFunc(handler), 通過類型轉(zhuǎn)換的方式【handler -->HandlerFunc】將一個出入?yún)⑿问綖閒unc(ResponseWriter, *Request)的函數(shù)轉(zhuǎn)換為HandlerFunc類型,而HandlerFunc實現(xiàn)了Handler接口,所以這個被轉(zhuǎn)換的函數(shù)handler可以被當做一個Handler對象進行賦值。
好處:HandlerFunc(handler)方式實現(xiàn)靈活的路由功能,方便的將普通函數(shù)轉(zhuǎn)換為Http處理程序,兼容注冊不同具體的業(yè)務(wù)邏輯的處理請求。
你看,mux.Handle的第二個參數(shù)Handler就是個接口,ServeMux.Handle就是路由模式和處理函數(shù)在map中進行關(guān)系映射。
ServeMux.Handle
func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() // 檢查路由和處理函數(shù) ... //檢查pattern是否存在 ... //如果 mux.m 為nil 進行make初始化 map if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} //注冊好路由都會存放到mux.m里面 mux.m[pattern] = e //patterm以'/'結(jié)尾 if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } }
Handle的實現(xiàn)主要是將傳進來的pattern和handler保存在muxEntry結(jié)構(gòu)中,然后將pattern作為key,把muxEntry添加到DefaultServeMux的Map里。
如果路由表達式以 '/' 結(jié)尾,則將對應(yīng)的muxEntry對象加入到[]muxEntry切片中,然后通過appendSorted對路由按從長到短進行排序。
注:
- map[string]muxEntry 的map使用哈希表是用于路由精確匹配
- []muxEntry用于部分匹配模式
到這里就完成了路由和handle的綁定注冊了,至于為什么分了兩個模式,在后面會說到,接下來就是啟動服務(wù)進行監(jiān)聽的過程。
監(jiān)聽和服務(wù)啟動
同樣的我用圖的方式監(jiān)聽和服務(wù)啟動的函數(shù)調(diào)用鏈路畫出來,讓大家先有個印象。
結(jié)合圖會對后續(xù)結(jié)合代碼邏輯更清晰,知道這塊代碼調(diào)用屬于哪個階段!
ListenAndServe啟動服務(wù):
func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } // 指定網(wǎng)絡(luò)地址并監(jiān)聽 ln, err := net.Listen("tcp", addr) if err != nil { return err } // 接收處理請求 return srv.Serve(ln) }
net.Listen 實現(xiàn)了TCP協(xié)議上監(jiān)聽本地的端口8080 (ListenAndServe()中傳過來的),Server.Serve接受 net.Listener實例傳入,然后為每個連接創(chuàng)建一個新的服務(wù)goroutine
使用net.Listen函數(shù)實現(xiàn)網(wǎng)絡(luò)監(jiān)聽需要經(jīng)過以下幾個步驟:
1. 調(diào)用net.Listen函數(shù),指定網(wǎng)絡(luò)類型和監(jiān)聽地址。
2. 使用listener.Accept函數(shù)接受客戶端的連接請求。
3. 在一個獨立的goroutine中處理每個連接。
4. 在處理完連接后,調(diào)用conn.Close()來關(guān)閉連接
Server.Serve:
func (srv *Server) Serve(l net.Listener) error { origListener := l //內(nèi)部實現(xiàn)Once是只執(zhí)行一次動作的對象 l = &onceCloseListener{Listener: l} defer l.Close() ... ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { //rw為可理解為tcp連接 rw, err := l.Accept() ... connCtx := ctx ... c := srv.newConn(rw) // go c.serve(connCtx) } }
使用 for + listener.accept 處理客戶端請求
• 在for 循環(huán)調(diào)用 Listener.Accept 方法循環(huán)讀取新連接
• 讀取到客戶端請求后會創(chuàng)建一個 goroutine 異步執(zhí)行 conn.serve 方法負責處理
type onceCloseListener struct { net.Listener once sync.Once closeErr error }
onceCloseListener 是sync.Once的一次執(zhí)行對象,當且僅當?shù)谝淮伪徽{(diào)用時才執(zhí)行函數(shù)。
*conn.serve():
func (c *conn) serve(ctx context.Context) { ... // 初始化conn的一些參數(shù) c.remoteAddr = c.rwc.RemoteAddr().String() c.r = &connReader{conn: c} c.bufr = newBufioReader(c.r) c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) for { // 讀取客戶端請求 w, err := c.readRequest(ctx) ... // 調(diào)用ServeHTTP來處理請求 serverHandler{c.server}.ServeHTTP(w, w.req) } }
conn.serve是處理客戶端連接的核心方法,主要是通過for循環(huán)不斷循環(huán)讀取客戶端請求,然后根據(jù)請求調(diào)用相應(yīng)的處理函數(shù)。
c.readRequest(ctx)方法是用來讀取客戶端的請求,然后返回一個response類型的w和一個錯誤err
最終是通過serverHandler{c.server}.ServeHTTP(w, w.req) 調(diào)用ServeHTTP處理連接客戶端發(fā)送的請求。
OK,經(jīng)歷了前面監(jiān)聽的過程,現(xiàn)在客戶端請求已經(jīng)拿到了,接下來就是到了核心的處理請求的邏輯了,打起十二分精神哦!
serverHandler.ServeHTTP:
上面說到的 serverHandler{c.server}.ServeHTTP(w, w.req) 其實就是下面函數(shù)的實現(xiàn)。
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傳的是nil就執(zhí)行 DefaultServeMux.ServeHTTP() 方法 handler.ServeHTTP(rw, req) }
獲取Server的handler流程:
1. 先獲取 sh.srv.Handler 的值,判斷是否為nil
2. 如果為nil則取全局單例 DefaultServeMux這個handler
3. PTIONS Method 請求且 URI 是 *,就使用globalOptionsHandler
注:這個handler其實就是在ListenAndServe()中的第二個參數(shù)
ServeMux.ServeHTTP
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { .... h, _ := mux.Handler(r) // 執(zhí)行匹配到的路由的ServeHTTP方法 h.ServeHTTP(w, r) }
ServeMux.ServeHTTP()方法主要代碼可以分為兩步:
1. 通過 ServerMux.Handler() 方法獲取到匹配的處理函數(shù) h
2. 調(diào)用 Handler.ServeHTTP() 執(zhí)行匹配到該路由的函數(shù)來處理請求 (h實現(xiàn)了ServeHTTP方法)
ServerMux.Handler():
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { ... //在mux.m和mux.es中 //根據(jù)host/url.path尋找對應(yīng)的handler return mux.handler(host, r.URL.Path) }
在 ServeMux.Handler() 方法內(nèi)部,會調(diào)用 ServerMux.handler(host, r.URL.Path) 方法來查找匹配的處理函數(shù)。
ServeMux.match
ServeMux.match()方法用于根據(jù)給定的具體路徑 path 找到最佳匹配的路由,并返回Handler和路徑。
值得一提的是,如果 mux.m 中不存在 path 完全匹配的路由時,會繼續(xù)遍歷 mux.es 字段中保存的模糊匹配路由。
func (mux *ServeMux) match(path string) (h Handler, pattern string) { // 是否完全匹配 v, ok := mux.m[path] if ok { return v.h, v.pattern } // mux.es是按pattern從長到短排列 for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" }
最后調(diào)用 handler.ServeHTTP 方法進行請求的處理和響應(yīng),而這個被調(diào)用的函數(shù)就是我們之前在路由注冊時對應(yīng)的函數(shù)。
type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
到這里整個服務(wù)的流程就到這里了,現(xiàn)在有對這塊有印象了嗎?
以上就是一文帶你吃透Golang中net/http標準庫服務(wù)端的詳細內(nèi)容,更多關(guān)于Go net/http標準庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang小數(shù)操作指南之判斷小數(shù)點位數(shù)與四舍五入
這篇文章主要給大家介紹了關(guān)于Golang小數(shù)操作指南之判斷小數(shù)點位數(shù)與四舍五入的相關(guān)資料,文中通過實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2022-03-03Go語言基于viper實現(xiàn)apollo多實例快速
viper是適用于go應(yīng)用程序的配置解決方案,這款配置管理神器,支持多種類型、開箱即用、極易上手。本文主要介紹了如何基于viper實現(xiàn)apollo多實例快速接入,感興趣的可以了解一下2023-01-01