Gin 源碼閱讀之 Gin 與 Net/Http 的關系
本文轉載自微信公眾號「HHFCodeRv」,作者haohongfan。轉載本文請聯(lián)系HHFCodeRv公眾號。
gin 是目前 Go 里面使用最廣泛的框架之一了,弄清楚 gin 框架的原理,有助于我們更好的使用 gin。這個系列 gin 源碼閱讀會逐步講明白 gin 的原理,歡迎關注后續(xù)文章。
gin 概覽
想弄清楚 gin, 需要弄明白以下幾個問題:
- request數據是如何流轉的
- gin框架到底扮演了什么角色
- 請求從gin流入net/http, 最后又是如何回到gin中
- gin的context為何能承擔起來復雜的需求
- gin的路由算法
- gin的中間件是什么
- gin的Engine具體是個什么東西
- net/http的requeset, response都提供了哪些有用的東西
從gin的官方第一個demo入手.
- package main
- import "github.com/gin-gonic/gin"
- func main() {
- r := gin.Default()
- r.GET("/ping", func(c *gin.Context) {
- c.JSON(200, gin.H{
- "message": "pong",
- })
- })
- r.Run() // listen and serve on 0.0.0.0:8080
- }
r.Run() 的源碼:
- func (engine *Engine) Run(addr ...string) (err error) {
- defer func() { debugPrintError(err) }()
- address := resolveAddress(addr)
- debugPrint("Listening and serving HTTP on %s\n", address)
- err = http.ListenAndServe(address, engine)
- return
- }
看到開始調用的是 http.ListenAndServe(address, engine), 這個函數是net/http的函數, 然后請求數據就在net/http開始流轉.
Request 數據是如何流轉的
先不使用gin, 直接使用net/http來處理http請求
- func main() {
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("Hello World"))
- })
- if err := http.ListenAndServe(":8000", nil); err != nil {
- fmt.Println("start http server fail:", err)
- }
- }
在瀏覽器中輸入localhost:8000, 會看到Hello World. 下面利用這個簡單demo看下request的流轉流程.
HTTP是如何建立起來的
簡單的說一下http請求是如何建立起來的(需要有基本的網絡基礎, 可以找相關的書籍查看, 推薦看UNIX網絡編程卷1:套接字聯(lián)網API)
TCP/IP 五層模型
socket建立過程
在TCP/IP五層模型下, HTTP位于應用層, 需要有傳輸層來承載HTTP協(xié)議. 傳輸層比較常見的協(xié)議是TCP,UDP, SCTP等. 由于UDP不可靠, SCTP有自己特殊的運用場景, 所以一般情況下HTTP是由TCP協(xié)議承載的(可以使用wireshark抓包然后查看各層協(xié)議)
使用TCP協(xié)議的話, 就會涉及到TCP是如何建立起來的. 面試中能夠常遇到的名詞三次握手, 四次揮手就是在這里產生的. 具體的建立流程就不在陳述了, 大概流程就是圖中左半邊
所以說, 要想能夠對客戶端http請求進行回應的話, 就首先需要建立起來TCP連接, 也就是socket. 下面要看下net/http是如何建立起來socket?
net/http 是如何建立 socket 的
從圖上可以看出, 不管server代碼如何封裝, 都離不開bind,listen,accept這些函數. 就從上面這個簡單的demo入手查看源碼.
- func main() {
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("Hello World"))
- })
- if err := http.ListenAndServe(":8000", nil); err != nil {
- fmt.Println("start http server fail:", err)
- }
- }
注冊路由
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("Hello World"))
- })
這段代碼是在注冊一個路由及這個路由的handler到DefaultServeMux中
- // server.go:L2366-2388
- func (mux *ServeMux) Handle(pattern string, handler Handler) {
- mux.mu.Lock()
- defer mux.mu.Unlock()
- if pattern == "" {
- panic("http: invalid pattern")
- }
- if handler == nil {
- panic("http: nil handler")
- }
- if _, exist := mux.m[pattern]; exist {
- panic("http: multiple registrations for " + pattern)
- }
- if mux.m == nil {
- mux.m = make(map[string]muxEntry)
- }
- mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
- if pattern[0] != '/' {
- mux.hosts = true
- }
- }
可以看到這個路由注冊太過簡單了, 也就給gin, iris, echo等框架留下了擴展的空間, 后面詳細說這個東西
服務監(jiān)聽及響應
上面路由已經注冊到net/http了, 下面就該如何建立socket了, 以及最后又如何取到已經注冊到的路由, 將正確的響應信息從handler中取出來返回給客戶端
1.創(chuàng)建 socket
- if err := http.ListenAndServe(":8000", nil); err != nil {
- fmt.Println("start http server fail:", err)
- }
- // net/http/server.go:L3002-3005
- func ListenAndServe(addr string, handler Handler) error {
- server := &Server{Addr: addr, Handler: handler}
- return server.ListenAndServe()
- }
- // net/http/server.go:L2752-2765
- func (srv *Server) ListenAndServe() error {
- // ... 省略代碼
- ln, err := net.Listen("tcp", addr) // <-----看這里listen
- if err != nil {
- return err
- }
- return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
- }
2.Aaccept 等待客戶端鏈接
- // net/http/server.go:L2805-2853
- func (srv *Server) Serve(l net.Listener) error {
- // ... 省略代碼
- for {
- rw, e := l.Accept() // <----- 看這里accept
- if e != nil {
- select {
- case <-srv.getDoneChan():
- return ErrServerClosed
- default:
- }
- if ne, ok := e.(net.Error); ok && ne.Temporary() {
- if tempDelay == 0 {
- tempDelay = 5 * time.Millisecond
- } else {
- tempDelay *= 2
- }
- if max := 1 * time.Second; tempDelay > max {
- tempDelay = max
- }
- srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
- time.Sleep(tempDelay)
- continue
- }
- return e
- }
- tempDelay = 0
- c := srv.newConn(rw)
- c.setState(c.rwc, StateNew) // before Serve can return
- go c.serve(ctx) // <--- 看這里
- }
- }
3. 提供回調接口 ServeHTTP
- // net/http/server.go:L1739-1878
- func (c *conn) serve(ctx context.Context) {
- // ... 省略代碼
- serverHandler{c.server}.ServeHTTP(w, w.req)
- w.cancelCtx()
- if c.hijacked() {
- return
- }
- w.finishRequest()
- // ... 省略代碼
- }
- // net/http/server.go:L2733-2742
- 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)
- }
- // net/http/server.go:L2352-2362
- 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
- }
- h, _ := mux.Handler(r) // <--- 看這里
- h.ServeHTTP(w, r)
- }
4. 回調到實際要執(zhí)行的 ServeHTTP
- // net/http/server.go:L1963-1965
- func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
- f(w, r)
- }
這基本是整個過程的代碼了.
- ln, err := net.Listen("tcp", addr)做了初試化了socket, bind, listen的操作.
- rw, e := l.Accept()進行accept, 等待客戶端進行連接
- go c.serve(ctx) 啟動新的goroutine來處理本次請求. 同時主goroutine繼續(xù)等待客戶端連接, 進行高并發(fā)操作
- h, _ := mux.Handler(r) 獲取注冊的路由, 然后拿到這個路由的handler, 然后將處理結果返回給客戶端
從這里也能夠看出來, net/http基本上提供了全套的服務.
為什么會出現很多go框架
- // net/http/server.go:L2218-2238
- func (mux *ServeMux) match(path string) (h Handler, pattern string) {
- // Check for exact match first.
- v, ok := mux.m[path]
- if ok {
- return v.h, v.pattern
- }
- // Check for longest valid match.
- var n = 0
- for k, v := range mux.m {
- if !pathMatch(k, path) {
- continue
- }
- if h == nil || len(k) > n {
- n = len(k)
- h = v.h
- pattern = v.pattern
- }
- }
- return
- }
從這段函數可以看出來, 匹配規(guī)則過于簡單, 當能匹配到路由的時候就返回其對應的handler, 當不能匹配到時就返回/. net/http的路由匹配根本就不符合 RESTful 的規(guī)則,遇到稍微復雜一點的需求時,這個簡單的路由匹配規(guī)則簡直就是噩夢。
所以基本所有的go框架干的最主要的一件事情就是重寫net/http的route。我們直接說 gin就是一個 httprouter 也不過分, 當然gin也提供了其他比較主要的功能, 后面會一一介紹。
綜述, net/http基本已經提供http服務的70%的功能, 那些號稱賊快的go框架, 基本上都是提供一些功能, 讓我們能夠更好的處理客戶端發(fā)來的請求. 如果你有興趣的話,也可以基于 net/http 做一個 Go 框架出來。