Golang Http 學習(一) Http Server 的實現
摘要:// 原子包裹 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 的構造
- 首先是對協議頭的解析,獲取請求的方法、請求Url,協議等,如果是代理模式,還會做Url的替換。
- 然後會解析Header,在Server 中,Golang 的Header 數據是存儲在 map[string][]string 結構中,Key 採用大駝峯和連字符描述。
- 對於Pragma:no-cache 的請求,標識 Cache-control:No-cache
- 對於Connection: close 的請求,不再keeplive
- 構造 Request 傳輸控制的數據:
- Transfer-Encoding 的修正
- Content-Length 的修正
- chunk 模式下的Trailer修正
- Body 的構造
- 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 的學習
總結
- 一個 Http 請求,至少會啓動兩個goroutine。一個groutine用來處理請求,另一個goroutine 用來異步讀取body 數據。
- http 的method 的合法性校驗是
幾個比較特殊的 Http 協議規則
- Http Except: 100-continue 協議
- Http CONNECT METHOD, 不僅會用在代理模式的Http Server中,還有可能用在RPC中。
- Chunk 模式, Trailer 設置