圖文講透 Golang 標準庫 net-http 實現原理 -- 服務端
前言
今天分享下 Go 語言 net/http 標準庫的內部實現邏輯,文章將從客戶端 (Client)-- 服務端(Server) 兩個方向作爲切入點,進而一步步分析 http 標準庫內部是如何運作的。
由於會涉及到不少的代碼流程的走讀,寫完後覺得放在一篇文章中會過於長,可能在閱讀感受上會不算很好,因此分爲【Server--Client 兩個篇文章】進行發佈。
本文內容是【服務端 Server 部分】,文章代碼版本是 Golang 1.19,文中會涉及較多的代碼,需要耐心閱讀,不過我會在儘量將註釋也邏輯闡述清楚。先看下所有內容的大綱:
Go 語言的 net/http 中同時封裝好了 HTTP 客戶端和服務端的實現,這裏分別舉一個簡單的使用示例。
Server 啓動示例
Server 和 Client 端的代碼實現來自 net/http 標準庫的文檔,都是簡單的使用,而且用很少的代碼就可以啓動一個服務!
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "xiaoxu code")
})
http.ListenAndServe(":8080", nil)
上面代碼中:
HandleFunc 方法註冊了一個請求路徑 /hello 的 handler 函數
ListenAndServe 指定了 8080 端口進行監聽和啓動一個 HTTP 服務端
Client 發送請求示例
HTTP 包一樣可以發送請求,我們以 Get 方法來發起請求,這裏同樣也舉一個簡單例子:
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 服務的啓動和發送 http 請求,其背後是如何進行封裝的,在接下的章節會講清楚!
服務端 Server
我們先預覽下圖過程,對整個服務端做的事情有個瞭解
從圖中大致可以看出主要有這些流程:
-
1. 註冊 handler 到 map 中,map 的 key 是鍵值路由
-
2. handler 註冊完之後就開啓循環監聽,監聽到一個連接就會異步創建一個 Goroutine
-
3. 在創建好的 Goroutine 內部會循環的等待接收請求數據
-
4. 接受到請求後,根據請求的地址去處理器路由表 map 中匹配對應的 handler,然後執行 handler
Server 結構體
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 結構體
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry
hosts bool
}
字段說明:
-
• sync.RWMutex:這是讀寫互斥鎖,允許 goroutine 併發讀取路由表,在修改路由 map 時獨佔
-
• map[string]muxEntry:map 結構維護 pattern (路由) 到 handler (處理函數) 的映射關係,精準匹配
-
• []muxEntry:存儲 "/" 結尾的路由,切片內按從最長到最短的順序排列,用作模糊匹配 patter 的 muxEntry
-
• hosts:是否有任何模式包含主機名
Mux 是【多路複用器】的意思,ServeMux 就是服務端路由 http 請求的多路複用器。
👉 作用: 管理和處理程序來處理傳入的 HTTP 請求
✏️ 原理:內部通過一個 map 類型 維護了從 pattern (路由) 到 handler (處理函數) 的映射關係,收到請求後根據路徑匹配找到對應的處理函數 handler,處理函數進行邏輯處理。
路由註冊
通過對 HandleFunc 的調用追蹤,內部的調用核心實現如下:
瞭解完流程之後接下來繼續追函數看代碼
var DefaultServeMux = &defaultServeMux
// 默認的ServeMux
var defaultServeMux ServeMux
// HandleFunc註冊函數
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
DefaultServeMux 是 ServeMux 的默認實例。
//接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
//HandlerFunc爲函數類型
type HandlerFunc func(ResponseWriter, *Request)
//實現了Handler接口
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
...
// handler是真正處理請求的函數
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc 函數類型是一個適配器,是 Handler 接口的具體實現類型,因爲它實現了 ServeHTTP 方法。
🚩 HandlerFunc(handler), 通過類型轉換的方式【handler -->HandlerFunc】將一個出入參形式爲 func(ResponseWriter, *Request) 的函數轉換爲 HandlerFunc 類型,而 HandlerFunc 實現了 Handler 接口,所以這個被轉換的函數 handler 可以被當做一個 Handler 對象進行賦值。
✏️ 好處:HandlerFunc(handler) 方式實現靈活的路由功能,方便的將普通函數轉換爲 Http 處理程序,兼容註冊不同具體的業務邏輯的處理請求。
你看,mux.Handle 的第二個參數 Handler 就是個接口,ServeMux.Handle 就是路由模式和處理函數在 map 中進行關係映射。
ServeMux.Handle
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
// 檢查路由和處理函數
...
//檢查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以'/'結尾
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
Handle 的實現主要是將傳進來的 pattern 和 handler 保存在 muxEntry 結構中,然後將 pattern 作爲 key,把 muxEntry 添加到 DefaultServeMux 的 Map 裏。
如果路由表達式以 '/' 結尾,則將對應的 muxEntry 對象加入到 []muxEntry 切片中,然後通過 appendSorted 對路由按從長到短進行排序。
🚩 注:
map[string]muxEntry 的 map 使用哈希表是用於路由精確匹配
[]muxEntry 用於部分匹配模式
到這裏就完成了路由和 handle 的綁定註冊了,至於爲什麼分了兩個模式,在後面會說到,接下來就是啓動服務進行監聽的過程。
監聽和服務啓動
同樣的我用圖的方式監聽和服務啓動的函數調用鏈路畫出來,讓大家先有個印象。
結合圖會對後續結合代碼邏輯更清晰,知道這塊代碼調用屬於哪個階段!
ListenAndServe 啓動服務:
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)
}
net.Listen 實現了 TCP 協議上監聽本地的端口 8080 (ListenAndServe() 中傳過來的),Server.Serve 接受 net.Listener 實例傳入,然後爲每個連接創建一個新的服務 goroutine
使用 net.Listen 函數實現網絡監聽需要經過以下幾個步驟:
-
- 調用 net.Listen 函數,指定網絡類型和監聽地址。
-
- 使用 listener.Accept 函數接受客戶端的連接請求。
-
- 在一個獨立的 goroutine 中處理每個連接。
-
4. 在處理完連接後,調用 conn.Close() 來關閉連接
Server.Serve:
func (srv *Server) Serve(l net.Listener) error {
origListener := l
//內部實現Once是隻執行一次動作的對象
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 循環調用 Listener.Accept 方法循環讀取新連接
-
• 讀取到客戶端請求後會創建一個 goroutine 異步執行 conn.serve 方法負責處理
type onceCloseListener struct {
net.Listener
once sync.Once
closeErr error
}
onceCloseListener 是 sync.Once 的一次執行對象,當且僅當第一次被調用時才執行函數。
*conn.serve():
func (c *conn) serve(ctx context.Context) {
...
// 初始化conn的一些參數
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)
...
// 調用ServeHTTP來處理請求
serverHandler{c.server}.ServeHTTP(w, w.req)
}
}
conn.serve 是處理客戶端連接的核心方法,主要是通過 for 循環不斷循環讀取客戶端請求,然後根據請求調用相應的處理函數。
c.readRequest(ctx) 方法是用來讀取客戶端的請求,然後返回一個 response 類型的 w 和一個錯誤 err
最終是通過 serverHandler{c.server}.ServeHTTP(w, w.req) 調用 ServeHTTP 處理連接客戶端發送的請求。
OK,經歷了前面監聽的過程,現在客戶端請求已經拿到了,接下來就是到了核心的處理請求的邏輯了,打起十二分精神哦!🤣🤣
請求處理
serverHandler.ServeHTTP:
上面說到的 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傳的是nil就執行 DefaultServeMux.ServeHTTP() 方法
handler.ServeHTTP(rw, req)
}
獲取 Server 的 handler 流程:
-
1. 先獲取 sh.srv.Handler 的值,判斷是否爲 nil
-
2. 如果爲 nil 則取全局單例 DefaultServeMux 這個 handler
-
3. PTIONS Method 請求且 URI 是 *,就使用 globalOptionsHandler
🚩 注:這個 handler 其實就是在 ListenAndServe() 中的第二個參數
ServeMux.ServeHTTP
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
....
h, _ := mux.Handler(r)
// 執行匹配到的路由的ServeHTTP方法
h.ServeHTTP(w, r)
}
ServeMux.ServeHTTP() 方法主要代碼可以分爲兩步:
-
1. 通過 ServerMux.Handler() 方法獲取到匹配的處理函數 h
-
2. 調用 Handler.ServeHTTP() 執行匹配到該路由的函數來處理請求 (h 實現了 ServeHTTP 方法)
ServerMux.Handler():
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
//在mux.m和mux.es中
//根據host/url.path尋找對應的handler
return mux.handler(host, r.URL.Path)
}
在 ServeMux.Handler() 方法內部,會調用 ServerMux.handler(host, r.URL.Path) 方法來查找匹配的處理函數。
ServeMux.match
ServeMux.match()方法用於根據給定的具體路徑 path 找到最佳匹配的路由,並返回 Handler 和路徑。
值得一提的是,如果 mux.m 中不存在 path 完全匹配的路由時,會繼續遍歷 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, ""
}
最後調用 handler.ServeHTTP 方法進行請求的處理和響應,而這個被調用的函數就是我們之前在路由註冊時對應的函數。
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
到這裏整個服務的流程就到這裏了,現在有對這塊有印象了嗎?
關於 Client 篇的文章會馬上更新,敬請期待,希望看完之後會對 net/http 有個新認識!
歡迎朋友們關注我的公衆號📢📢:【小許 code】🤣🤣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/e7Z_kZrayTFx7y0hlzoTdg?poc_token=HOsUt2WjwyqHgxsZ9G8NcLt3PqphqdBezUJcDitO