Go 實現獨立的 Web 服務器(一)

Go 實現獨立的 Web 服務器

一. Web 服務器

說起 web 服務器,相信大家都比較熟悉,比如 Nginx、Apache、Tomcat 等,通過代理或者反向代理方式爲用戶提供服務。如果使用這些組件,則需要部署 Web 服務器、項目代碼等,而且相關配置等一堆,還是比較麻煩的,而且很多功能需要基於網關開發或者在項目代碼中支持開發等。那麼如果脫離這些 Web 服務器,我們是否可以實現一個 Web 服務器,完全自我可控?

二. Go 實現 Web 服務

Golang 本身提供了一個比較完善的 Http 服務的內置包,在業務開發中,只需要在此包基礎上就可以實現一個功能豐富、強大的 web 服務器。

2.1 Golang 標準庫:net/http

net/http 庫實現了整套的 http 服務中的客戶端、服務端接口,可以基於此輕鬆的發起 HTTP 請求或者對外提供 HTTP 服務。本期主要介紹基於此包實現對外提供 HTTP 服務。

server 基本介紹

server 服務的基本信息

type Server struct {
    Addr string // 定義服務監聽的地址端口,如果爲空,則默認監聽80端口
    Handler Handler // 請求被處理的業務方,默認 http.DefaultServeMux
    TLSConfig *tls.Config // 可選的TLS配置,對外提供https服務
    ReadTimeout time.Duration // 讀取客戶端請求的超時時間,包含讀取請求體
    ReadHeaderTimeout time.Duration // 讀取請求頭的超時時間,如果爲空,則使用 ReadTimeout, 如果兩者都沒有,則沒有超時時間
    WriteTimeout time.Duration // 服務響應的超時時間
    IdleTimeout time.Duration // 長鏈接空閒的超時時間
    MaxHeaderBytes int // 客戶端請求頭的最大大小,默認爲1MB
    ConnState func(net.Conn, ConnState) // 指定可選的回調方法,當客戶端連接狀態發生改變時
    ErrorLog *log.Logger // 連接錯誤、handlers異常或者文件系統異常時使用,默認使用標準庫的logger接口
    onShutdown []func() // 服務停止時觸發的方法調用
}

基於以上 server 結構,Golang 標準庫提供瞭如下幾個服務接口

func (srv *Server) Close() error // 立即關閉所有的活躍監聽以及所有的連接,包括新建的連接、活躍的或者空閒的連接
func (srv *Server) ListenAndServe() error // 啓動服務監聽tcp連接以及將請求轉發到handler中
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error // 支持https服務
func (srv *Server) Shutdown(ctx context.Context) error // 實現優雅關閉連接

2.2 啓動首個 web 服務示例

2.2.1 Web 服務示例

最簡單的 Server:

package main
    
import (
    "fmt"
    "net/http"
)
    
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello world\n")
}
    
func main() {
    http.HandleFunc("/", helloWorldHandler)
    srv := http.Server{
        Addr: ":9090",
    }
    er := srv.ListenAndServe()
    if er != nil {
        panic("Start Server Failed With " + er.Error())
    }
}

啓動服務:

go run http_be.go

請求

curl -i -X 'GET' 'http://127.0.0.1:9090/'
HTTP/1.1 200 OK
Date: Sun, 25 Jul 2021 05:43:52 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello world

僅僅 20 行代碼就實現了一個 web 服務器,那麼這中間都發生了什麼事情呢?

2.2.2 起源

世界萬物的起源都來自於一點,如同單細胞生物進化到現如今五彩斑斕的世界。Web 服務的起源也來自於一點,即

srv := http.Server{
    Addr: ":9090",
}

在此處初始化 server 服務,除顯示指定監聽端口外,還有一個重要的默認 handler 參數,即 http.DefaultServeMux ,提供 web 服務的路由解析功能。即

 http.HandleFunc("/", helloWorldHandler)

以上代碼其實等同於如下:

hdler := http.DefaultServeMux
hdler.HandleFunc("/", helloWorldHandler)
srv := http.Server{
    Addr: ":9090",
    Handler: hdler,
}

http 的 mux 維護了一個路由解析的 map 表,服務在啓動時,所有的請求路由會被解析到這個 map 表中,其結構體爲:

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
}

其中 muxEntry 中存儲着路由的路徑以及對應的處理方法 handler。基於此 Mux 接口,還可以實現更加複雜的路由協議。

2.2.3 請求的處理
er := srv.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(ln)
}

func (srv *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
        fn(srv, l) // call hook with unwrapped listener
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    if !srv.trackListener(&l, true) {
        return ErrServerClosed
    }
    defer srv.trackListener(&l, false)

    baseCtx := context.Background()
    if srv.BaseContext != nil {
        baseCtx = srv.BaseContext(origListener)
        if baseCtx == nil {
            panic("BaseContext returned a nil context")
        }
    }

    var tempDelay time.Duration // how long to sleep on accept failure

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(connCtx)
    }
}

可以看出,Golang 接收 web 是基於 TCP 協議之上,然後就是調用 Serve 方法,處理連接請求,Serve 方法會啓動 goroutine 進行異步處理,這也是高併發的基本所在。

宏觀層面查看大致流程如圖: 

serve 方法中以無限循環方式 (for) 接收客戶端請求並進行處理,主要邏輯如下:

if tlsConn, ok := c.rwc.(*tls.Conn); ok {
        if d := c.server.ReadTimeout; d != 0 {
            c.rwc.SetReadDeadline(time.Now().Add(d))
        }
        if d := c.server.WriteTimeout; d != 0 {
            c.rwc.SetWriteDeadline(time.Now().Add(d))
        }
        if err := tlsConn.Handshake(); err != nil {
            // If the handshake failed due to the client not speaking
            // TLS, assume they're speaking plaintext HTTP and write a
            // 400 response on the TLS conn's underlying net.Conn.
            if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
                io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
                re.Conn.Close()
                return
            }
            c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
            return
        }
        c.tlsState = new(tls.ConnectionState)
        *c.tlsState = tlsConn.ConnectionState()
        if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
            if fn := c.server.TLSNextProto[proto]; fn != nil {
                h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
                fn(c.server, tlsConn, h)
            }
            return
        }
    }
    
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)

其中 ServeHTTP 的實現:

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

在這裏終於發現我們定義的路由處理的 handler 方法。

整體流程如圖:

通過以上源碼的觀看,基本瞭解了 golang 處理一次 web 請求的大體過程。對此,你是否已經有所瞭解?

本文首次初步探索 Golang 的 web 服務的大體過程,提供一個功能強大的 web 服務這纔是初步探索,還要有路由解析、中間件等更多的組件進行封裝,敬請期待後續的探索研究。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DiJYnoZ4mO4iATDGgxEApg