文章目錄
出發
Go 的 web 編程非常簡潔。python 寫 web app,比如 flask 或者 django,都要使用 uwsgi 或者 gunicorn 這種 web server 來提供生產級服務,web server 可以啓動多進程來利用多核 CPU 實現更高的併發處理能力。Go 天生的 goroutine 實現了用戶級線程,不僅更快而且可以充分利用多核 CPU,併發性能非常好,編譯成二進制文件以後直接啓動就可以提供生產可用的服務了,非常簡潔。
web app 必要構件
最簡單的 web app 只需要如下幾個組成部分:
- 請求處理函數
func test(w http.ResponseWriter, _ *http.Request){
fmt.Fprintf(w, "test\n")
}
- 註冊路由
http.HandleFunc("/test", test)
- 啓動服務器
log.Fatal(http.ListenAndServe(":5000", nil))
整體流程
閱讀源碼
下面我從服務啓動函數讀起,逐步瞭解一次 http 請求的處理過程。
1 http.ListenAndServe
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
該函數監聽 TCP 地址然後調用處理器(handler)處理到來的請求,典型的 handler 爲 nil,這時使用默認的路由器來路由請求。ListenAndServe 只會返回非 nil 的錯誤。
這個函數的主要作用是初始化一個服務器結構體(Server)的實例,爲它的Handler屬性賦值。
看一下 Server 結構體,它定義了運行一個 http 服務器所需的參數,這個結構體的零值(默認值)就是一個可用的配置。
type Server struct {
Addr string // 監聽的 TCP 地址,爲空的話默認爲 ":http",也就是80端口
Handler Handler // 處理器,如果爲 nil 的話使用默認的路由器 http.DefaultServeMux
// TLSConfig 是可選項,配置 TLS
TLSConfig *tls.Config
// 讀取完整請求的超時時長,包括請求體
ReadTimeout time.Duration
// 允許讀取請求頭的時長。
ReadHeaderTimeout time.Duration
// 寫回響應的超時時長
WriteTimeout time.Duration
// 啓用 keep-alives 時,一個長連接的兩個請求之間間隔的最大時長
IdleTimeout time.Duration
// 允許的請求頭的最大字節數,不限制請求體
MaxHeaderBytes int
// TLS 相關的配置
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
// 當客戶端連接狀態改變時調用的回調函數,可選
ConnState func(net.Conn, ConnState)
// 可選的自定義 logger,不設就是標準日誌庫的 logger
ErrorLog *log.Logger
// 爲來到此服務器的請求指定基 context ,不設就是 context.Background()
BaseContext func(net.Listener) context.Context
// 可選,通常指定一個函數,用於修改新連接的 context
ConnContext func(ctx context.Context, c net.Conn) context.Context
disableKeepAlives int32 // 關閉 keep-alives
inShutdown int32 // 非0表示正在關閉
nextProtoOnce sync.Once // http2相關
nextProtoErr error // http2相關
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
doneChan chan struct{}
onShutdown []func()
}
2 Server.ListenAndServe
接下來是 Server 的 ListenAndServe 方法,它監聽 TCP 網絡地址 srv.Addr 然後調用 Serve 函數處理到達連接的請求。支持 TCP keep-alives。該函數返回非空的 error。
func (srv *Server) ListenAndServe() error {
// 如果服務關閉了,返回的錯誤爲 ErrServerClosed。
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
// 如果結構體的 Addr 屬性爲空就設置
// 默認值 ":http"
if addr == "" {
addr = ":http"
}
// 調用 net 的 Listen 方法,得到一個 TCPListener
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
返回 srv 的 Serve 方法,上面得到的 TCPListener作爲參數
return srv.Serve(ln)
}
net.Listen 方法監聽本地的網絡地址,network參數可以是 tcp、tcp4、tcp6、 unix 或者 unixpacket。address 參數可以用 hostname,但是不建議,因爲這樣創建的 listener(監聽器)最多監聽主機的一個IP地址。如果 address 參數的 port 爲空或者“0”,如“127.0.0.1:” 或者 “[::1]:0”,將自動選擇一個端口號。
這個函數的主要作用就是初始化一個net包的 TCPListener 結構體的實例,然後調用服務器的Serve方法爲這個連接提供服務。
3 Server.Server
Server 結構體的 Serve 方法。它爲 listerner 上每個到達的連接創建一個新的服務協程(goroutine)。服務協程讀取request(請求)然後調用 srv.Handler 方法做出迴應。
Serve 方法返回非空的error並關閉listener。
這個方法運行一個死循環,等待底層tcp或者tls連接到來的請求數據,沒有請求數據就休眠一段時間,數據到來以後把服務器實例(Server)和底層連接(net.Conn)封裝爲一個內部的conn結構體的實例,執行它的serve方法。
func (srv *Server) Serve(l net.Listener) error {
// 如果有一個包裹了 srv 和 listener 的鉤子函數,就執行它
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
// 把 listener 賦值給 origListener 變量,然後將原來的 linstner
// 作爲 Listener 屬性初始化一個onceCloseListener結構體的實例,
// 顧名思義,只關閉一次的監聽器,這個包裹是爲了防止有多個 Close 調用。
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
// srv.setupHTTP2_Serve 方法有條件的配置 srv 的 HTTP/2。
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
// srv.trackListener 向跟蹤的監聽器集(srv.listeners)中添加一個監聽器(l),
// 並保證它最終會刪除這個監聽器
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
var tempDelay time.Duration // 如果沒有接收到請求的話,休眠多久
// 定義基礎context
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
// 爲ctx的ServerContextKey賦值srv
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
// 死循環,即伺服
for {
// rw 是net包裏的Conn接口,他是一個通用的面向流的網絡連接。
rw, e := l.Accept()
// 如果監聽器接收報錯
if e != nil {
select {
// 如果 srv.getDoneChan 中有內容,返回 ErrServerClosed
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
// 如果 e 是 net.Error, 並且錯誤是臨時性的
if ne, ok := e.(net.Error); ok && ne.Temporary() {
// 第一次沒接收到數據,睡眠5毫秒
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
// 每次隨眠結束,喚醒後還是沒接收到數據,睡眠時間加倍
} else {
tempDelay *= 2
}
// 單次睡眠時間上限設定爲1秒
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
// 記錄接收數據失敗日誌,休眠 tempDelay
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
// 如果e不是net.Error,或者不是臨時性錯誤,就返回錯誤
return e
}
// 如果接收到數據就做如下處理:
// 如果指定了srv的ConnContext,就用它修改連接的context
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
// 休眠定時器歸零
tempDelay = 0
// 使用當前的Conn接口構建新的 conn 實例,它包含了srv服務器和rw連接。
// conn 實例代表一個 HTTP 連接的服務端,rw是底層的網絡連接
c := srv.newConn(rw)
// 設置底層連接的狀態
// 第一次調用的時候初始化srv.activeConn爲一個map[*conn]struct{}
// srv.activeConn是一個集合,保存服務器的活躍連接
// setState方法將當前連接加入集合跟蹤狀態
c.setState(c.rwc, StateNew) // before Serve can return
// 啓動一個goroutine處理請求
go c.serve(connCtx)
}
}
看一下這個server.go 內的conn結構體
// conn 代表 HTTP 連接的服務側.
type conn struct {
// 連接到達的服務器。
// 不可變,不能爲空。
server *Server
// 取消連接層面的context的取消函數
cancelCtx context.CancelFunc
// rwc 是底層網絡連接。
// 它不能被其他類型包裹,通常是 *net.TCPConn 或 *tls.Conn 類型。
rwc net.Conn
// remoteAddr 就是 rwc.RemoteAddr().String()。它不在 Listener 接收數據的 goroutine,裏同步填充。
// 它在 (*conn).serve goroutine裏被立即填充
// 這是 Handler 的 (*Request).RemoteAddr 的值。
remoteAddr string
// tlsState 是使用 TLS 時 TLS 的連接狀態。
// nil 表示不用 TLS。
tlsState *tls.ConnectionState
// werr is set to the first write error to rwc.
// 通過 checkConnErrorWriter{w} 設置,bufw 寫入。
werr error
// r 是 bufr 的讀入源。它是 rwc 的一個包裹器,提供io.LimitedReader
// 式的限制 (在讀取請求頭的時候)
// 並支持 CloseNotifier 的功能。詳閱 *connReader 的文檔。
r *connReader
// bufr 從 r 讀取。
bufr *bufio.Reader
// bufw 寫入 checkConnErrorWriter{c}, 當發生錯誤時填充 werr。
bufw *bufio.Writer
// lastMethod 是當前連接的最後一個請求的方法。
lastMethod string
curReq atomic.Value // of *response (which has a Request in it)
curState struct{ atomic uint64 } // packed (unixtime<<8|uint8(ConnState))
// mu 保護 hijackedv
mu sync.Mutex
// hijackedv 表示這個連接是否被一個帶有Hijacker接口的 Handler hijacked了。
hijackedv bool
}
4 *conn.Serve
下面看看goroutine處理http(s) 請求的過程:
調用上面srv.newConn(rw)方法創建的conn實例的serve方法服務一個新的連接
func (c *conn) serve(ctx context.Context) {
// 設置遠端地址和本端地址
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
// 錯誤處理
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
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)
}
}()
// https相關
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; validNPN(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initNPNRequest{ctx, tlsConn, serverHandler{c.server}}
fn(c.server, tlsConn, h)
}
return
}
}
// HTTP/1.x 相關從這裏開始
// 確保執行cancelCtx
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
// connReader 是一個包裹了 *conn的 io.Reader。
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
// 從連接中讀取請求,在讀取超時時間內解析驗證請求頭中的內容。
// w 是返回的response實例
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)
}
// 根據錯誤碼,向底層的net.Conn(實現了讀寫關閉接口)實例寫入錯誤信息
if err != nil {
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"
switch {
case err == errTooLarge:
// 請求頭字段過大。
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
// 寫回響應並關閉連接。
c.closeWriteAndWait()
return
case isUnsupportedTEError(err):
// 無法識別請求編碼。
// Respond as per RFC 7230 Section 3.3.1 which says,
// A server that receives a request message with a
// transfer coding it does not understand SHOULD
// respond with 501 (Unimplemented).
code := StatusNotImplemented
// We purposefully aren't echoing back the transfer-encoding's value,
// so as to mitigate the risk of cross side scripting by an attacker.
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
return
case isCommonNetReadError(err):
return // don't reply
default:
// err 是 badRequestError 類型
publicErr := "400 Bad Request"
if v, ok := err.(badRequestError); ok {
publicErr = publicErr + ": " + string(v)
}
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
}
}
// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
c.curReq.Store(w)
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
// HTTP 不能同時有多個活躍的請求。[*]
// 服務器回覆了一個請求後才能讀取下一個請求,
// 所以我們可以在這個 goroutine 裏執行 handler 。
// [*] 並非嚴格限定: HTTP pipelining. 即便它們需要串行地響應,
// 我們也可以讓它們全部並行地處理。
// 但我們不打算實現 HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
// 將 server 包裹爲 serverHandler 的實例,執行它的
// ServeHTTP 方法,處理請求,返回響應。
// serverHandler 委託給 server 的 Handler 或者 DefaultServeMux(默認路由器)
// 來處理 "OPTIONS *" 請求。
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))
if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}
if d := c.server.idleTimeout(); d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
if _, err := c.bufr.Peek(4); err != nil {
return
}
}
c.rwc.SetReadDeadline(time.Time{})
}
}
這一步的主要工作是解析驗證請求頭,然後把server 包裹爲 serverHandler 的實例,執行它的ServeHTTP 方法,處理請求,返回響應。
5. serverHandler.ServeHTTP
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
如果沒有定義 server.Handler,就是用默認路由器
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
Handler 是一個接口,嵌入在Server裏。默認的DefaultServeMux是一個ServeMux類型的實例。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler 響應 HTTP 請求。
ServeHTTP 方法將響應頭賀響應體寫入 ResponseWriter 並返回。返回標誌着請求的結束;與 ServeHTTP 調用同時或者在ServeHTTP後使用 ResponseWriter 或者 讀取 Request.Body 都是無效的。
由於 HTTP 客戶端、HTTP 協議版本和任何客戶端與Go服務器之間的中間人的不同,有可能在寫入 ResponseWriter 後就不能讀取Request.Body了。注意handlers應該先讀取Request.Body然後再響應。
除了讀取請求體之外,handlers不應該修改請求。
ServeHTTP panic了,(調用ServeHTTP方法的)服務器假設 panic 的影響僅限於當前活躍的請求。它恢復 panic、將堆棧追蹤記入錯誤日誌,然後根據不同的HTTP協議執行關閉網絡連接或者發送一個HTTP/2 RST_STREAM,使用 ErrAbortHandler panic可以中斷處理請求,客戶端可以看到中斷的響應而服務器不會記錄錯誤日誌。
6. ServeMux.ServeHTTP
ServeMux是標準庫提供的HTTP請求路由器,它將到來的請求的url與一套註冊的模板進行匹配,調用與請求的URL匹配最接近的handler處理請求。
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // 路由項切片,從長到短排好序的
hosts bool // 匹配的模板是否包含主機名
}
ServeHTTP 方法將請求分發給與請求URL匹配最接近的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)
}
*ServeMux的Handler方法根據請求的方法、主機名和URL路徑返回給定請求的處理器(handler)。它總是會返回非空的handler。如果路徑不符合格式規範,handler將是一個跳轉到規範路徑的內部生成的handler。如果host有端口,在匹配handler時會忽略端口。
Handler也返回請求匹配的模板,如果是內部生成的跳轉,模板就在跳轉後匹配。
如果沒有爲請求註冊handler,Handler就返回 ‘page not found’ 和空模板
h.ServeHTTP(w, r)
是具體的處理函數,最後說明。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
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)
}
// 刪除port,整理path
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// 如果給的路徑是 /tree 但沒註冊這個handler,就跳轉到 /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)
}
7 *ServeMux.handler
這是Handler的主要實現。除了 CONNECT 方法以外,路徑格式必須符合規範。
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// 指定host的模板優先處理
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
這裏的match
函數返回HandlerFunc(f).
之前函數func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
的最後一句h.ServeHTTP(w, r)
找了半天沒找到是什麼,其實上面這個func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)
方法返回的就是通過路由匹配到的註冊到Server的HandleFunc,它的ServeHTTP方法即是註冊的HandleFunc函數,也就是最上面的那個test
==函數。至此,一個http 請求的完整處理過程基本捋出來了。==下面是HandlerFunc類型和它的ServeHTTP方法:
// HandlerFunc 類型是一個允許一個普通函數用於HTTP處理函數的適配器。
// 如果 f 是一個簽名適配的函數,HandlerFunc(f) 是一個調用 f 的 Handler。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP 調用 f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
總結
因爲很喜歡 Go 的 Http Server 所以很想了解源碼。第一次啃這麼大量的源碼,用了幾天的時間,只是捋出一個梗概的脈絡,有一個總體的認知,並沒有深入很多細枝末節,但是收穫還是很大,後面有時間再細緻研究。
代碼方面的收穫主要有以下幾點:
- 使用接口,實現了
ServeHTTP(ResponseWriter, *Request)
方法就是Handler
接口,http包的server.go裏面有大量這個方法調用。有很多類型都實現了這個方法,比如*ServeMux
、HandlerFunc
,而且HandlerFunc
是一個函數類型,也可以實現方法。有點麻煩的地方在於不一定能在IDE裏直接跳轉到方法定義,可能需要先找到正確的結構體,再找它的方法定義。 - 結構體嵌套,代碼用了很多次結構體包裹結構體的方式,實現類似繼承但更加清晰簡潔的複用方式。
- 集合類型用值是空結構體的map,最節省空間。
- 伺服函數用死循環,每次循環的等待時間成倍增加,設置一個最大等待時間的上限,這種逐漸增大等待時間配合等待時間上限即節約了cpu又保證了響應的性能。
type badRequestError string
類型有個返回string的Error()
方法,對於自定義類型,fmt.Print 會打印Error方法返回的字符串,如果沒有Error方法會打印String方法返回的字符串