1. 程式人生 > 程式設計 >Go - http.Server原始碼分析

Go - http.Server原始碼分析

?

本文將從如下幾部分分析net/http模組中關於server部分的原始碼:

  • Handler型別和HandlerFunc型別是什麼?

  • ServeMuxHTTP handler的註冊管理和分發

  • Server啟動流程

1. Handler

1.1 從註冊HTTP handler入手

原生註冊HTTP handler有如下兩種寫法,它們有什麼區別呢?

func handler(w http.ResponseWriter,r *http.Request) {}

http.HandleFunc("/some-pattern",handler)
http.Handle("/some-pattern"
,http.HandlerFunc(handler)) 複製程式碼

兩個方法對應的原始碼如下:

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

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

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

ServeMux型別是什麼暫時可以不用理會,會在後文提到。

可以發現,差異體現在HandlerFunc(handler)這一語句,一個在內部呼叫,一個在外部呼叫。 而這一語句的作用在於將一個普通的函式轉換成為Handler型別,最終只有實現了Handler介面的物件可以註冊到HTTP服務端,為特定的路徑及其子樹提供服務, 它起到一個介面卡的作用。

原始碼如下

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

type HandlerFunc func(ResponseWriter,*Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter,r *Request) {
	f(w,r)
}
複製程式碼

假設f是一個有著正確簽名的函式,那麼HandlerFunc(f)就代表一個HTTP handler,除此之外,ServeHTTP方法的呼叫也代表著請求的處理, 它的呼叫時機將在後文提到

1.2 內建的幾個Handler原始碼以及使用

1.2.1 NotFoundHandler

返回一個請求處理器,該處理其對每個請求都返回404 page not found

http.Handle("/some-pattern",http.NotFoundHandler())
複製程式碼

原始碼

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

func NotFoundHandler() Handler { return HandlerFunc(NotFound) }
複製程式碼

1.2.2 RedirectHandler

返回一個請求處理器,該處理器對每個請求都使用狀態碼code重定向到網址url

http.Handle("/some-pattern",http.RedirectHandler("/",301))
複製程式碼

原始碼,有一說一,其中的Redirect函式看起來沒什麼分析價值,跳過

type redirectHandler struct {
	url  string
	code int
}

func RedirectHandler(url string,code int) Handler {
	return &redirectHandler{url,code}
}

func (rh *redirectHandler) ServeHTTP(w ResponseWriter,r *Request) {
	Redirect(w,r,rh.url,rh.code)
}
複製程式碼

1.2.3 StripPrefix

在將請求定向到你通過引數指定的請求處理器之前,將特定的prefix從URL中過濾出去

http.Handle("/api/some-pattern",http.StripPrefix("/api",handler))
複製程式碼

原始碼

func StripPrefix(prefix string,h Handler) Handler {
	if prefix == "" {
		return h
	}
	return HandlerFunc(func(w ResponseWriter,r *Request) {
		if p := strings.TrimPrefix(r.URL.Path,prefix); len(p) < len(r.URL.Path) {
			// 淺拷貝request
			r2 := new(Request)
			*r2 = *r
			// r.URL是一個引用,需要再拷貝一次
			r2.URL = new(url.URL)
			*r2.URL = *r.URL
			// 重置請求路徑
			r2.URL.Path = p
			h.ServeHTTP(w,r2)
		} else {
			NotFound(w,r)
		}
	})
}
複製程式碼

理解該部分只需要瞭解兩個點:

  • new(T)為型別申請一片記憶體空間,並返回指向這片記憶體的指標

  • 對指標變數進行取值*操作,可以獲得指標變數指向的原變數的值

1.2.4 TimeoutHandler

返回一個採用指定時間限制的請求處理器,如果某一次呼叫耗時超過了時間限制,該處理器會回覆請求狀態碼503 Service Unavailable,並將msg作為回覆的主體。

func handler(w http.ResponseWriter,r *http.Request) {
	time.Sleep(2 * time.Second)
}

http.Handle("/some-pattern",http.TimeoutHandler(http.HandlerFunc(handler),1 * time.Second,"Timeout"))
複製程式碼

定義

type timeoutHandler struct {
	handler Handler
	body    string
	dt      time.Duration
}

func TimeoutHandler(h Handler,dt time.Duration,msg string) Handler {
	return &timeoutHandler{
		handler: h,body:    msg,dt:      dt,}
}
複製程式碼

當觸發請求處理器時,ServeHTTP方法會執行下面的操作(為了保證可讀性簡化非關鍵程式碼)

func (h *timeoutHandler) ServeHTTP(w ResponseWriter,r *Request) {
    // 初始化一個可被取消的上下文
    ctx,cancelCtx := context.WithTimeout(r.Context(),h.dt)
    defer cancelCtx()
    // 設定r.ctx
    r = r.WithContext(ctx)
    done := make(chan struct{})
    tw := &timeoutWriter{
        w: w,h: make(Header),}
    // 起一個goroutine來執行原本的邏輯
    go func() {
        h.handler.ServeHTTP(tw,r)
        close(done)
    }()
    // 等待一個通訊
    select {
    // 如果沒有超時正常返回
    case <-done:
        tw.mu.Lock()
        defer tw.mu.Unlock()
        dst := w.Header()
        for k,vv := range tw.h {
            dst[k] = vv
        }
        if !tw.wroteHeader {
            tw.code = StatusOK
        }
        w.WriteHeader(tw.code)
        w.Write(tw.wbuf.Bytes())
    // 如果超時
    case <-ctx.Done():
        tw.mu.Lock()
        defer tw.mu.Unlock()
        w.WriteHeader(StatusServiceUnavailable)
        io.WriteString(w,h.errorBody())
        tw.timedOut = true
    }
}
複製程式碼

2. 多路轉接器ServeMux

ServeMux是HTTP請求的多路轉接器,它會將每一個接收請求的URL與一個註冊模式的列表進行匹配,並呼叫最匹配的模式的處理器, 第一部分的註冊HTTP handler的過程實際上是在ServeMux內部的一個雜湊表中新增一條記錄

2.1 註冊流程

ServeMux結構體如下

type ServeMux struct {
	// 讀寫鎖
	mu    sync.RWMutex
	// 管理所有註冊路由雜湊表
	m     map[string]muxEntry
	// 按pattern長度降序排列的匹配列表,記錄值均以/結尾
	es    []muxEntry
	// 是否存在hosts,即不以'/'開頭的pattern
	hosts bool
}

type muxEntry struct {
	h       Handler
	pattern string
}

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
複製程式碼

上文提到了,每次註冊一個HTTP handler最終呼叫的都是DefaultServeMux.Handle(pattern,handler)方法, 這個方法做的事情很簡單,就是維護內部雜湊表m,省略部分錯誤處理程式碼後原始碼如下:

func (mux *ServeMux) Handle(pattern string,handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()
	
	// 如果註冊一個已註冊的處理器,將panic
	if _,exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}
	// 註冊
	e := muxEntry{h: handler,pattern: pattern}
	mux.m[pattern] = e
	// 以斜槓結尾的pattern將存入es切片並按pattern長度降序排列
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es,e)
	}
	// 不以"/"開頭的模式將視作存在hosts
	if pattern[0] != '/' {
		mux.hosts = true
	}
}
複製程式碼

註冊流程結束,所以說http.Handle(pattern,handler)只是單純的在DefaultServeMux的雜湊表m中執行註冊,並沒有開始分發

2.2 分發

ServeMux結構體也實現了Handler介面,因此它才是真正的分發者!

func (mux *ServeMux) ServeHTTP(w ResponseWriter,r *Request) {
	if r.RequestURI == "*" {
		// 對於http協議小於1.1的處理
		if r.ProtoAtLeast(1,1) {
			w.Header().Set("Connection","close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	// 尋找到最接近的HTTP handler
	h,_ := mux.Handler(r)
	h.ServeHTTP(w,r)
}
複製程式碼

mux.Handler(r)方法始終會返回一個不為空的HTTP handler,其中包含較多的對特殊情況的處理,從原始碼學習的角度來說,陷入這些分支是不正確的,只應該 考慮最主要的情況進行分析,因此,Handler方法簡化為:

func (mux *ServeMux) Handler(r *Request) (h Handler,pattern string) {
	// 當請求地址為/tree,但只註冊了/tree/未註冊/tree時,301重定向
	// 此處redirectToPathSlash並沒有分析價值,檢測一下兩者是否在mux.m雜湊表中即可
	if u,ok := mux.redirectToPathSlash(host,path,r.URL); ok {
		return RedirectHandler(u.String(),StatusMovedPermanently),u.Path
	}
    
	return mux.handler(host,r.URL.Path)
}
複製程式碼

最後的handler才是Handler方法實現的核心,原始碼如下:

func (mux *ServeMux) handler(host,path string) (h Handler,pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// 指定主機的模式優於一般的模式
	if mux.hosts {
		h,pattern = mux.match(host + path)
	}
	if h == nil {
		h,pattern = mux.match(path)
	}
	// 如果沒有匹配到任何Handler,將返回404 handler
	if h == nil {
		h,pattern = NotFoundHandler(),""
	}
	return
}
複製程式碼

假設此時DefaultServeMux註冊了兩個模式: /a/,/a/b/,此時DefaultServeMux的結構為

{
    m: {
        "/a/": { h: HandlerA,pattern: "/a/" },"/a/b/": { h: HandlerB,pattern: "/a/b" },},es: [{ h: HandlerB,{ h: HandlerA,pattern: "/a/" }]
}
複製程式碼

當請求路徑為/a/b/c,將進入第二個if語句,在match方法中進行匹配:

func (mux *ServeMux) match(path string) (h Handler,pattern string) {
	// 直接匹配成功的情況
	v,ok := mux.m[path]
	if ok {
		return v.h,v.pattern
	}
	// 尋找最接近的最長匹配,mux.es切片中包含了所有子樹,並降序排列,因此遍歷一次即可找出最接近的模式
	for _,e := range mux.es {
		if strings.HasPrefix(path,e.pattern) {
			return e.h,e.pattern
		}
	}
	return nil,""
}
複製程式碼

最終路徑/a/b/c將返回handlerB

3. Server

Server的部分結構如下:

type Server struct {
    // 監聽的地址和埠
    Addr string
    // 所有請求都要呼叫的Handler
    Handler Handler
    // 讀的最大超時時間
    ReadTimeout time.Duration
    // 寫的最大超時時間
    WriteTimeout time.Duration
    // 請求頭的最大長度
    MaxHeaderBytes int
    ...
}
複製程式碼

3.1 從啟動HTTP server入手

例項程式碼

http.ListenAndServe(":8001",nil)
複製程式碼

原始碼中實際上是建立一個server例項,當handlernil時,將使用DefaultServerMux作為預設的handler, 也就是第二節中提到的"多路轉接器",這也是最常見的做法

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

server.ListenAndServe方法中,將呼叫net.Listen("tcp",addr)監聽埠, 不過關於TCP的內容不在分析範圍內,直接進入srv.Serve方法

func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	ln,err := net.Listen("tcp",addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
複製程式碼

srv.Serve方法中則會在一個for迴圈中,完成如下的工作:

  • 呼叫l.Accept()獲得一個新的連線,進行後續操作

  • TCP conn轉換為服務端的HTTP conn

  • 啟動一個goroutine來處理這個HTTP conn

func (srv *Server) Serve(l net.Listener) error {
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	// 獲得根context
	baseCtx := context.Background()
	// 返回一個在根context的基礎上新增鍵為ServerContextKey,值為當前Server引用的context
	ctx := context.WithValue(baseCtx,ServerContextKey,srv)
	for {
		// 接收一個請求
		rw,e := l.Accept()
		// 將tcp conn轉換為http conn
		c := srv.newConn(rw)
		// 啟動一個goroutine處理這個請求
		go c.serve(ctx)
	}
}
複製程式碼

c.serve(ctx)則會進行最後的處理,此部分比較複雜,其實只需要關心serverHandler{c.server}.ServeHTTP(w,w.req)這一行

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	// HTTP/1.x from here on.
	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c},4<<10)
	
	ctx,cancelCtx := context.WithCancel(ctx)
	defer cancelCtx()

	for {
		w,err := c.readRequest(ctx)
		......

		serverHandler{c.server}.ServeHTTP(w,w.req)
		......
	}
}
複製程式碼

最終也就是呼叫DefaultServeMux作為整體的HTTP handler

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)
}
複製程式碼

OVER

3.2 整體流程