要想學好 gin 框架,首先要學習 net/http 服務,而二者的關系又是重中之重。
本文所要做的任務就是將二者“連接” 起來,讓讀者掌握其中之精髓。
一、Golang HTTP 標準庫示例
使用 golang 啟動 http 服務非常簡單,就是一個標準的 C/S 架構服務,代碼:
package mainimport ("fmt""net/http"
)func pingHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, net/http! v2\n")
}
func main() {http.HandleFunc("/ping", pingHandler)http.ListenAndServe(":8091", nil)
}
這段代碼主要完成了兩件事:
- 通過
http.HandleFunc
方法注冊里 處理函數 - 啟動 指定端口的 http 服務。
那背后隱藏了什么呢,我們主要致力于挖掘出核心的東西:
- 路徑注冊、匹配是如何實現的,依托的核心是什么? 關鍵詞:前綴樹、暴露接口
- http 服務的請求路徑是怎么樣的? 關鍵詞:one-loop 模型
二、Golang HTTP 標準庫 原理
2.1 服務注冊
首先我們圍繞 http.HandleFunc
源碼展開:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMuxtype ServeMux struct { // 對 Handler 的具體實現,內部通過一個 map 維護了從 path 到 handler 的映射關系.mu sync.RWMutexm map[string]muxEntryes []muxEntry // slice of entries sorted from longest to shortest.hosts bool // whether any patterns contain hostnames
}type muxEntry struct { // 一個 handler 單元,內部包含了請求路徑 path + 處理函數 handler 兩部分.h Handlerpattern string
}
可以看到,是通過默認數據 defaultServeMux 實現的,該結構重點包含的方法:ServeHTTP 和 HandleFunc
首先講解下為什么 ServeHTTP
方法很重要,因為 ServeMux 是對 Handler 的具體實現:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {h, _ := mux.Handler(r)h.ServeHTTP(w, r)
}
而 Handler 的定義如下:
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
Handler 是一個 interface,暴露了方法: ServeHTTP,該方法根據 http 請求 Request 中的請求路徑 path 映射到對應的 handler 處理函數,對請求進行處理和響應.
這種實現接口方法有什么好處呢,這里我們先留一個懸念,之后我們可以在后面的請求流程中看到,暫且不表。
其次我們來看 HandleFunc
方法,內部會將處理函數 handler 轉為實現了 ServeHTTP 方法的 HandlerFunc 類型,將其作為 Handler interface 的實現類注冊到 ServeMux 的路由 map 當中.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {mux.Handle(pattern, HandlerFunc(handler))
}type HandlerFunc func(ResponseWriter, *Request)// Handle registers the handler for the given pattern.
func (mux *ServeMux) Handle(pattern string, handler Handler) {// 將 path 和 handler 包裝成一個 muxEntry,以 path 為 key 注冊到路由 map ServeMux.m 中
}
2.2 服務啟動
http.ListenAndServe
通過調用 net/http 包公開的方法,實現對服務端的一鍵啟動. 內部定義了一個新的 Server 對象,嵌套執行 Server.ListenAndServe 方法:
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}
Server.ListenAndServe 方法中,根據用戶傳入的端口,申請到一個監聽器 listener,繼而調用 Server.Serve 方法.
func (srv *Server) ListenAndServe() error {addr := srv.Addrln, err := net.Listen("tcp", addr)return srv.Serve(ln)
}
Server.Serve 方法很核心,體現了 http 服務端的運行架構:for + listener.accept 模式:
func (srv *Server) Serve(l net.Listener) error {ctx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, err := l.Accept()// ...connCtx := ctx// ...c := srv.newConn(rw)// ...go c.serve(connCtx)}}
}
主要實現功能:
- 將 server 封裝成一組 kv 對,添加到 context 當中
- 開啟 for 循環,每輪循環調用 Listener.Accept 方法阻塞等待新連接到達
- 每有一個連接到達,創建一個 goroutine 異步執行 conn.serve 方法負責處理
其中 conn.serve 是響應客戶端連接的核心方法:
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {// ...c.r = &connReader{conn: c}c.bufr = newBufioReader(c.r)c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)for {w, err := c.readRequest(ctx)// 核心serverHandler{c.server}.ServeHTTP(w, w.req)}
可以看下核心的實現:
type serverHandler struct {srv *Server
}func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerhandler.ServeHTTP(rw, req)
}
在 serveHandler.ServeHTTP 方法中,會對 Handler 作判斷,倘若其未聲明,則取全局單例 DefaultServeMux 進行路由匹配,呼應了 http.HandleFunc 中的處理細節。
基于接口而非實現,此后開始調用實現的 ServeHTTP 方法,匹配到相應的處理函數后執行:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {h, _ := mux.Handler(r)h.ServeHTTP(w, r)
}func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {return mux.handler(host, r.URL.Path)
}func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {mux.mu.RLock()defer mux.mu.RUnlock()h, pattern = mux.match(path)
}
三、Gin 框架原理
Gin 是在 Golang HTTP 標準庫 net/http 基礎之上的再封裝,兩者的交互邊界圖。
可以看出,在 net/http 的既定框架下,gin 所做的是提供了一個 gin.Engine 對象作為 Handler 注入其中,從而實現路由注冊/匹配、請求處理鏈路的優化。
我們通過一個 簡化版 gin進行學習核心思想,示例代碼:
func testMiddle(c *gin.Context) {fmt.Println("middle test")
}func main() {// 構造默認配置的 gin.Engineengine := gin.Default()// 注冊中間件engine.Use(testMiddle)// 注冊方法engine.Handle("GET", "/test", func(c *gin.Context) {fmt.Println("route test")})// 啟動 http serverif err := engine.Run(); err != nil {fmt.Println(err)}
}
主要做了幾件事:
- 構造默認配置的 gin.Engine
- 注冊中間件
- 注冊方法
- 啟動 http server
gin 是如何與 net/http 鏈接起來的呢?
- 路由注冊與查找:gin 的核心結構體 Engine 即實現了該接口:
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {engine.handleHTTPRequest(c)
}
- 服務啟動:通過 Engine.Run() 啟動 http server 的背后其實是通過 http.ListenAndServe() 啟動
func (engine *Engine) Run(addr ...string) (err error) {address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return
}
至此,整個文章已經實現了閉環,更能夠學習到連接的核心思想。
參考:
[1]: https://zhuanlan.zhihu.com/p/609258171 Golang HTTP 標準庫實現原理
[2]: https://astro.yufengbiji.com/posts/golang/ Golang net/http
[3]: https://zhuanlan.zhihu.com/p/611116090 解析 Gin 框架底層原理
[4]: https://blog.csdn.net/weixin_45177370/article/details/135295839?spm=1001.2014.3001.5501 Gin 源碼深度解析及實現