golang構建http服務

前面有介紹beego web框架, 其實很多框架都是在 最簡單的http服務上做擴展的的,基本上都是遵循http協議,將底層的封裝好,我們使用web框架只要寫業務邏輯,填代碼就可以了,不用關心底層實現。

下面??實現一個最簡單的http服務

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

HTTP

除去細節,理解HTTP構建的網絡應用只要關注兩個端--客戶端(client)和服務端(server),兩個端的交互來自client的request,以及server端的response。所謂的http服務器,主要在于如何接受client的request,并向client返回response。

接收request的過程中,最重要的莫過于路由(router),即實現一個Multiplexer器。Go中既可以使用內置的mutilplexer--DefaultServeMux,也可以自定義。Multiplexer路由的目的就是為了找到處理器函數(hander),后者將對request進行處理,同時構建response。

簡單總結就是這個流程:

client -> Request -> Multiplexer(router)->handler ->Response -> client

因此,理解go中的http服務,最重要的就是要理解Multiplexerhander,Golang中的Multiplexer基于ServerMux結構,同時也實現了Handler接口

  • handler函數:具有func(w http.ResponseWriter, r *http.Requests)簽名的函數
  • handler處理器(函數):經過HanderFunc結構包裝的handler函數,它實現了ServeHTTP接口方法的函數。調用handler處理器的ServeHTTP方法時,即調用handler函數本身。
  • handler對象:實現了Hander接口ServeHTTP方法的結構。

handler函數和handler對象的差別在于, 一個是函數,另一個是結構體,他們都有實現了serverHTTP方法,很多情況下,他們是類似的。

Golang的http處理流程可以用下面一張圖表示,后面內容是針對圖進行說明:


Http包的三個關鍵類型

  1. Handler接口
  2. ServeMux接口
  3. HandlerFunc適配器
  4. Server

Handler

Golang沒有繼承,類多態的方法可以通過接口實現。所謂接口則是定義聲明了函數簽名,任何結構只要實現了與接口函數簽名相同的方法,就等同于實現了接口。go的http服務都是基于handler進行處理的。


// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and either closes the network connection or sends an HTTP/2
// RST_STREAM, depending on the HTTP protocol. To abort a handler so
// the client sees an interrupted response but the server doesn't log
// an error, panic with the value ErrAbortHandler.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)   //路由具體實現
}

任何結構體,只要實現了ServeHTTP方法,這個結構就可以稱之為handler對象。ServeMux會使用handler并調用其ServeHTTP方法處理請求并返回響應。
所有請求的處理器、路由ServeMux都滿足該接口。

ServeMux

了解了Handler之后,再看ServeMux。
HTTP請求的多路轉接器(路由),它負責將每一個接收到的請求的URL與一個注冊模式的列表進行匹配,并調用和URL最匹配的模式的處理器。它內部用一個map來保存所有處理器Handler。
ServeMux源碼很簡單:

type ServeMux struct {
    mu sync.RWMutex   //鎖,由于請求涉及到并發處理,因此這里需要一個鎖機制
    m  map[string]muxEntry  // 路由規則,一個string對應一個mux實體,這里的string就是注冊的路由表達式
    hosts bool // 是否在任意的規則中帶有host信息
} 
type muxEntry struct {
    h        Handler // 這個路由表達式對應哪個handler
    pattern  string  //匹配字符串
}

ServeMux結構中最重要的字段為m,這是一個map,key是一些url模式,value是一個muxEntry結構,后者里定義存儲了具體的url模式和handler。

當然,所謂的ServeMux也實現了ServeHTTP接口,也算是一個handler,不過ServeMux的ServeHTTP方法不是用來處理request和respone,而是用來找到路由注冊的handler。

  • http包有一個包級別變量DefaultServeMux,表示默認路由:var DefaultServeMux = NewServeMux(),使用包級別的http.Handle()、http.HandleFunc()方法注冊處理器時都是注冊到該路由中。
// 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
  • ServeMux結構體有ServeHTTP()方法(滿足Handler接口),主要用于間接調用它所保存的muxEntry中保存的Handler處理器的ServeHTTP()方法。

HandlerFunc適配器

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
  • 自行定義的處理函數轉換為Handler類型就是HandlerFunc調用之后的結果,這個類型默認就實現了ServeHTTP這個接口,即我們調用了HandlerFunc(f),強制類型轉換f成為HandlerFunc類型,這樣f就擁有了ServeHTTP方法。

Server

除了ServeMux和Handler,還有一個結構Server需要了解。從http.ListenAndServe的源碼可以看出,它創建了一個server對象,并調用server對象的ListenAndServe方法:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

查看server的結構如下:

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結構存儲了服務器處理請求常見的字段。其中Handler字段也保留Hander接口。如果Server接口沒有提供Handler結構對象,那么會使用DefaultServeMux做Multiplexer。

創建HTTP服務

創建一個http服務,大致需要經歷兩個過程,首先需要注冊路由,即提供url模式和handler函數的映射,其次就是實例化一個server對象,并開啟對客戶端的監聽。

http.HandleFunc("/", indexHandler)
http.ListenAndServe("127.0.0.1:8000", nil)

或者

server := &Server{Addr: addr, Handler: handler}
server.ListenAndServe()

http注冊路由

? net/http包暴露的注冊路由的api很簡單,http.HandleFunc選取了DefaultServeMux作為multiplexer:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

? 實際上,DefaultServeMux是ServeMux的一個實例。當然http包也提供了NewServeMux方法創建一個ServeMux實例,默認則創建一個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創建實例的過程中,也可以使用指針方式,即

type Server struct{}
server := Server{}

和下面的一樣都可以創建Server的實例

var DefaultServer Server
var server = &DefalutServer

因此DefaultServeMux的HandleFunc(pattern,handler)方法實際是定義在ServeMux下的:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

上述代碼中,HandlerFunc是一個函數類型。同時實現了Handler接口的ServeHTTP方法。使用HandlerFunc類型包裝一下路由定義的indexHandler函數,其目的就是為了讓這個函數也實現ServeHTTP方法,即轉變成一個handler處理器(函數)。

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

一旦這樣做了,就意味著我們的indexHandler函數也有了ServeHTTP方法。 此外,ServeMux的Handle方法,將會對pattern和handler函數做一個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函數的主要目的在于把handler和pattern模式綁定到map[string]muxEntry的map上,其中muxEntry保存了更多pattern和handler的信息。Server的m字段就是map[string]muxEntry這樣一個map。

此時,pattern和handler的路由注冊完成。

開啟監聽

注冊好路由之后,啟動web服務還需要開啟服務器監聽。http的ListenAndServer方法中可以看到創建了一個Server對象,并調用了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的ListenAndServer方法中,會初始化監聽地址Addr,同時調用Listen方法設置監聽。最后將監聽的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)
    }
}

處理請求

監聽開啟之后,一旦客戶端請求到底,go就開啟一個協程處理請求,主要邏輯都在server方法之中。

serve方法比較長,其主要職能就是,創建一個上下文對象,然后調用Listener的Accept方法用來獲取連接數據并使用newConn方法創建連接對象。最后使用goroutein協程的方式處理連接請求。因此每一個連接都開啟了一個協程,請求的上下文都不同,同時又保證了go的高并發。serve也是一個長長的方法:

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很長,里面的結構和邏輯還是很清晰的,使用defer定義了函數退出時,連接關閉相關的處理。然后就是讀取連接的網絡數據,并處理讀取完畢時候的狀態。接下來就是調用serverHandler{c.server}.ServeHTTP(w,w.req)方法處理請求了。最后就是請求處理完畢的邏輯。serverHandler是一個重要的結構,它僅有一個字段,即Server結構,同時它也實現了Hander接口方法ServeHTTP,同時它也實現了Handler接口方法ServeHTTP,并在該接口方法中做了一個重要的事情,初始化multiplexer路由多路復用器。如果server對象沒有制定Handler,則使用默認的DefaultServeMux作為路由Multiplexer。并調用初始化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方法其實也是定義在ServeMux結構中的,相關代碼如下:

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方法通過調用其Handler方法尋找注冊到路由上的handler函數,并調用該函數的ServeHTTP方法。

mux的Handler方法對URL簡單的處理,然后調用handler方法,后者會創建一個鎖,同時調用match方法返回一個handler和pattern。

在match方法中,mux的m字段是map[string]muxEntry,后者存儲了pattern和handler處理器函數,因此通過迭代m尋找出注冊路由的pattern模式與實際url匹配的handler函數并返回。

返回的結構一直傳遞到mux的ServeHTTP方法,接下來調用handler函數的ServeHTTP方法,即IndexHandler函數,然后把reponse寫到http.RequestWirter對象返回給客戶端。

上述函數運行結束即serverHandler{c.server}.ServeHTTP(w, w.req)運行結束。接下來就是對請求處理完畢之后上希望和連接斷開的相關邏輯。

至此,Golang中一個完整的http服務介紹完畢,包括注冊路由,開啟監聽,處理連接,路由處理函數。

最后總結一下:


GO Http執行流程

首先調用Http.HandleFunc
按順序做了幾件事:

  1. 調用了DefaultServeMux的HandleFunc

  2. 調用了DefaultServeMux的Handle

  3. 往DefaultServeMux的map[string]muxEntry中增加對應的handler和路由規則

其次調用http.ListenAndServe(":9090", nil) - nil使用默認路由器
按順序做了幾件事情:

  1. 實例化Server
  2. 調用Server的ListenAndServe()
  3. 調用net.Listen("tcp", addr)監聽端口
  4. 啟動一個for循環,在循環體中Accept請求
  5. 對每個請求實例化一個Conn,并且開啟一個goroutine為這個請求進行服務go c.serve()
  6. 讀取每個請求的內容w, err := c.readRequest()
  7. 判斷handler是否為空,如果沒有設置handler(這個例子就沒有設置handler),handler就設置為DefaultServeMux
  8. 調用handler的ServeHttp
  9. 在這個例子中,下面就進入到DefaultServeMux.ServeHttp
  10. 根據request選擇handler,并且進入到這個handler的ServeHTTP mux.handler(r).ServeHTTP(w, r)
    11 選擇handler:

A 判斷是否有路由能滿足這個request(循環遍歷ServeMux的muxEntry)
B 如果有路由滿足,調用這個路由handler的ServeHTTP
C 如果沒有路由滿足,調用NotFoundHandler的ServeHTTP

參考文章

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容