gin 源碼閱讀 -1- - gin 與 net-http 的關係

gin 是目前 Go 裏面使用最廣泛的框架之一了,弄清楚 gin 框架的原理,有助於我們更好的使用 gin。這個系列 gin 源碼閱讀會逐步講明白 gin 的原理,歡迎關注後續文章。

gin 概覽

想弄清楚 gin, 需要弄明白以下幾個問題:

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:套接字聯網API)

TCP/IP 五層模型

socket 建立過程

TCP/IP五層模型下, HTTP位於應用層, 需要有傳輸層來承載HTTP協議. 傳輸層比較常見的協議是TCP,UDP, SCTP等. 由於UDP不可靠, SCTP有自己特殊的運用場景, 所以一般情況下HTTP是由TCP協議承載的 (可以使用 wireshark 抓包然後查看各層協議)

使用TCP協議的話, 就會涉及到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等框架留下了擴展的空間, 後面詳細說這個東西

服務監聽及響應

上面路由已經註冊到net/http了, 下面就該如何建立 socket 了, 以及最後又如何取到已經註冊到的路由, 將正確的響應信息從 handler 中取出來返回給客戶端

1. 創建 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.Accept 等待客戶端鏈接

// 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. 回調到實際要執行的 ServeHTTP

// net/http/server.go:L1963-1965
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
   f(w, r)
}

這基本是整個過程的代碼了.

  1. ln, err := net.Listen("tcp", addr)做了初試化了socket, bind, listen的操作.

  2. rw, e := l.Accept()進行 accept, 等待客戶端進行連接

  3. go c.serve(ctx) 啓動新的 goroutine 來處理本次請求. 同時主 goroutine 繼續等待客戶端連接, 進行高併發操作

  4. 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
}

從這段函數可以看出來, 匹配規則過於簡單, 當能匹配到路由的時候就返回其對應的 handler, 當不能匹配到時就返回/. net/http的路由匹配根本就不符合 RESTful 的規則,遇到稍微複雜一點的需求時,這個簡單的路由匹配規則簡直就是噩夢。

所以基本所有的 go 框架乾的最主要的一件事情就是重寫net/http的 route。我們直接說 gin就是一個 httprouter 也不過分, 當然gin也提供了其他比較主要的功能, 後面會一一介紹。

綜述, net/http 基本已經提供http服務的 70% 的功能, 那些號稱賊快的 go 框架, 基本上都是提供一些功能, 讓我們能夠更好的處理客戶端發來的請求. 如果你有興趣的話,也可以基於 net/http 做一個 Go 框架出來。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/935NOIg4GzRsupZefwqWeA