【Go 標準庫】詳解 net-http 包實現原理

一、前言

Go 語言自帶的 net/http 包提供了 HTTP 客戶端和服務端的實現,實現一個簡單的 http 服務非常容易,其自帶了一些列結構和方法來幫助開發者簡化 HTTP 服務開發的相關流程,因此我們不需要依賴任何第三方組件就能構建並啓動一個高併發的 HTTP 服務器。

HTTP 中客戶端與服務器建立連接發送請求與響應,這個過程可以描述爲如下圖。Server 端需要有 router 、handler 和 response;Client 端需要有 request;另外,它們之間需要 Address:Port 來建立連接。

二、源碼分析

2.1 官方案例

/ 創建一個Foo路由和處理函數
http.Handle("/foo", fooHandler)
// 創建一個bar路由和處理函數
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
// 監聽8080端口
log.Fatal(http.ListenAndServe(":8080", nil))

在上述代碼中,完成了兩件事:

如此簡潔輕便即實現了一個 http server 的啓動,其背後究竟隱藏了哪些實施細節呢. 這個問題,就讓我們在第 2 章的內容中,和大家一同展開探討.

2.2 源碼分析

2.2.1 handler 接口

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler 是一個接口,定義了方法:ServeHTTP。任何結構只要實現了這個 ServeHTTP 方法,那麼這個結構體就是一個 Handler 對象。

ServeHTTP 方法的作用是,根據 http 請求 Request 中的請求路徑 path 映射到對應的 handler 處理函數,對請求進行處理和響應。ServeHTTP 方法有兩個參數:ResponseWriter 和 * Request。

通過實現 Handler 接口並定義自己的 ServeHTTP 方法,可以創建 HTTP 處理程序。當收到一個 HTTP 請求時,服務器會調用這個處理程序的 ServeHTTP 方法,並將適當的 ResponseWriter 和 * Request 給它,以便處理請求並生成響應。

除了自己定義結構體來實現 Handler 接口,net/http 庫中提供了兩個函數可以調用,分別是 HandleFunc 和 Handle。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  DefaultServeMux.HandleFunc(pattern, handler)
}
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

如上所示,當調用 http.HandleFunc() 時,會直接調用全局變量 DefaultServeMux.HandleFunc()。HandleFunc 的第一個參數是路由表達式,也就是請求路徑,第二個參數是一個函數類型,也就是真正處理請求的函數。繼續跟蹤 DefaultServeMux.HandleFunc:

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    // ...
    mux.Handle(pattern, HandlerFunc(handler))
}

如上所示,在 ServeMux.HandleFunc 定義了接收一個 ResponseWriter 對象和一個 Request 對象的 HandlerFunc 類型。

HandlerFunc 類型是一個適配器,並且這種類型實現了 ServeHTTP 方法,即是 Handler 接口的具體實現類型,並在 ServeHTTP 方法中又調用了被轉換的函數自身,也就是說這個類型的函數其實就是一個 Handler 類型的對象,通過類型轉換可以將一個具有 func(ResponseWriter, *Request) 簽名的普通函數轉換爲一個 Handler 對象,而不需要再定義一個結構體,再讓這個結構實現 ServeHTTP 方法,非常方便的將普通函數用作 HTTP 處理程序。

通過將處理函數轉換爲 HandlerFunc 類型,並將其作爲參數傳遞給 ServeMux 的 Handle 方法,我們可以通過 mux.Handle 將處理函數註冊到 ServeMux 的路由 map 中,以便在收到請求時能夠正確匹配處理函數並處理該請求。這種設計模式使得我們可以將不同的處理函數註冊到 ServeMux,並根據請求的路徑來選擇相應的處理函數進行處理。這樣可以實現靈活的路由功能,使我們能夠根據具體的業務邏輯來處理不同的請求。

ServeMux的結構體如下:

type ServeMux struct {
  mu    sync.RWMutex
  m     map[string]muxEntry
  es    []muxEntry // slice of entries sorted from longest to shortest.
  hosts bool       // whether any patterns contain hostnames
}

ServeMux 是一個用於路由 HTTP 請求的多路複用器(multiplexer)。它可以將收到的請求根據路徑匹配與註冊的處理函數進行對應關聯,並分發給相應的處理函數進行處理,實際上是一個實現了 http.Handler 接口的結構體。內部通過一個 map 維護了從 path 到 handler 的映射關係, 有效地管理和選擇正確的處理程序來處理傳入的 HTTP 請求。當收到一個請求時,ServeMux 會根據請求的 URL 路徑進行匹配,並檢查註冊的路由模式 pattern,然後選擇適合的處理程序 handler 進行處理。

type muxEntry struct {
    h Handler
    pattern string 
}

muxEntry 是一個結構體類型,代表具體的路由條目。每個路由條目包含一個路徑模式 pattern 和關聯的處理函數 h,用於匹配和處理特定的 URL 路徑。

h 是一個 Handler 接口類型的變量,表示與該路由條目關聯的處理程序。通過該處理程序可以執行特定的邏輯來處理 HTTP 請求並生成響應。

pattern 是一個字符串類型的變量,表示該路由條目所匹配的 URL 模式或路徑模式。這個模式用於將特定的 HTTP 請求與路由條目關聯起來,以便選擇正確的處理程序進行處理。它用於確定請求的 URL 是否與該路由條目匹配,並決定選擇哪個處理程序來處理該請求。以下是一些 pattern 的匹配規則

2.2.2 監聽與服務底層原理

使用 & http.Server{} 可以創新一個服務器對象,它定義了運行一個 HTTP 服務端的參數,具體爲:

type Server struct {
    Addr           string        // 監聽的TCP地址,如果爲空字符串會使用":http"
    Handler        Handler       // 調用的處理器,如爲nil會調用http.DefaultServeMux
    ReadTimeout    time.Duration // 請求的讀取操作在超時前的最大持續時間
    WriteTimeout   time.Duration // 回覆的寫入操作在超時前的最大持續時間
    MaxHeaderBytes int           // 請求的頭域最大長度,如爲0則用DefaultMaxHeaderBytes
    TLSConfig      *tls.Config   // 可選的TLS配置,用於ListenAndServeTLS方法
    // TLSNextProto(可選地)指定一個函數來在一個NPN型協議升級出現時接管TLS連接的所有權。
    // 映射的鍵爲商談的協議名;映射的值爲函數,該函數的Handler參數應處理HTTP請求,
    // 並且初始化Handler.ServeHTTP的*Request參數的TLS和RemoteAddr字段(如果未設置)。
    // 連接在函數返回時會自動關閉。
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    // ConnState字段指定一個可選的回調函數,該函數會在一個與客戶端的連接改變狀態時被調用。
    // 參見ConnState類型和相關常數獲取細節。
    ConnState func(net.Conn, ConnState)
    // ErrorLog指定一個可選的日誌記錄器,用於記錄接收連接時的錯誤和處理器不正常的行爲。
    // 如果本字段爲nil,日誌會通過log包的標準日誌記錄器寫入os.Stderr。
    ErrorLog *log.Logger
    disableKeepAlives int32     
    // 內含隱藏或非導出字段
}

這裏可以自己定義一些參數,或者使用默認值。但是,Addr 監聽的地址和端口號需要自己添加。

創建完服務器對象後,調用 ListenAndServe() error 就可以開啓監聽,建立 TCP 連接了。

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(network, address string) (Listener, error) 初始化一個用於建立 TCP 連接的 socket 對象,並進行端口綁定。繼續往下,調用 srv.Serve(ln),函數里的 for 循環內調用 rw, err := l.Accept() 函數,它會阻塞地監聽端口直到有連接請求時,這時會調用 socket 接口建立 TCP 連接,並返回 net.Conn 對象 rw。之後調用的 srv.newConn(rw) 返回的 c 可以看作是建立 HTTP 應用層的連接返回的連接對象。

建立完連接後,開啓 go c.serve(connCtx) 處理 HTTP 請求響應的邏輯。

  for {
    rw, err := l.Accept()
    if err != nil {
      select {
      case <-srv.getDoneChan():
        return ErrServerClosed
      default:
    }
     ... 
    connCtx := ctx
    if cc := srv.ConnContext; cc != nil {
      connCtx = cc(connCtx, rw)
      if connCtx == nil {
        panic("ConnContext returned nil")
      }
    }
    tempDelay = 0
    c := srv.newConn(rw)
    c.setState(c.rwc, StateNew, runHooks) // before Serve can return
    go c.serve(connCtx)
  }

Server.Serve 方法體現了典型的服務端的運行架構,使用 For + Listener.Accept 的模式來接受並處理客戶端請求。這種模式是一種常見的服務器設計模式,通常用於監聽網絡連接請求的處理。

在這個方法中,通過循環調用 listener.Accept() 方法來接受客戶端連接。每當有新的連接建立時,將 server 封裝成一組 valueContext,並開啓一個新的 goroutine 調用 conn.serve 來處理該連接並傳入 server 封裝後的 valueContext,這確保了可以同時處理多個同時到達的連接請求。下面省略了部分代碼:

func (c *conn) serve(ctx context.Context) {
  c.remoteAddr = c.rwc.RemoteAddr().String()
  ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
  ctx, cancelCtx := context.WithCancel(ctx)
  c.cancelCtx = cancelCtx
  defer cancelCtx()
  c.r = &connReader{conn: c}
  c.bufr = newBufioReader(c.r)
  c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
  for {
    w, _ := c.readRequest(ctx)
    serverHandler{c.server}.ServeHTTP(w, w.req)
    w.finishRequest()
    if !w.shouldReuseConnection() {
      if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
        c.closeWriteAndWait()
      }
      return
    }
    if !w.conn.server.doKeepAlives() {
      return
    }
    if d := c.server.idleTimeout(); d != 0 {
      c.rwc.SetReadDeadline(time.Now().Add(d))
      if _, err := c.bufr.Peek(4); err != nil {
        return
      }
    }
    w.cancelCtx()
    ...
  }
}

*conn.serve 是響應客戶端連接的核心方法,用於處理 HTTP 連接客戶端發送的請求,實現了 HTTP 連接的處理邏輯。它通過不斷循環讀取和處理客戶端請求,根據請求調用相應的處理函數,並最終取消請求的上下文。

首先,該方法初始化了讀取器 c.bufr 與帶有緩衝的寫入器 c.bufw。這些緩衝區可以提高性能,減少對底層網絡的讀寫次數。

接着,通過一個 for 循環來處理客戶端請求。

傳入的 ctx 爲 valueContext,其 key 爲 ServerContextKey 即 “http-server”,其 value 爲 server 結構

在每次迭代中,調用 c.readRequest(ctx) 方法來讀取客戶端的請求,並返回一個 response 類型的寫入器 w 和一個錯誤 err。

通過 serverHandler{c.server}.ServeHTTP(w, w.req) 調用服務器的處理器函數 ServeHTTP 來處理請求。該處理器函數負責根據請求路徑和其他特定規則來處理請求並生成相應的響應,會去匹配在 http.HandleFunc() 中註冊的路由, 找到對應的處理函數。

它會調用 w.finishRequest() 來關閉響應報文的寫通道等操作,但是不會並不會關閉連接。

這裏會有邏輯判斷:

如果關閉了連接複用以及長連接的話,在響應完成後,會 Return,然後執行 defer 關閉 TCP 連接;如果沒有關閉,則會 c.server.idleTimeout(); 在 IdleTimeout 內沒有請求的話,會釋放連接。

隨後,調用 w.cancelCtx() 方法取消當前請求的上下文。

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