網站首頁 編程語言 正文
從一個 Demo 入手
俗話說萬事開頭難,但用 Go 實現一個 Http Server 真不難,簡單到什么程度?起一個 Server,并且能響應請求,算上包名、導入的依賴,甚至空行,也就只要 15 行代碼:
package main import ( "io" "net/http" ) func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":81", nil) } func hello(response http.ResponseWriter, request *http.Request) { io.WriteString(response, "hello world") }
這么簡單,能與之一戰的恐怕只有 Python 了吧,而且 Go 還能編譯成可執行的二進制文件,你說牛啤不牛啤?
Http Server 如何處理連接?
我們從這一行代碼看起
http.ListenAndServe(":81", nil)
從命名來看,這個方法干了兩件事,監聽并且服務,從方法的單一職責上來說,我覺得不ok,一個方法怎么能干兩件事?但這是大佬寫的代碼,就很合理。
第一個參數Addr
是要監聽的地址和端口,第二個參數Handler
一般是nil
,它是真正的邏輯處理,但我們通常用第一行代碼那樣來注冊處理器,這代碼一看就感覺是把 path 映射到業務邏輯上,我們先大概了解,待會再來看它
http.HandleFunc("/hello", hello)
如果了解過一點網絡編程基礎,就會知道操作系統提供了bind
、listen
、accept
這樣的系統調用,我們只要按順序發起調用,就能組合出一個 Server。
Go 也是利用這些系統調用,把他們都封裝在了ListenAndServe
中。
Listen
往下追究就是系統調用,所以我們重點看 Serve
:
把分支代碼收起來,只看主干,發現是一個 for 循環里面在不停地 Accept,而這個 Accept 在沒有連接時是阻塞的,當有連接時,起一個新的協程來處理。
Http Server 如何處理請求的?
一些前置工作
處理請求的一行代碼是,可以看出是每個連接單開了一個協程處理:
go c.serve(connCtx)
這里的 connCtx 代入了當前的 Server 對象:
ctx := context.WithValue(baseCtx, ServerContextKey, srv) ... connCtx := ctx
而且還提供了修改它的 hook 方法 srv.ConnContext
,可以在每次 Accept 時修改原始的 context
if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } }
它的定義是:
// ConnContext optionally specifies a function that modifies // the context used for a new connection c. The provided ctx // is derived from the base context and has a ServerContextKey // value. ConnContext func(ctx context.Context, c net.Conn) context.Context
但是如果按照我開頭給的代碼,你是沒法修改 srv.ConnContext
的,可以改成這樣來自定義:
func main() { http.HandleFunc("/hello", hello) server := http.Server{ Addr: ":81", ConnContext: func(ctx context.Context, c net.Conn) context.Context { return context.WithValue(ctx, "hello", "roshi") }, } server.ListenAndServe() }
同樣的 c.setState
也提供了 hook,可采取如上的方法設置,在每次連接狀態改變時執行 hook 方法:
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
// ConnState specifies an optional callback function that is // called when a client connection changes state. See the // ConnState type and associated constants for details. ConnState func(net.Conn, ConnState)
serve 方法到底干了什么
為了能看清楚 Accept 后,serve 方法到底干了什么,我們再簡化一下:
func (c *conn) serve(ctx context.Context) { ... for { w, err := c.readRequest(ctx) ... serverHandler{c.server}.ServeHTTP(w, w.req) ... } }
serve 也是一個大循環,循環里面主要是讀取一個請求,然后將請求交給 Handler 處理。
為什么是一個大循環呢?因為每個 serve 處理的是一個連接,一個連接可以有多次請求。
讀請求就顯得比較枯燥乏味,按照Http協議,讀出URL,header,body等信息。
這里有個細節是在每次讀取了一個請求后,還開了一個協程去讀下一個請求,也算是做了優化吧。
for { w, err := c.readRequest(ctx) ... if requestBodyRemains(req.Body) { registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) } else { w.conn.r.startBackgroundRead() } ... }
請求如何路由?
當讀取到一個請求后,便進入這一行代碼:
serverHandler{c.server}.ServeHTTP(w, w.req)
ServeHTTP 找到我們注冊的 Handler 去處理,如果請求的URI 是 *
或請求 Method 是 OPTIONS
,則使用globalOptionsHandler,也就是說這類請求不需要我們手動處理,直接就返回了。
對于我們注冊的 Handler 也需要去尋找路由,這個路由的規則還是比較簡單,主要由如下三條:
- 如果注冊了帶 host 的路由,則按 host + path 去尋找,如果沒注冊帶 host 的路由,則按 path 尋找
- 路由規則匹配以完全匹配優先,如果注冊的路由規則最后一個字符是
/
,則除了完全匹配外,還會以前綴查找
舉幾個例子來理解一下:
- 帶 host 的匹配規則
注冊路由為
http.HandleFunc("/hello", hello) http.HandleFunc("127.0.0.1/hello", hello2)
此時如果執行
curl 'http://127.0.0.1:81/hello'
則會匹配到 hello2,但如果執行
curl 'http://localhost:81/hello'
就匹配的是 hello
- 前綴匹配
如果注冊路由為
http.HandleFunc("/hello", hello) http.HandleFunc("127.0.0.1/hello/", hello2)
注意第二個最后還有個/
,此時如果執行
curl 'http://127.0.0.1:81/hello/roshi'
也能匹配到 hello2,怎么樣,是不是理解了?
找到路由之后就直接調用我們開頭注冊的方法,如果我們往 Response 中寫入數據,就能返回給客戶端,這樣一個請求就處理完成了。
總結
最后我們回憶下 Go Http Server 的要點:
- 用 Go 起一個 Http Server 非常簡單
- Go Http Server 本質是一個大循環,每當有一個新連接時,會起一個新的協程來處理
- 每個連接的處理也是一個大循環,這個循環里做了讀取請求、尋找路由、執行邏輯三件大事
原文鏈接:https://juejin.cn/post/7187718981933858877
相關推薦
- 2022-10-22 react實現Modal彈窗效果_React
- 2022-04-06 關于.Net?6?添加NLog的方法_實用技巧
- 2022-11-17 python標準庫random模塊處理隨機數_python
- 2022-05-17 IDEA使用Tomcat
- 2022-10-31 Golang中map數據類型的使用方法_Golang
- 2022-06-23 Python在畫圖時使用特殊符號的方法總結_python
- 2022-09-09 使用?React?Hooks?重構類組件的示例詳解_React
- 2023-02-25 一文搞懂Python中is和==的區別_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支