一文詳解Go Http Server原理
從一個(gè) Demo 入手
俗話說(shuō)萬(wàn)事開(kāi)頭難,但用 Go 實(shí)現(xiàn)一個(gè) Http Server 真不難,簡(jiǎn)單到什么程度?起一個(gè) Server,并且能響應(yīng)請(qǐng)求,算上包名、導(dǎo)入的依賴,甚至空行,也就只要 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")
}
這么簡(jiǎn)單,能與之一戰(zhàn)的恐怕只有 Python 了吧,而且 Go 還能編譯成可執(zhí)行的二進(jìn)制文件,你說(shuō)牛啤不牛???
Http Server 如何處理連接?
我們從這一行代碼看起
http.ListenAndServe(":81", nil)
從命名來(lái)看,這個(gè)方法干了兩件事,監(jiān)聽(tīng)并且服務(wù),從方法的單一職責(zé)上來(lái)說(shuō),我覺(jué)得不ok,一個(gè)方法怎么能干兩件事?但這是大佬寫的代碼,就很合理。
第一個(gè)參數(shù)Addr是要監(jiān)聽(tīng)的地址和端口,第二個(gè)參數(shù)Handler一般是nil,它是真正的邏輯處理,但我們通常用第一行代碼那樣來(lái)注冊(cè)處理器,這代碼一看就感覺(jué)是把 path 映射到業(yè)務(wù)邏輯上,我們先大概了解,待會(huì)再來(lái)看它
http.HandleFunc("/hello", hello)
如果了解過(guò)一點(diǎn)網(wǎng)絡(luò)編程基礎(chǔ),就會(huì)知道操作系統(tǒng)提供了bind、listen、accept這樣的系統(tǒng)調(diào)用,我們只要按順序發(fā)起調(diào)用,就能組合出一個(gè) Server。
Go 也是利用這些系統(tǒng)調(diào)用,把他們都封裝在了ListenAndServe中。

Listen 往下追究就是系統(tǒng)調(diào)用,所以我們重點(diǎn)看 Serve:

把分支代碼收起來(lái),只看主干,發(fā)現(xiàn)是一個(gè) for 循環(huán)里面在不停地 Accept,而這個(gè) Accept 在沒(méi)有連接時(shí)是阻塞的,當(dāng)有連接時(shí),起一個(gè)新的協(xié)程來(lái)處理。
Http Server 如何處理請(qǐng)求的?
一些前置工作
處理請(qǐng)求的一行代碼是,可以看出是每個(gè)連接單開(kāi)了一個(gè)協(xié)程處理:
go c.serve(connCtx)
這里的 connCtx 代入了當(dāng)前的 Server 對(duì)象:
ctx := context.WithValue(baseCtx, ServerContextKey, srv) ... connCtx := ctx
而且還提供了修改它的 hook 方法 srv.ConnContext,可以在每次 Accept 時(shí)修改原始的 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
但是如果按照我開(kāi)頭給的代碼,你是沒(méi)法修改 srv.ConnContext 的,可以改成這樣來(lái)自定義:
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,可采取如上的方法設(shè)置,在每次連接狀態(tài)改變時(shí)執(zhí)行 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 方法到底干了什么,我們?cè)俸?jiǎn)化一下:
func (c *conn) serve(ctx context.Context) {
...
for {
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
}
serve 也是一個(gè)大循環(huán),循環(huán)里面主要是讀取一個(gè)請(qǐng)求,然后將請(qǐng)求交給 Handler 處理。
為什么是一個(gè)大循環(huán)呢?因?yàn)槊總€(gè) serve 處理的是一個(gè)連接,一個(gè)連接可以有多次請(qǐng)求。
讀請(qǐng)求就顯得比較枯燥乏味,按照Http協(xié)議,讀出URL,header,body等信息。
這里有個(gè)細(xì)節(jié)是在每次讀取了一個(gè)請(qǐng)求后,還開(kāi)了一個(gè)協(xié)程去讀下一個(gè)請(qǐng)求,也算是做了優(yōu)化吧。
for {
w, err := c.readRequest(ctx)
...
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
...
}
請(qǐng)求如何路由?
當(dāng)讀取到一個(gè)請(qǐng)求后,便進(jìn)入這一行代碼:
serverHandler{c.server}.ServeHTTP(w, w.req)
ServeHTTP 找到我們注冊(cè)的 Handler 去處理,如果請(qǐng)求的URI 是 *或請(qǐng)求 Method 是 OPTIONS,則使用globalOptionsHandler,也就是說(shuō)這類請(qǐng)求不需要我們手動(dòng)處理,直接就返回了。
對(duì)于我們注冊(cè)的 Handler 也需要去尋找路由,這個(gè)路由的規(guī)則還是比較簡(jiǎn)單,主要由如下三條:
- 如果注冊(cè)了帶 host 的路由,則按 host + path 去尋找,如果沒(méi)注冊(cè)帶 host 的路由,則按 path 尋找
- 路由規(guī)則匹配以完全匹配優(yōu)先,如果注冊(cè)的路由規(guī)則最后一個(gè)字符是
/,則除了完全匹配外,還會(huì)以前綴查找
舉幾個(gè)例子來(lái)理解一下:
- 帶 host 的匹配規(guī)則
注冊(cè)路由為
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello", hello2)
此時(shí)如果執(zhí)行
curl 'http://127.0.0.1:81/hello'
則會(huì)匹配到 hello2,但如果執(zhí)行
curl 'http://localhost:81/hello'
就匹配的是 hello
- 前綴匹配
如果注冊(cè)路由為
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello/", hello2)
注意第二個(gè)最后還有個(gè)/,此時(shí)如果執(zhí)行
curl 'http://127.0.0.1:81/hello/roshi'
也能匹配到 hello2,怎么樣,是不是理解了?
找到路由之后就直接調(diào)用我們開(kāi)頭注冊(cè)的方法,如果我們往 Response 中寫入數(shù)據(jù),就能返回給客戶端,這樣一個(gè)請(qǐng)求就處理完成了。
總結(jié)
最后我們回憶下 Go Http Server 的要點(diǎn):
- 用 Go 起一個(gè) Http Server 非常簡(jiǎn)單
- Go Http Server 本質(zhì)是一個(gè)大循環(huán),每當(dāng)有一個(gè)新連接時(shí),會(huì)起一個(gè)新的協(xié)程來(lái)處理
- 每個(gè)連接的處理也是一個(gè)大循環(huán),這個(gè)循環(huán)里做了讀取請(qǐng)求、尋找路由、執(zhí)行邏輯三件大事

以上就是一文詳解Go Http Server原理的詳細(xì)內(nèi)容,更多關(guān)于Go Http Server原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言使用MongoDB數(shù)據(jù)庫(kù)詳細(xì)步驟
mongodb是一種高性能、開(kāi)源、文檔型的nosql數(shù)據(jù)庫(kù),被廣泛應(yīng)用于web應(yīng)用、大數(shù)據(jù)以及云計(jì)算領(lǐng)域,下面這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言使用MongoDB數(shù)據(jù)庫(kù)的詳細(xì)步驟,需要的朋友可以參考下2024-05-05
使用Go Validator有效驗(yàn)證數(shù)據(jù)示例分析
作為一名開(kāi)發(fā)者,確保Go應(yīng)用中處理的數(shù)據(jù)是有效和準(zhǔn)確的非常重要,Go Validator是一個(gè)開(kāi)源的數(shù)據(jù)驗(yàn)證庫(kù),為Go結(jié)構(gòu)體提供強(qiáng)大且易于使用的數(shù)據(jù)驗(yàn)證功能,本篇文章將介紹Go Validator庫(kù)的主要特點(diǎn)以及如何在Go應(yīng)用中使用它來(lái)有效驗(yàn)證數(shù)據(jù)2023-12-12
GO語(yǔ)言實(shí)現(xiàn)列出目錄和遍歷目錄的方法
這篇文章主要介紹了GO語(yǔ)言實(shí)現(xiàn)列出目錄和遍歷目錄的方法,涉及ioutil.ReadDir()與filepath.Walk()的應(yīng)用,是非常實(shí)用的技巧,需要的朋友可以參考下2014-12-12
Golang使用反射的動(dòng)態(tài)方法調(diào)用詳解
Go是一種靜態(tài)類型的語(yǔ)言,提供了大量的安全性和性能。這篇文章主要和大家介紹一下Golang使用反射的動(dòng)態(tài)方法調(diào)用,感興趣的小伙伴可以了解一下2023-03-03
golang連接池檢查連接失敗時(shí)如何重試(示例代碼)
在Go中,可以通過(guò)使用database/sql包的DB類型的Ping方法來(lái)檢查數(shù)據(jù)庫(kù)連接的可用性,本文通過(guò)示例代碼,演示了如何在連接檢查失敗時(shí)進(jìn)行重試,感興趣的朋友一起看看吧2023-10-10

