深刻學習用 Go 編寫 HTTP 服務器


Go是一門通用的編程語言,想要學習 Go 語言的 Web 開發,就必須知道如何用 Go 啓動一個 HTTP 服務器用於接收和響應來自客戶端的 HTTP 請求。用 Go實現一個http server很是容易,Go 語言標準庫net/http自帶了一系列結構和方法來幫助開發者簡化 HTTP 服務開發的相關流程。所以,咱們不須要依賴任何第三方組件就能構建並啓動一個高併發的 HTTP 服務器。這篇文章會學習如何用net/http本身編寫實現一個HTTP Serve並探究其實現原理,以此來學習瞭解網絡編程的常見範式以及設計思路。git

HTTP 服務處理流程

基於HTTP構建的服務標準模型包括兩個端,客戶端(Client)和服務端(Server)。HTTP 請求從客戶端發出,服務端接受到請求後進行處理而後將響應返回給客戶端。因此http服務器的工做就在於如何接受來自客戶端的請求,並向客戶端返回響應。github

典型的 HTTP 服務的處理流程以下圖所示:web

HTTP處理流程

服務器在接收到請求時,首先會進入路由(router),也成爲服務複用器(Multiplexe),路由的工做在於請求找到對應的處理器(handler),處理器對接收到的請求進行相應處理後構建響應並返回給客戶端。Go實現的http server一樣遵循這樣的處理流程。編程

咱們先看看Go如何實現一個簡單的返回 "Hello World"http server瀏覽器

package main

import (
   "fmt"
   "net/http"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello World")
}

func main () {
   http.HandleFunc("/", HelloHandler)
   http.ListenAndServe(":8000", nil)
}

運行代碼以後,在瀏覽器中打開localhost:8000就能夠看到Hello World。這段代碼先利用http.HandleFunc在根路由/上註冊了一個HelloHandler, 而後利用http.ListenAndServe啓動服務器並監聽本地的 8000 端口。當有請求過來時,則根據路由執行對應的handler函數。服務器

咱們再看一下另一種常見的實現方式:網絡

package main

import (
   "fmt"
   "net/http"
)

type HelloHandlerStruct struct {
   content string
}

func (handler *HelloHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, handler.content)
}

func main()  {
   http.Handle("/", &HelloHandlerStruct{content: "Hello World"})
   http.ListenAndServe(":8000", nil)
}

這段代碼再也不使用 http.HandleFunc 函數,取而代之的是直接調用 http.Handle 並傳入咱們自定義的 http.Handler 接口的實例。併發

Go實現的http服務步驟很是簡單,首先註冊路由,而後建立服務並開啓監聽便可。下文咱們將從註冊路由、開啓服務、處理請求,以及關閉服務這幾個步驟瞭解Go如何實現http服務。app

路由註冊

http.HandleFunchttp.Handle都是用於給路由規則指定處理器,http.HandleFunc的第一個參數爲路由的匹配規則(pattern)第二個參數是一個簽名爲func(w http.ResponseWriter, r *http.Requests)的函數。而http.Handle的第二個參數爲實現了http.Handler接口的類型的實例。框架

http.HandleFunchttp.Handle的源碼以下:

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

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler)
}

能夠看到這兩個函數最終都由DefaultServeMux調用Handle方法來完成路由處理器的註冊。
這裏咱們遇到兩種類型的對象:ServeMuxHandler

Handler

http.Handlernet/http中定義的接口用來表示 HTTP 請求:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler接口中聲明瞭名爲ServeHTTP的函數簽名,也就是說任何結構只要實現了這個ServeHTTP方法,那麼這個結構體就是一個Handler對象。其實go的http服務都是基於Handler進行處理,而Handler對象的ServeHTTP方法會讀取Request進行邏輯處理而後向ResponseWriter中寫入響應的頭部信息和響應內容。

回到上面的HandleFunc函數,它調用了*ServeMux.HandleFunc將處理器註冊到指定路由規則上:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

注意一下這行代碼:

mux.Handle(pattern, HandlerFunc(handler))

這裏HandlerFunc其實是將handler函數作了一個類型轉換,將函數轉換爲了http.HandlerFunc類型(注意:註冊路由時調用的是 http.HandleFunc,這裏類型是http.HandlerFunc)。看一下HandlerFunc的定義:

type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFunc類型表示的是一個具備func(ResponseWriter, *Request)簽名的函數類型,而且這種類型實現了ServeHTTP方法(在其實現的ServeHTTP方法中又調用了被轉換的函數自身)。也就是說這個類型的函數其實就是一個Handler類型的對象。利用這種類型轉換,咱們能夠將將具備func(ResponseWriter, *Request)簽名的普通函數轉換爲一個Handler對象,而不須要定義一個結構體,再讓這個結構實現ServeHTTP方法。

ServeMux(服務複用器)

上面的代碼中能夠看到不管是使用http.HandleFunc仍是http.Handle註冊路由的處理函數時最後都會用到ServerMux結構的Handle方法去註冊路由處理函數。

咱們先來看一下ServeMux的定義:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {
    h       Handler
    pattern string
}

ServeMux中的字段m,是一個mapkey是路由表達式,value是一個muxEntry結構,muxEntry結構體存儲了路由表達式和對應的handler。字段m對應的 map用於路由的精確匹配而es字段的slice會用於路由的部分匹配,這個到了路由匹配部分再細講。

ServeMux也實現了ServeHTTP方法:

func (mux *ServeMux) ServeHTTP(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)
}

也就是說ServeMux結構體也是Handler對象,只不過ServeMuxServeHTTP方法不是用來處理具體的request和構建response,而是用來經過路由查找對應的路由處理器Handler對象,再去調用路由處理器的ServeHTTP 方法去處理request和構建reponse

註冊路由

搞明白HandlerServeMux以後,咱們再回到以前的代碼:

DefaultServeMux.Handle(pattern, handler)

這裏的DefaultServeMux表示一個默認的ServeMux實例,在上面的例子中咱們沒有建立自定義的ServeMux,因此會自動使用DefaultServeMux

而後再看一下ServeMuxHandle方法是怎麼註冊路由的處理函數的:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    if pattern == "" {
        panic("http: invalid pattern")
    }
    if handler == nil {
        panic("http: nil handler")
    }
  // 路由已經註冊過處理器函數,直接panic
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
  // 用路由的pattern和處理函數建立 muxEntry 對象
    e := muxEntry{h: handler, pattern: pattern}
  // 向ServeMux的m 字段增長新的路由匹配規則
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
  // 若是路由patterm以'/'結尾,則將對應的muxEntry對象加入到[]muxEntry中,路由長的位於切片的前面
        mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
        mux.hosts = true
    }
}

Handle方法註冊路由時主要作了兩件事情:一個就是向ServeMuxmap[string]muxEntry增長給定的路由匹配規則;而後若是路由表達式以'/'結尾,則將對應的muxEntry對象加入到[]muxEntry中,按照路由表達式長度倒序排列。前者用於路由精確匹配,後者用於部分匹配,具體怎麼匹配的後面再看。

自定義 ServeMux

經過http.NewServeMux()能夠建立一個ServeMux實例取代默認的DefaultServeMux

咱們把上面輸出Hello Worldhttp server再次改造一下,使用自定義的 ServeMux實例做爲ListenAndServe()方法的第二個參數,而且增長一個/welcome路由(下面的代碼主要是展現用Handle HandleFunc 註冊路由,實際使用的時候沒必要這麼麻煩,選一種就好):

package main

import (
    "fmt"
    "net/http"
)

type WelcomeHandlerStruct struct {

}

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func (*WelcomeHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome")
}

func main () {
    mux := http.NewServeMux()
    mux.HandleFunc("/", HelloHandler)
    mux.Handle("/welcome", &WelcomeHandlerStruct{})
    http.ListenAndServe(":8080", mux)
}

以前提到ServeMux也實現了ServeHTTP方法,所以mux也是一個Handler對象。對於ListenAndServe()方法,若是第二個參數是自定義ServeMux實例,那麼Server實例接收到的ServeMux服務複用器對象將再也不是DefaultServeMux而是mux

啓動服務

路由註冊完成後,使用http.ListenAndServe方法就能啓動服務器開始監聽指定端口過來的請求。

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return 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(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

這先建立了一個Server對象,傳入了地址和handler參數(這裏的handler參數時 ServeMux 實例),而後調用Server對象ListenAndServe()方法。

Server(服務器對象)

先看一下Server這個結構體的定義,字段比較多,能夠先大體瞭解一下:

type Server struct {
    Addr    string  // TCP address to listen on, ":http" if empty
    Handler Handler // handler to invoke, http.DefaultServeMux if nil
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger

    disableKeepAlives int32     // accessed atomically.
    inShutdown        int32     
    nextProtoOnce     sync.Once 
    nextProtoErr      error     

    mu         sync.Mutex
    listeners  map[*net.Listener]struct{}
    activeConn map[*conn]struct{}// 活躍鏈接
    doneChan   chan struct{}
    onShutdown []func()
}

ServerListenAndServe方法中,會初始化監聽地址Addr,同時調用Listen方法設置監聽。最後將監聽的TCP對象傳入其Serve方法。Server 對象的 Serve 方法會接收 Listener 中過來的鏈接,爲每一個鏈接建立一個goroutine,在goroutine 中會用路由處理 Handler 對請求進行處理並構建響應。

func (srv *Server) Serve(l net.Listener) error {
......
   baseCtx := context.Background() // base is always background, per Issue 16220 
   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
   for {
      rw, e := l.Accept()// 接收 listener 過來的網絡鏈接請求
      ......
      c := srv.newConn(rw)
      c.setState(c.rwc, StateNew) // 將鏈接放在 Server.activeConn這個 map 中
      go c.serve(ctx)// 建立協程處理請求
   }
}

這裏隱去了一些細節,以便了解Serve方法的主要邏輯。首先建立一個上下文對象,而後調用ListenerAccept()接收監聽到的網絡鏈接;一旦有新的鏈接創建,則調用ServernewConn()建立新的鏈接對象,並將鏈接的狀態標誌爲StateNew,而後開啓一個goroutine處理鏈接請求。

處理鏈接

在開啓的 goroutineconnserve()會進行路由匹配找到路由處理函數而後調用處理函數。這個方法很長,咱們保留關鍵邏輯。

func (c *conn) serve(ctx context.Context) {

    ...

    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)
        c.curReq.Store((*response)(nil))

        ...
    }
}

當一個鏈接創建以後,該鏈接中全部的請求都將在這個協程中進行處理,直到鏈接被關閉。在serve()方法中會循環調用readRequest()方法讀取下一個請求進行處理,其中最關鍵的邏輯是下面行代碼:

serverHandler{c.server}.ServeHTTP(w, w.req)

serverHandler是一個結構體類型,它會代理Server對象:

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)
}

serverHandler實現的ServeHTTP()方法裏的sh.srv.Handler就是咱們最初在http.ListenAndServe()中傳入的Handler參數,也就是咱們自定義的ServeMux對象。若是該Handler對象爲nil,則會使用默認的DefaultServeMux。最後調用ServeMuxServeHTTP()方法匹配當前路由對應的handler方法。

func (mux *ServeMux) ServeHTTP(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 u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
            return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
        }

        return mux.handler(r.Host, r.URL.Path)
    }

    // All other requests have any port stripped and path cleaned
    // before passing to mux.handler.
    host := stripHostPort(r.Host)
    path := cleanPath(r.URL.Path)

    // If the given path is /tree and its handler is not registered,
    // redirect for /tree/.
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
        return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    if path != r.URL.Path {
        _, pattern = mux.handler(host, path)
        url := *r.URL
        url.Path = path
        return RedirectHandler(url.String(), StatusMovedPermanently), pattern
    }

    return mux.handler(host, r.URL.Path)
}

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
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
}

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

match方法裏咱們看到以前提到的mux的m字段(類型爲map[string]muxEntry)和es(類型爲[]muxEntry)。這個方法裏首先會利用進行精確匹配,在map[string]muxEntry中查找是否有對應的路由規則存在;若是沒有匹配的路由規則,則會利用es進行近似匹配。

以前提到在註冊路由時會把以'/'結尾的路由(可稱爲節點路由)加入到es字段的[]muxEntry中。對於相似/path1/path2/path3這樣的路由,若是不能找到精確匹配的路由規則,那麼則會去匹配和當前路由最接近的已註冊的父節點路由,因此若是路由/path1/path2/已註冊,那麼該路由會被匹配,不然繼續匹配下一個父節點路由,直到根路由/

因爲[]muxEntry中的muxEntry按照路由表達式從長到短排序,因此進行近似匹配時匹配到的節點路由必定是已註冊父節點路由中最相近的。

查找到路由實際的處理器Handler對象返回給調用者ServerMux.ServeHTTP方法後,最後在方法裏就會調用處理器HandlerServeHTTP方法處理請求、構建寫入響應:

h.ServeHTTP(w, r)

實際上若是根據路由查找不處處理器Handler那麼也會返回NotFoundHandler:

func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }

func NotFoundHandler() Handler { return HandlerFunc(NotFound) }

這樣標準統一,在調用 h.ServeHTTP(w, r)後則會想響應中寫入 404 的錯誤信息。

中止服務

咱們寫的http server已經能監聽網絡鏈接、把請求路由處處理器函數處理請求並返回響應了,可是還須要能優雅的關停服務,在生產環境中,當須要更新服務端程序時須要重啓服務,但此時可能有一部分請求進行到一半,若是強行中斷這些請求可能會致使意外的結果。

從 Go 1.8 版本開始,net/http原生支持使用http.ShutDown來優雅的關停HTTP 服務。這種方案一樣要求用戶建立自定義的 http.Server 對象,由於Shutdown方法沒法經過其它途徑調用。

咱們來看下面的代碼,這段代碼經過結合捕捉系統信號(Signal)、goroutine 和管道(Channel)來實現服務器的優雅中止:

package main

import (
   "context"
   "fmt"
   "log"
   "net/http"
   "os"
   "os/signal"
   "syscall"
)

func main() {
   mux := http.NewServeMux()
   mux.Handle("/", &helloHandler{})

   server := &http.Server{
      Addr:    ":8081",
      Handler: mux,
   }

   // 建立系統信號接收器
   done := make(chan os.Signal)
   signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
   go func() {
      <-done

      if err := server.Shutdown(context.Background()); err != nil {
         log.Fatal("Shutdown server:", err)
      }
   }()

   log.Println("Starting HTTP server...")
   err := server.ListenAndServe()
   if err != nil {
      if err == http.ErrServerClosed {
         log.Print("Server closed under request")
      } else {
         log.Fatal("Server closed unexpected")
      }
   }
}

type helloHandler struct{}

func (*helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello World")
}

這段代碼經過捕捉 os.Interrupt 信號(Ctrl+C)和syscall,SIGTERM信號(kill 進程時傳遞給進程的信號)而後調用 server.Shutdown 方法告知服務器應中止接受新的請求並在處理完當前已接受的請求後關閉服務器。爲了與普通錯誤相區別,標準庫提供了一個特定的錯誤類型 http.ErrServerClosed,咱們能夠在代碼中經過判斷是否爲該錯誤類型來肯定服務器是正常關閉的仍是意外關閉的。

用Go 編寫http server的流程就大體學習完了,固然要寫出一個高性能的服務器還有不少要學習的地方,net/http標準庫裏還有不少結構和方法來完善http server,學會這些最基本的方法後再看其餘Web 框架的代碼時就清晰不少。甚至熟練了以爲框架用着太複雜也能本身封裝一個HTTP 服務的腳手架(我用echo 和 gin 以爲還挺簡單的,跟PHP 的Laravel框架比起來他們也就算個腳手架吧,沒黑 PHP,關注個人用 Laravel 的小夥伴可別取關【哈哈哈...嗝】)。

參考文章:

https://juejin.im/post/5dd11b...

https://github.com/unknwon/bu...

https://medium.com/honestbee-...

相關文章
相關標籤/搜索