- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
本篇指南以開源電子書《build-web-application-with-golang》第 3.3 節為主體,逐行拆解 Go 標準庫net/http的底層運作原理,回答「如何監聽埠、如何接收客戶端請求、如何分配 handler」三個核心問題。讀完後你將能從原始碼層面理解ListenAndServe的完整呼叫鏈,掌握 goroutine 高併發模型的由來,並知道DefaultServeMux這個預設路由器是如何把 URL 路由到對應處理函式的。
在上一節(Go 建立一個 Web 伺服器)中,我們看到只用net/http套件的兩個函式,短短幾行程式碼就能啟動一個可用的 Web 服務。那麼 Go 在底層到底是怎麼做的?萬變不離其宗,Go 的 Web 服務運作同樣離不開 Web 工作方式 一節所介紹的 HTTP 基本流程——只是把「監聽、接收、處理、回應」這些環節用 Go 自己的語言實現了一遍。
前置概念:Web 伺服器端的四個角色
在深入原始碼之前,先建立四個核心概念(以下均是伺服器端的概念):
- Request:使用者請求的資訊,用來解析使用者的請求資訊,包括 post、get、cookie、url 等資訊。
- Response:伺服器需要反饋給客戶端的資訊。
- Conn:使用者的每次請求連結。
- Handler:處理請求和產生回傳資訊的處理邏輯。
這四個概念貫穿net/http的整個執行過程:伺服器從 TCP 連線(Conn)中讀取請求(Request),交給處理邏輯(Handler)生成回應(Response),最後寫回客戶端。
http 套件執行機制總覽
下圖是 Go 實現 Web 服務的工作模式流程圖,它概括了整個請求生命週期:
圖 3.9 http 套件執行流程
整個過程可以概括為三步:
- 建立 Listen Socket,監聽指定的埠,等待客戶端請求到來。
- Listen Socket 接受客戶端的請求,得到 Client Socket,接下來透過 Client Socket 與客戶端通訊。
- 處理客戶端的請求:首先從 Client Socket 讀取 HTTP 請求的協議頭,如果是 POST 方法,還可能要讀取客戶端提交的資料,然後交給相應的 handler 處理請求;handler 處理完畢準備好客戶端需要的資料,透過 Client Socket 寫給客戶端。
只要把這三步對應到下面三個問題,也就知道 Go 是如何讓 Web 執行起來了:
- 如何監聽埠?
- 如何接收客戶端請求?
- 如何分配 handler?
接下來我們逐一從原始碼中找到答案。
問題一:如何監聽埠?——ListenAndServe 的底層呼叫鏈
前面小節的程式碼中,Go 是透過一個函式http.ListenAndServe(":9090", nil)來處理這些事情的。它在底層其實做了這樣的事:初始化一個server物件,然後呼叫net.Listen("tcp", addr),也就是用 TCP 協議建立一個服務,再監聽我們設定的埠。
ListenAndServe的實現如下(以簡體中文版章節 zh/03.3.md 中的原始碼為準,更貼近現代 Go 標準庫):
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }它初始化一個Server物件後,呼叫Server物件的方法ListenAndServe,其原始碼如下:
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) }從原始碼可以看出兩個關鍵細節:
- 如果
addr為空字串,會自動使用":http",即預設的 80 埠; net.Listen("tcp", addr)是真正的底層動作——它直接基於 TCP 協議建立監聽,這正是上一節提到的「Go 不需要 nginx、apache 等外部伺服器」的原因:Go 自己就監聽了 TCP 埠。
最後,ListenAndServe呼叫srv.Serve(ln)進入主迴圈,把監聽到的 socket 交給Serve方法處理。
問題二:如何接收客戶端請求?——Serve 的主迴圈與 goroutine
監聽之後如何接收客戶端的請求呢?答案在Server.Serve方法中。下面程式碼來自 Go http 套件的原始碼(zh-tw 版節選),透過它可以看到整個接收與排程過程:
func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { 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 } log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) if err != nil { continue } go c.serve() } }這個函式裡面起了一個for{}無限迴圈,其核心動作有三個:
- 透過 Listener 接收請求:
l.Accept()阻塞等待新連線,取得連線物件rw; - 建立一個 Conn:
c, err := srv.newConn(rw),把每次請求的連線封裝為conn; - 單獨開一個 goroutine:
go c.serve(),把這個請求的資料當做參數扔給這個 conn 去服務。
這就是高併發的體現:使用者的每一次請求都是在一個新的 goroutine 中服務,相互不影響、不阻塞。每個請求都有自己獨立的 Conn 與執行上下文,這與傳統「一個執行緒服務一個連線」的阻塞模型有本質區別。
值得注意的還有原始碼中的錯誤處理邏輯(從原始碼結構可以推斷其設計意圖):當Accept遇到暫時性錯誤(ne.Temporary())時,不會立即返回,而是採用指數退避策略——從 5 毫秒開始,每次加倍,最大到 1 秒——然後重試,避免在系統資源緊張時「忙等」導致 CPU 空轉。
簡體版章節中展示了更完整的Serve原始碼(zh/03.3.md),可以看到每個連線在交給 goroutine 之前還會設定StateNew狀態,並支援透過ConnContext為每個連線注入自訂 context,這些都是現代版本net/http在基礎模型之上增加的擴充能力。
問題三:如何分配 handler?——從 readRequest 到 DefaultServeMux
那麼具體如何分配到相應的函式來處理請求呢?conn 的serve方法給出了答案。以下是其關鍵流程(原始碼見 zh/03.3.md):
func (c *conn) serve(ctx context.Context) { ... ctx, cancelCtx := context.WithCancel(ctx) c.cancelCtx = cancelCtx defer cancelCtx() ... for { w, err := c.readRequest(ctx) ... serverHandler{c.server}.ServeHTTP(w, w.req) w.cancelCtx() ... } }conn 首先會解析 request:c.readRequest(),取得請求內容之後,透過serverHandler{c.server}.ServeHTTP(w, w.req)去取得並呼叫對應的 handler。serverHandler.ServeHTTP的實現如下:
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) }關鍵在這裡:sh.srv.Handler就是我們在呼叫ListenAndServe時傳入的第二個參數。我們前面的例子傳遞的是nil(為空),於是預設取得handler = DefaultServeMux。
那麼這個DefaultServeMux是用來做什麼的呢?它就是一個路由器——用來匹配 url 並跳轉到其相應的 handle 函式。這個我們有設定過嗎?有!我們呼叫的程式碼第一句就是:
http.HandleFunc("/", sayhelloName)這行的作用就是註冊請求/的路由規則:當請求 uri 為/時,路由器就會轉到函式sayhelloName。具體來說,DefaultServeMux會呼叫ServeHTTP方法,這個方法內部其實就是呼叫sayhelloName本身,最後透過寫入 response 的資訊反饋到客戶端。
完整的示例程式碼(sayhelloName的定義與HandleFunc的註冊)可以在上一節 Go 建立一個 Web 伺服器 中看到,而倉庫中對應的完整可執行範例位於 en/code/src/apps/ch.3.2/main.go,包含r.ParseForm()解析參數、遍歷r.Form列印請求參數、fmt.Fprintf(w, "Hello astaxie!")向客戶端寫回應的完整邏輯。
一張圖看懂完整的請求處理流程
把以上三個問題串起來,詳細的整個流程如下圖所示:
圖 3.10 一個 http 連線處理流程
從圖中可以看到完整鏈路:ListenAndServe→net.Listen("tcp", addr)監聽埠 →srv.Serve(l net.Listener)進入主迴圈 →l.Accept()接收連線 →c := srv.newConn()建立連接物件 →go c.serve()開啟 goroutine → 在 goroutine 中c.readRequest()解析請求 → 取得handler = DefaultServeMux→handler.ServeHTTP(resp, req)完成路由分配並呼叫對應的處理函式。
補充:DefaultServeMux 的內部結構(為下一節做鋪墊)
如果你對「路由器到底如何儲存和匹配規則」感興趣,可以提前看一眼下一節 Go 的 http 套件詳解 中的結構定義。ServeMux的本質是一張「規則 → handler」的雜湊表:
type ServeMux struct { mu sync.RWMutex //鎖,由於請求涉及到併發處理,因此這裡需要一個鎖機制 m map[string]muxEntry // 路由規則,一個 string 對應一個 mux 實體 hosts bool // 是否在任意的規則中帶有 host 資訊 }其中muxEntry儲存了路由規則對應的 handler:
type muxEntry struct { explicit bool // 是否精確匹配 h Handler // 這個路由表示式對應哪個 handler pattern string //匹配字串 }而Handler是一個介面,只要求實現一個方法:
type Handler interface { ServeHTTP(ResponseWriter, *Request) // 路由實現器 }你可能會疑惑:上一節中的sayhelloName函式並沒有實作ServeHTTP這個介面,為什麼能註冊進路由器呢?祕密在http.HandleFunc的內部轉換(詳見 zh-tw/03.4.md):
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }net/http定義了型別HandlerFunc,它本身是一個函式型別,但透過給它掛上ServeHTTP方法,就「預設實現了 Handler 介面」。HandleFunc註冊時進行的HandlerFunc(f)強制型別轉換,讓普通函式瞬間獲得了作為 Handler 的能力。這正是 Go 語言「函式即型別、型別即行為」的典型運用,也是理解HandleFunc與Handle兩者關係的關鍵。
執行流程總整理:從註冊到回應的完整步驟
把整個net/http的執行過程按時間順序梳理如下(與 zh-tw/03.4.md 中的總結一致):
首先呼叫http.HandleFunc,按順序做了幾件事:
- 呼叫
DefaultServeMux的HandleFunc - 呼叫
DefaultServeMux的Handle - 往
DefaultServeMux的map[string]muxEntry中增加對應的 handler 和路由規則
其次呼叫http.ListenAndServe(":9090", nil),按順序做了幾件事:
- 實體化
Server - 呼叫
Server的ListenAndServe() - 呼叫
net.Listen("tcp", addr)監聽埠 - 啟動一個
for迴圈,在迴圈體中Accept請求 - 對每個請求實體化一個
Conn,並且開啟一個 goroutine 為這個請求進行服務:go c.serve() - 讀取每個請求的內容:
w, err := c.readRequest() - 判斷 handler 是否為空,如果沒有設定 handler,handler 就設定為
DefaultServeMux - 呼叫 handler 的
ServeHTTP - 進入
DefaultServeMux.ServeHTTP - 根據 request 選擇 handler,並且進入到這個 handler 的
ServeHTTP:mux.handler(r).ServeHTTP(w, r) - 選擇 handler:判斷是否有路由能滿足這個 request(迴圈遍歷 ServeMux 的
muxEntry);有則呼叫該路由 handler 的ServeHTTP,沒有則呼叫NotFoundHandler的ServeHTTP(即 404)
結語
至此,三個問題已經全部得到了解答:監聽埠靠net.Listen("tcp", addr),接收請求靠Serve主迴圈中的Accept加每請求一個 goroutine,分配 handler 靠DefaultServeMux路由器與HandleFunc註冊的規則。Go 之所以能用幾行程式碼跑起一個高併發 Web 服務,正是因為把這套成熟模型內建在了標準庫中。
如果想進一步深入,推薦繼續閱讀 Go 的 http 套件詳解,它將詳細解剖Conn的 goroutine 模型、ServeMux的匹配演算法,以及如何透過實現Handler介面來打造自訂路由器(ListenAndServe的第二個參數正是預留給外部路由器的擴充點)。
相關章節:
- 目錄
- 上一節:Go 建立一個 Web 伺服器
- 下一節:Go 的 http 套件詳解
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
深入剖析 Go 的 net/http 套件:Conn 與 ServeMux 的高併發路由原理(build-web-application-with-golang 第 3.4 節)
深入剖析 Go 的 net/http 套件:Conn 與 ServeMux 的高併發路由原理(build web application with golang
文档教程Go Web 运行机制详解:从 `net/http` 源码看懂 Go 是如何处理 Web 请求的(build-web-application-with-golang 第 3.3 节)
Go Web 运行机制详解:从 net/http 源码看懂 Go 是如何处理 Web 请求的(build web application with golang
文档教程Go Web 开发:防止表單多次提交的 Token 機制實戰(build-web-application-with-golang 第 4.4 節)
Go Web 开发:防止表單多次提交的 Token 機制實戰(build web application with golang 第 4.4 節) 本指南源自开
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考