)
文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-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建立一個 Connc, err : srv.newConn(rw)把每次請求的連線封裝為conn單獨開一個 goroutinego 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.mdfunc (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 首先會解析 requestc.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儲存了路由規則對應的 handlertype muxEntry struct { explicit bool // 是否精確匹配 h Handler // 這個路由表示式對應哪個 handler pattern string //匹配字串 }而Handler是一個介面只要求實現一個方法type Handler interface { ServeHTTP(ResponseWriter, *Request) // 路由實現器 }你可能會疑惑上一節中的sayhelloName函式並沒有實作ServeHTTP這個介面為什麼能註冊進路由器呢祕密在http.HandleFunc的內部轉換詳見 zh-tw/03.4.mdtype 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 是否為空如果沒有設定 handlerhandler 就設定為DefaultServeMux呼叫 handler 的ServeHTTP進入DefaultServeMux.ServeHTTP根據 request 選擇 handler並且進入到這個 handler 的ServeHTTPmux.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-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-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 節 本指南源自开文档教程上一篇LinkSwift一键获取九大网盘直链的终极解决方案下一篇如何三步告别网盘限速网盘直链解析工具完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考