原文:Golang構(gòu)建HTTP服務(wù)(一)--- net/http庫源碼筆記 - 簡書 (jianshu.com)
實(shí)現(xiàn)一個(gè)最簡單HTTP server
需要多少代碼?只需要一行,Python2的python -m SimpleHTTPServer
厦瓢,ruby的ruby -run -e httpd . -p 8888
寇甸。對于Golang芽卿,實(shí)現(xiàn)一個(gè)最簡單的http server也用不著幾行肋僧,卻能帶來更具殺傷力的性能羞秤。
一個(gè)Go最簡單的http服務(wù)器:
package main
import (
"fmt"
"net/http"
)
func IndexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", IndexHandler)
http.ListenAndServe("127.0.0.0:8000", nil)
}
上述代碼究竟有什么魔法呢来候?下面將會解密go的net/http
實(shí)現(xiàn)http服務(wù)的原理跷叉。學(xué)習(xí)net/http源碼不僅可以學(xué)習(xí)網(wǎng)絡(luò)編程中常見范式,也是學(xué)習(xí)Golang接口設(shè)計(jì)哲學(xué)营搅。
HTTP
網(wǎng)絡(luò)發(fā)展云挟,很多網(wǎng)絡(luò)應(yīng)用都是構(gòu)建再 HTTP 服務(wù)基礎(chǔ)之上。HTTP 協(xié)議從誕生到現(xiàn)在植锉,發(fā)展從1.0峭拘,1.1到2.0也不斷再進(jìn)步。除去細(xì)節(jié)鸡挠,理解 HTTP 構(gòu)建的網(wǎng)絡(luò)應(yīng)用只要關(guān)注兩個(gè)端---客戶端(clinet)和服務(wù)端(server),兩個(gè)端的交互來自 clinet 的 request拣展,以及server端的response。所謂的http服務(wù)器姓惑,主要在于如何接受 clinet 的 request,并向client返回response按脚。
接收request的過程中于毙,最重要的莫過于路由(router
),即實(shí)現(xiàn)一個(gè)Multiplexer
器辅搬。Go中既可以使用內(nèi)置的mutilplexer --- DefautServeMux
唯沮,也可以自定義。Multiplexer路由的目的就是為了找到處理器函數(shù)(handler
),后者將對request進(jìn)行處理介蛉,同時(shí)構(gòu)建response萌庆。
簡單總結(jié)就是這個(gè)流程為:
Clinet -> Requests -> [Multiplexer(router) -> handler -> Response -> Clinet
因此,理解go中的http服務(wù)币旧,最重要就是要理解Multiplexer和handler践险,Golang中的Multiplexer基于ServeMux
結(jié)構(gòu),同時(shí)也實(shí)現(xiàn)了Handler
接口佳恬。
對于handler的其實(shí)沒有合適的中文詞語捏境,只可意會,不可言傳的感覺毁葱。為了更好的說明問題垫言,本文約定了如下規(guī)則:
- hander函數(shù): 具有
func(w http.ResponseWriter, r *http.Requests)
簽名的函數(shù) - handler處理器(函數(shù)): 經(jīng)過
HandlerFunc
結(jié)構(gòu)包裝的handler函數(shù)
,它實(shí)現(xiàn)了ServeHTTP接口方法的函數(shù)倾剿。調(diào)用handler處理器的ServeHTTP方法時(shí)筷频,即調(diào)用handler函數(shù)本身。 - handler對象:實(shí)現(xiàn)了Handler接口ServeHTTP方法的結(jié)構(gòu)前痘。
handler處理器和handler對象的差別在于凛捏,一個(gè)是函數(shù),另外一個(gè)是結(jié)構(gòu)芹缔,它們都有實(shí)現(xiàn)了ServeHTTP方法坯癣。很多情況下它們的功能類似,下文就使用統(tǒng)稱為handler最欠。這算是Golang通過接口實(shí)現(xiàn)的類動態(tài)類型吧示罗。
Golang的http處理流程可以用下面一張圖表示,后面內(nèi)容是針對圖進(jìn)行說明:
Handler
Golang沒有繼承芝硬,類多態(tài)的方式可以通過接口實(shí)現(xiàn)蚜点。所謂接口則是定義聲明了函數(shù)簽名绍绘,任何結(jié)構(gòu)只要實(shí)現(xiàn)了與接口函數(shù)簽名相同的方法陪拘,就等同于實(shí)現(xiàn)了接口左刽。go的http服務(wù)都是基于handler進(jìn)行處理。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
任何結(jié)構(gòu)體馍佑,只要實(shí)現(xiàn)了ServeHTTP方法拭荤,這個(gè)結(jié)構(gòu)就可以稱之為handler對象舅世。ServeMux會使用handler并調(diào)用其ServeHTTP方法處理請求并返回響應(yīng)。
ServeMux
了解了Handler之后缨硝,再看ServeMux查辩。ServeMux的源碼很簡單:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool
}
type muxEntry struct {
explicit bool
h Handler
pattern string
}
ServeMux結(jié)構(gòu)中最重要的字段為m
宜岛,這是一個(gè)map萍倡,key是一些url模式列敲,value是一個(gè)muxEntry結(jié)構(gòu)酿炸,后者里定義存儲了具體的url模式和handler填硕。
當(dāng)然扁眯,所謂的ServeMux也實(shí)現(xiàn)了ServeHTTP接口翅帜,也算是一個(gè)handler涝滴,不過ServeMux的ServeHTTP方法不是用來處理request和respone,而是用來找到路由注冊的handler诈唬,后面再做解釋缩麸。
Server
除了ServeMux和Handler阅仔,還有一個(gè)結(jié)構(gòu)Server需要了解弧械。從http.ListenAndServe
的源碼可以看出梦谜,它創(chuàng)建了一個(gè)server對象闭树,并調(diào)用server對象的ListenAndServe方法:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
查看server的結(jié)構(gòu)如下:
type Server struct {
Addr string
Handler Handler
ReadTimeout time.Duration
WriteTimeout time.Duration
TLSConfig *tls.Config
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
disableKeepAlives int32 nextProtoOnce sync.Once
nextProtoErr error
}
server結(jié)構(gòu)存儲了服務(wù)器處理請求常見的字段报辱。其中Handler字段也保留Handler接口碍现。如果Server接口沒有提供Handler結(jié)構(gòu)對象昼接,那么會使用DefautServeMux做multiplexer慢睡,后面再做分析漂辐。
創(chuàng)建HTTP服務(wù)
創(chuàng)建一個(gè)http服務(wù)棕硫,大致需要經(jīng)歷兩個(gè)過程哈扮,首先需要注冊路由蚓再,即提供url模式和handler函數(shù)的映射,其次就是實(shí)例化一個(gè)server對象,并開啟對客戶端的監(jiān)聽按声。
再看gohttp服務(wù)的代碼
http.HandleFunc("/", indexHandler)
即是注冊路由恬吕。
http.ListenAndServe("127.0.0.1:8000", nil)
或者:
server := &Server{Addr: addr, Handler: handler}
server.ListenAndServe()
注冊路由
閱讀框架源碼是學(xué)習(xí)的好方式渐裂,通常閱讀也有兩個(gè)方法柒凉,一是不求甚解膝捞,框架的主要流程要清晰蔬咬,別的細(xì)枝末節(jié)林艘,如果尚不能理解作者的用意狐援,可以先忽略,不必馬上深究蚊俺;其次批钠,庖丁解牛,對于作者想要表達(dá)的主要流程指郁,一定要明確闲坎,執(zhí)行的邏輯和結(jié)構(gòu)腰懂。兩者看起來略矛盾绣溜,其實(shí)不然怖喻。大體而言就是對主流程要清晰锚沸,主流程以外的細(xì)節(jié)需要先忽略咒吐。最簡單實(shí)踐方式就是,看不懂的就先放一邊绽昼。直到所有的都看不懂,再回去看以前不懂的部分明肮,搞懂為止循未。下面就查看http是如何注冊路由的妖。
net/http包暴露的注冊路由的api很簡單娇未,http.HandleFunc選取了DefaultServeMux作為multiplexer:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
那么什么是DefaultServeMux呢?實(shí)際上星虹,DefaultServeMux是ServeMux的一個(gè)實(shí)例零抬。當(dāng)然http包也提供了NewServeMux
方法創(chuàng)建一個(gè)ServeMux實(shí)例,默認(rèn)則創(chuàng)建一個(gè)DefaultServeMux:
// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
注意宽涌,go創(chuàng)建實(shí)例的過程中媚值,也可以使用指針方式,即
type Server struct{}
server := Server{}
和下面的一樣都可以創(chuàng)建Server的實(shí)例
var DefalutServer Server
var server = &DefalutServer
因此DefaultServeMux的HandleFunc(pattern, handler)方法實(shí)際是定義在ServeMux下的:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
上述代碼中护糖,HandlerFunc是一個(gè)函數(shù)類型。同時(shí)實(shí)現(xiàn)了Handler接口的ServeHTTP方法嚼松。使用HandlerFunc類型包裝一下路由定義的indexHandler函數(shù)嫡良,其目的就是為了讓這個(gè)函數(shù)也實(shí)現(xiàn)ServeHTTP方法,即轉(zhuǎn)變成一個(gè)handler處理器(函數(shù))。
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
一旦這樣做了,就意味著我們的 indexHandler 函數(shù)也有了ServeHTTP方法。
此外,ServeMux的Handle方法,將會對pattern和handler函數(shù)做一個(gè)map映射:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern " + pattern)
}
if handler == nil {
panic("http: nil handler")
}
if mux.m[pattern].explicit {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
path := pattern
if pattern[0] != '/' {
path = pattern[strings.Index(pattern, "/"):]
}
url := &url.URL{Path: path}
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
}
}
由此可見搔耕,Handle函數(shù)的主要目的在于把handler和pattern模式綁定到map[string]muxEntry的map上惭墓,其中muxEntry保存了更多pattern和handler的信息褐缠,還記得前面討論的Server結(jié)構(gòu)嗎胡桨?Server的m字段就是map[string]muxEntry這樣一個(gè)map呢诬。
此時(shí)钓猬,pattern和handler的路由注冊完成澳迫。接下來就是如何開始server的監(jiān)聽,以接收客戶端的請求卒稳。
#### 開啟監(jiān)聽
注冊好路由之后辈灼,啟動web服務(wù)還需要開啟服務(wù)器監(jiān)聽。http的ListenAndServer方法中可以看到創(chuàng)建了一個(gè)Server對象吧史,并調(diào)用了Server對象的同名方法:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
func (srv Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(net.TCPListener)})
}
Server的ListenAndServe方法中,會初始化監(jiān)聽地址Addr阎毅,同時(shí)調(diào)用Listen方法設(shè)置監(jiān)聽捡絮。最后將監(jiān)聽的TCP對象傳入Serve方法:
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
baseCtx := context.Background()
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
for {
rw, e := l.Accept()
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
#### 處理請求
監(jiān)聽開啟之后区岗,一旦客戶端請求到底,go就開啟一個(gè)協(xié)程處理請求谴古,主要邏輯都在serve方法之中带饱。
serve方法比較長恢口,其主要職能就是婚被,創(chuàng)建一個(gè)上下文對象址芯,然后調(diào)用Listener的Accept方法用來 獲取連接數(shù)據(jù)并使用newConn方法創(chuàng)建連接對象。最后使用goroutein協(xié)程的方式處理連接請求匿醒。因?yàn)槊恳粋€(gè)連接都開起了一個(gè)協(xié)程举瑰,請求的上下文都不同廓握,同時(shí)又保證了go的高并發(fā)。serve也是一個(gè)長長的方法:
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
defer func() {
if err := recover(); err != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}()
...
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
}
...
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle)
}
}
盡管serve很長,里面的結(jié)構(gòu)和邏輯還是很清晰的隙券,使用defer定義了函數(shù)退出時(shí)男应,連接關(guān)閉相關(guān)的處理。然后就是讀取連接的網(wǎng)絡(luò)數(shù)據(jù)娱仔,并處理讀取完畢時(shí)候的狀態(tài)沐飘。接下來就是調(diào)用`serverHandler{c.server}.ServeHTTP(w, w.req)`方法處理請求了。最后就是請求處理完畢的邏輯牲迫。serverHandler是一個(gè)重要的結(jié)構(gòu)耐朴,它近有一個(gè)字段,即Server結(jié)構(gòu)盹憎,同時(shí)它也實(shí)現(xiàn)了Handler接口方法ServeHTTP筛峭,并在該接口方法中做了一個(gè)重要的事情,初始化multiplexer路由多路復(fù)用器脚乡。如果server對象沒有指定Handler蜒滩,則使用默認(rèn)的DefaultServeMux作為路由Multiplexer。并調(diào)用初始化Handler的ServeHTTP方法奶稠。
type serverHandler struct {
srv *Server
}
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)
}
這里DefaultServeMux的ServeHTTP方法其實(shí)也是定義在ServeMux結(jié)構(gòu)中的俯艰,相關(guān)代碼如下:
func (mux *ServeMux) (w ResponseWriter, r Request) {
if r.RequestURI == "" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
url := *r.URL
url.Path = p
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
var n = 0
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}
mux的ServeHTTP方法通過調(diào)用其Handler方法尋找注冊到路由上的handler函數(shù),并調(diào)用該函數(shù)的ServeHTTP方法锌订,本例則是IndexHandler函數(shù)竹握。
mux的Handler方法對URL簡單的處理,然后調(diào)用handler方法辆飘,后者會創(chuàng)建一個(gè)鎖啦辐,同時(shí)調(diào)用match方法返回一個(gè)handler和pattern。
在match方法中蜈项,mux的m字段是map[string]muxEntry圖芹关,后者存儲了pattern和handler處理器函數(shù),因此通過迭代m尋找出注冊路由的patten模式與實(shí)際url匹配的handler函數(shù)并返回紧卒。
返回的結(jié)構(gòu)一直傳遞到mux的ServeHTTP方法侥衬,接下來調(diào)用handler函數(shù)的ServeHTTP方法,即IndexHandler函數(shù)跑芳,然后把response寫到http.RequestWirter對象返回給客戶端轴总。
上述函數(shù)運(yùn)行結(jié)束即`serverHandler{c.server}.ServeHTTP(w, w.req)`運(yùn)行結(jié)束。接下來就是對請求處理完畢之后上希望和連接斷開的相關(guān)邏輯博个。
至此怀樟,Golang中一個(gè)完整的http服務(wù)介紹完畢,包括注冊路由盆佣,開啟監(jiān)聽往堡,處理連接械荷,路由處理函數(shù)。
### 總結(jié)
多數(shù)的web應(yīng)用基于HTTP協(xié)議虑灰,客戶端和服務(wù)器通過request-response的方式交互养葵。一個(gè)server并不可少的兩部分莫過于路由注冊和連接處理。Golang通過一個(gè)ServeMux實(shí)現(xiàn)了的multiplexer路由多路復(fù)用器來管理路由瘩缆。同時(shí)提供一個(gè)Handler接口提供ServeHTTP用來實(shí)現(xiàn)handler處理其函數(shù)关拒,后者可以處理實(shí)際request并構(gòu)造response。
ServeMux和handler處理器函數(shù)的連接橋梁就是Handler接口庸娱。ServeMux的ServeHTTP方法實(shí)現(xiàn)了尋找注冊路由的handler的函數(shù)着绊,并調(diào)用該handler的ServeHTTP方法。ServeHTTP方法就是真正處理請求和構(gòu)造響應(yīng)的地方熟尉。
回顧go的http包實(shí)現(xiàn)http服務(wù)的流程归露,可見大師們的編碼設(shè)計(jì)之功力。學(xué)習(xí)有利提高自身的代碼邏輯組織能力斤儿。更好的學(xué)習(xí)方式除了閱讀剧包,就是實(shí)踐,接下來往果,我們將著重討論來構(gòu)建http服務(wù)疆液。尤其是構(gòu)建http中間件函數(shù)。
作者:人世間
鏈接:http://www.reibang.com/p/be3d9cdc680b
來源:簡書
著作權(quán)歸作者所有陕贮。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)堕油,非商業(yè)轉(zhuǎn)載請注明出處。