摘要:// 原子包裹 response 對象 c.curReq.Store(w) // 異步讀取Body (此處也有對 except 100 的處理) // 傳入 request 和 response 處理 handler serverHandler{c.server}.ServeHTTP(w, w.req) // 連接複用判斷, 複用則退出 // 請求結束,寫header 和resp,並關閉req w.finishRequest() if。// Serve a new connection. func (c *conn) serve(ctx context.Context) { // ... defer 處理異常退出 和連接關閉 // ... tls 握手 // 初始化conn 的讀寫 // 對於keeplive 循環處理請求 for { // 讀取/處理請求頭 w, err := c.readRequest(ctx) // ... 連接狀態變更 && 異常處理 // 對 Except 100-continue 的特殊處理。

Golang Http Server 實現的學習

Http 服務是基於 Tcp 的應用層的實現,也是我們常見的網絡協議之一。go 語言提供了較爲豐富的http協議的實現包 net/http 包。http 是典型的C/S 架構(也是B/S架構),我們先從Server端入手,看看Http Server 是如何實現的。

請求連接的管理

golang 中, 連接的管理採用的是 Reactor 模式。每個請求到達服務器之後,都會分配一個 goroutine 做任務處理。

 func (srv *Server) Serve(l net.Listener) error {
  // ... 初始化和驗證listener
  // ... 構造 context
  for {
    rw, e := l.Accept()
    if e != nil {
      select {
      case <-srv.getDoneChan():
        return ErrServerClosed
      default:
      }
      // ... 若爲臨時錯誤,啓動重試機制
      // 否則退出
    }
    tempDelay = 0
    c := srv.newConn(rw)
    c.setState(c.rwc, StateNew)

    // 創建goroutine, 單獨處理連接
    go c.serve(ctx)
  }
}

我們在處理 http 請求時,不同請求在不同goroutine中,需要注意併發請求數據共享的問題。

連接的狀態

Server 在Accept 後創建連接(conn),連接可能有多種狀態。通過連接的狀態轉移,可以方便我們瞭解一個conn 的處理流程。下面是狀態的轉移圖:

當Accept後,構建了新的連接,狀態將標記爲New。如果可以讀取數據,連接將標記爲Active(即,活動的Conn)。作爲一個活動的Conn,可能在處理完畢後變爲Idle狀態用於請求複用;也有可能因爲請求協議故障,變爲Close狀態;也有可能被服務調用方直接管理Conn,狀態變更爲Hijacked 狀態。

Hijacked 狀態下,Conn 被使用方自行管理,一般用於協議升級的情況。例如:通過http 請求後,協議升級爲websocket 請求,或者Rpc 請求等。

連接的處理

做http 的連接處理,重點有幾個方面:① 通過連接讀取數據,並做協議分析和處理;②對http請求做處理(我們正常需要做的業務處理);③ 連接的複用和升級。

首先,我們看看整體的處理流程:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
  
  // ... defer 處理異常退出 和連接關閉
  // ... tls 握手
  // 初始化conn 的讀寫

  // 對於keeplive 循環處理請求
  for {
    // 讀取/處理請求頭
    w, err := c.readRequest(ctx)
    // ... 連接狀態變更 && 異常處理
    // 對  Except 100-continue 的特殊處理。

    // 原子包裹 response 對象
    c.curReq.Store(w)
    // 異步讀取Body (此處也有對 except 100 的處理)

    // 傳入 request 和 response 處理 handler
    serverHandler{c.server}.ServeHTTP(w, w.req)
    // 連接複用判斷, 複用則退出

    // 請求結束,寫header 和resp,並關閉req
    w.finishRequest()
    if !w.shouldReuseConnection() {
      if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
        c.closeWriteAndWait()
      }
      return
    }
    // 更改狀態,釋放 response

    // 如果不需要keeplive, 連接將被關閉
    if !w.conn.server.doKeepAlives() {
      return
    }
    // 判斷是否超時,連接是否可用, 若不可用則關閉

    // 重新設置超時
    c.rwc.SetReadDeadline(time.Time{})
  }
}

從代碼中可以看出,除了需要做Http 的解析外,還需要不斷判斷Conn 的狀態。當進入Hijack狀態後,不再控制Conn;當連接異常後,不再處理請求;當keeplive後,需要複用連接;超時之後,對連接的關閉等。此外,還需要對http 協議做適配處理,例如 對 Except: 100-continue的支持等。

對於每個請求,我們都會有一個 Request 和 Response 對象,分別標識一個請求和響應。從Request 中讀取請求Body,將我們的響應寫入Response對象中。下面我們來看看Server端是如何構造這兩個對象的。

Request 的構造

  1. 首先是對協議頭的解析,獲取請求的方法、請求Url,協議等,如果是代理模式,還會做Url的替換。
  2. 然後會解析Header,在Server 中,Golang 的Header 數據是存儲在 map[string][]string 結構中,Key 採用大駝峯和連字符描述。
    • 對於Pragma:no-cache 的請求,標識 Cache-control:No-cache
    • 對於Connection: close 的請求,不再keeplive
  3. 構造 Request 傳輸控制的數據:
    • Transfer-Encoding 的修正
    • Content-Length 的修正
    • chunk 模式下的Trailer修正
    • Body 的構造
  4. PRI header 對Http2的支持。(需要通過HiJack 支持)

Response 的構造

Response 作爲服務的響應節點,比較簡單,初始化:

w = &response{
  conn:          c,
  cancelCtx:     cancelCtx,
  req:           req,
  reqBody:       req.Body,
  handlerHeader: make(Header),
  contentLength: -1,
  closeNotifyCh: make(chan bool, 1),

  // We populate these ahead of time so we're not
  // reading from req.Header after their Handler starts
  // and maybe mutates it (Issue 14940)
  wants10KeepAlive: req.wantsHttp10KeepAlive(),
  wantsClose:       req.wantsClose(),
}
w.cw.res = w

//創建 一個寫的緩衝區 (這裏還用到了 sync.Pool 做對象存儲
w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)

Handler 的學習

總結

  1. 一個 Http 請求,至少會啓動兩個goroutine。一個groutine用來處理請求,另一個goroutine 用來異步讀取body 數據。
  2. http 的method 的合法性校驗是

幾個比較特殊的 Http 協議規則

  1. Http Except: 100-continue 協議
  2. Http CONNECT METHOD, 不僅會用在代理模式的Http Server中,還有可能用在RPC中。
  3. Chunk 模式, Trailer 設置
相關文章