http服務原始碼分析

peng發表於2019-05-13

多讀go的原始碼,可以加深對go語言的理解和認知,今天分享一下http相關的原始碼部分
在不使用第三方庫的情況下,我們可以很容易的的用go實現一個http服務,

package main

import (
    "fmt"
    "net/http"
)

func IndexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world ! ")
}

func main() {
    http.HandleFunc("/", IndexHandler)
    if err := http.ListenAndServe(":9100", nil); err != nil {
        panic(err)
    }
}

直接在瀏覽器裡訪問9100埠就可以返回 hello world !
go已經把所有的細節封裝好了,我們只需要自己去寫Handler實現就夠了。原始碼簡單來說做了以下幾件事:

  • 把我們自定義的Handler方法新增到預設路由DefaultServeMux的Map裡比如:http.HandleFunc("/", IndexHandler) (btw: go語言的map是非執行緒安全的,可以在http原始碼裡看到官方的處理方式);
  • 啟動一個tcp服務監聽9100埠,等待http呼叫;
  • 當監聽到有http呼叫時,啟動一個協程來處理這個請求,這個是go的http服務快的一個重要原因,把請求內容轉換成http.Request, 把當前連線封裝http.RespnseWriter;
  • 預設路由DefaultServeMux根據request的path找到相應的Handler,把 request和 responseWriter傳給Handler 進行業務邏輯處理,response響應資訊write給客戶端;

ServeMux & Handler

http 包的預設路由 DefaultServeMuxServeMux 結構休的例項
http.HandleFunc("/", IndexHandler) 的呼叫,會把path資訊和自定義的方法資訊儲存到 DefaultServeMuxm map[string]muxEntry變數裡
我們看一下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 中儲存了pathHandler 的對應關係,也是路由關係。

Handler

muxEntry 中的 h Handler 對就的就是我們自定義的Handler方法比如,我們自己例子中的方法 func IndexHandler(w http.ResponseWriter, r *http.Request) 細心的同學可能會問 Handler是一個介面,但是我們只是定義了一個方法,這是怎麼轉換的呢?
介面Halder設定了簽名規則,也就是我們自定義的處理方法

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

go語言中所有的自定義型別都可以實現自己的方法,http包是用一個自定義的func來去實現了Handler介面,

type HandlerFunc func(ResponseWriter, *Request)

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

然後在ServerMux的方法HandleFunc處理的時候會把 handler func(ResponseWriter, *Request) 轉換成 HandlerFunc, 如下所示:

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

ServerMux 結構中還有一個讀寫鎖 mu sync.RWMutex mu就是用來處理多執行緒下map的安全訪問的。

查詢&呼叫 Handler

得到自定義的handler方法,就是去map中根據path匹配得到Handler

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
}
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, ""
}

ServeMux 實現了 Handler 介面,也是預設的路由呼叫的具體規則實現的地方,他的 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)
}

介面Halder設定了簽名規則,也就是我們自定義的處理方法
比如下面的程式碼,函式IndexHandler就是我們自定義的方法,返回給客戶端請求一個 hello world ! 字串。中間請求是如何呼叫到我們自定義的方法的具體邏輯都是http包提供的,但是一點也不神祕,

http.HandleFunc("/", IndexHandler)

// IndexHandler 我們自己定義的Handler方法
func IndexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world ! ")
}
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
// 
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

http ListenAndServe

說完 ServeMux 是如何結合 Handler 介面,來實現路由和呼叫後,就要說一下,http服務是如何得到客戶端傳入的資訊,封裝requet和rresponse的。
在啟動程式的時候http.ListenAndServe, 有兩個引數,第一個是指寫埠號,第二個是處理邏輯,如果我們沒有給定處理邏輯,會使用預設的處理DefaultServeMux

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

ListenAndServe 方法開啟tcp埠進行監聽,然後把Listener 傳給srv.Serve方法

func (srv *Server) ListenAndServe() error {
    // 省略部分程式碼 ...
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

具體要說一下 Service 方法,這個方法中,對監聽tcp請求,然後把請求的客戶端連線進行封裝,

func (srv *Server) Serve(l net.Listener) error {
    // 省略部分程式碼 ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        // 省略部分程式碼 ...
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

把客戶端的請求封裝成一個Conn,然後啟動一個協程go c.serve(ctx)來處理這個連線請求,這就是http包快的一個重要原因,每一個連線就是一個協程。客戶端可以先和伺服器進行連線,然後利用這個conn來多次傳送http請求,這樣,就可以減少每次的進行連線而提高一些速度。像一些rpc裡就是利用這點去實現的雙向的stream流,比如我之前的帖子go微服務框架go-micro深度學習(五) stream 呼叫過程詳解,他就是建立一個tcp連線,然後基於這個conn,傳送多個request,返回多次respose資料。

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    // 省略部分程式碼 ...
    // 迴圈讀取請求 ...
    for {
        // 讀取請求資料,封裝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)
        }
        // 省略部分程式碼 ...
        // 路由呼叫自定義的方法,把封裝好的responseWrite和 request傳到方法內
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest()
        // 省略部分程式碼 ...
    }
}

相關文章