Go是一門通用的程式語言,想要學習 Go 語言的 Web 開發,就必須知道如何用 Go 啟動一個 HTTP 伺服器用於接收和響應來自客戶端的 HTTP 請求。用 Go實現一個http server
非常容易,Go 語言標準庫net/http
自帶了一系列結構和方法來幫助開發者簡化 HTTP 服務開發的相關流程。因此,我們不需要依賴任何第三方元件就能構建並啟動一個高併發的 HTTP 伺服器。這篇文章會學習如何用net/http
自己編寫實現一個HTTP Serve
並探究其實現原理,以此來學習瞭解網路程式設計的常見正規化以及設計思路。
HTTP 服務處理流程
基於HTTP構建的服務標準模型包括兩個端,客戶端(Client
)和服務端(Server
)。HTTP 請求從客戶端發出,服務端接受到請求後進行處理然後將響應返回給客戶端。所以http伺服器的工作就在於如何接受來自客戶端的請求,並向客戶端返回響應。
典型的 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
服務。
路由註冊
http.HandleFunc
和http.Handle
都是用於給路由規則指定處理器,http.HandleFunc
的第一個引數為路由的匹配規則(pattern)第二個引數是一個簽名為func(w http.ResponseWriter, r *http.Requests)
的函式。而http.Handle
的第二個引數為實現了http.Handler
介面的型別的例項。
http.HandleFunc
和http.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
方法來完成路由處理器的註冊。
這裡我們遇到兩種型別的物件:ServeMux
和Handler
。
Handler
http.Handler
是net/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
,是一個map
,key
是路由表示式,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
物件,只不過ServeMux
的ServeHTTP
方法不是用來處理具體的request
和構建response
,而是用來透過路由查詢對應的路由處理器Handler
物件,再去呼叫路由處理器的ServeHTTP 方法去處理request
和構建reponse
。
註冊路由
搞明白Handler
和ServeMux
之後,我們再回到之前的程式碼:
DefaultServeMux.Handle(pattern, handler)
這裡的DefaultServeMux
表示一個預設的ServeMux
例項,在上面的例子中我們沒有建立自定義的ServeMux
,所以會自動使用DefaultServeMux
然後再看一下ServeMux
的Handle
方法是怎麼註冊路由的處理函式的:
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
方法註冊路由時主要做了兩件事情:一個就是向ServeMux
的map[string]muxEntry
增加給定的路由匹配規則;然後如果路由表示式以'/'
結尾,則將對應的muxEntry
物件加入到[]muxEntry
中,按照路由表示式長度倒序排列。前者用於路由精確匹配,後者用於部分匹配,具體怎麼匹配的後面再看。
自定義 ServeMux
透過http.NewServeMux()
可以建立一個ServeMux
例項取代預設的DefaultServeMux
我們把上面輸出Hello World
的 http 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()
}
在Server
的ListenAndServe
方法中,會初始化監聽地址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
方法的主要邏輯。首先建立一個上下文物件,然後呼叫Listener
的Accept()
接收監聽到的網路連線;一旦有新的連線建立,則呼叫Server
的newConn()
建立新的連線物件,並將連線的狀態標誌為StateNew
,然後開啟一個goroutine
處理連線請求。
處理連線
在開啟的 goroutine
中conn
的serve()
會進行路由匹配找到路由處理函式然後呼叫處理函式。這個方法很長,我們保留關鍵邏輯。
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
。最後呼叫ServeMux
的ServeHTTP()
方法匹配當前路由對應的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
方法後,最後在方法裡就會呼叫處理器Handler
的ServeHTTP
方法處理請求、構建寫入響應:
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/5dd11baff265da0c0c1...
https://github.com/unknwon/building-web-ap...
https://medium.com/honestbee-tw-engineer/g...
本作品採用《CC 協議》,轉載必須註明作者和本文連結