Golang構建HTTP服務(一)--- net/http庫源碼筆記

搭建一個簡單的Go Web服務器

Go語言標準庫 - net/http

在學習Go語言有一個很好的起點,Go語言官方文檔很詳細,今天我們學習的Go Web服務器的搭建就需要用到Go語言官方提供的標準庫 net/http,通過http包提供了HTTP客戶端和服務端的實現。同時使用這個包能很簡單地對web的路由,靜態文件,模版,cookie等數據進行設置和操作。如果對http概念不是太清楚的朋友可以自行google。

http包建立Web服務器

package main
import (
    "fmt"
    "net/http"
    "strings"
    "log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm() //解析參數,默認是不會解析的
    fmt.Println(r.Form) //這些信息是輸出到服務器端的打印信息
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello Wrold!") //這個寫入到w的是輸出到客戶端的
}
func main() {
    http.HandleFunc("/", sayhelloName) //設置訪問的路由
    err := http.ListenAndServe(":9090", nil) //設置監聽的端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上面的代碼我們在IDE中編譯後並運行成功後,這個時侯我們就可以在9090端口監聽http鏈接請求了
頁面請求效果如圖:
image
代碼運行結果如圖:
image
這個時侯如果我們在瀏覽器地址後面加一些參數試試:http://localhost:9090?url_long=111&url_long=222,看看瀏覽器中輸出什麼?服務器端輸出的又是什麼

image
image

我們看到了上面的代碼,要編寫一個Web服務器是不是很簡單,只要調用http包的兩個函數就可以了。
我們看到Go通過簡單的幾行代碼就已經運行起來一個Web服務了,而且這個Web服務內部有支持高併發的特性。現在Web服務已經搭建完成了,那我們現在來了解一個這個服務是怎麼運行起來的呢?

Web工作方式的幾個概念

  • Request:用戶請求的信息,用來解析用戶的請求信息,包括post、get、cookie、url等信息
  • Response:服務器需要反饋給客戶端的信息
  • Conn:用戶的每次請求鏈接
  • Handler:處理請求和生成返回信息的處理邏輯

分析http包運行機制

image
這個過程我們需要清楚以下三個問題,則就清楚Go是如何讓Web運行起來了

  • 如何監聽端口?
    通過上面的代碼我們看到Go是通過一個函數ListenAndServe來處理這些事情的,這個底層其實這樣處
    理的:初始化一個server對象,然後調用了net.Listen("tcp", addr),也就是底層用TCP協議搭建了一個服
    務,然後監控我們設置的端口。

Go http包的源碼,這裏我們可以看到整個http處理過程

Http請求

網絡發展,很多網絡應用都是構建再 HTTP 服務基礎之上。HTTP 協議從誕生到現在,發展從1.0,1.1到2.0也不斷再進步。除去細節,理解 HTTP 構建的網絡應用只要關注兩個端---客戶端(clinet)和服務端(server),兩個端的交互來自 clinet 的 request,以及server端的response。所謂的http服務器,主要在於如何接受 clinet 的 request,並向client返回response。

接收request的過程中,最重要的莫過於路由(router),即實現一個Multiplexer器。Go中既可以使用內置的mutilplexer --- DefautServeMux,也可以自定義。Multiplexer路由的目的就是爲了找到處理器函數(handler),後者將對request進行處理,同時構建response。

簡單總結就是這個流程爲:

Clinet -> Requests -> [Multiplexer(router) -> handler -> Response -> Clinet

因此,理解go中的http服務,最重要就是要理解Multiplexer和handler,Golang中的Multiplexer基於ServeMux結構,同時也實現了Handler接口。

對於handler,如果有從事Java開發的朋友,你如果瞭解spring框架,應該聽說過這個詞,這裏的handler和spring框架裏的handler聽上去很相似,同樣是作爲一個方法的載體,但他們還是不同的。爲了更好的說明問題,本文約定了如下規則:

  • hander函數: 具有func(w http.ResponseWriter, r *http.Requests)簽名的函數
  • handler處理器(函數): 經過HandlerFunc結構包裝的handler函數,它實現了ServeHTTP接口方法的函數。調用handler處理器的ServeHTTP方法時,即調用handler函數本身。
  • handler對象:實現了Handler接口ServeHTTP方法的結構。

handler處理器和handler對象的差別在於,一個是函數,另外一個是結構,它們都有實現了ServeHTTP方法。很多情況下它們的功能類似,下文就使用統稱爲handler。這算是Golang通過接口實現的類動態類型吧。

image

image

Handler

Golang沒有繼承,類多態的方式可以通過接口實現。所謂接口則是定義聲明瞭函數簽名,任何結構只要實現了與接口函數簽名相同的方法,就等同於實現了接口。go的http服務都是基於handler進行處理。

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

任何結構體,只要實現了ServeHTTP方法,這個結構就可以稱之爲handler對象。ServeMux會使用handler並調用其ServeHTTP方法處理請求並返回響應。

ServeMux

瞭解了Handler之後,再看ServeMux。ServeMux的源碼很簡單:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    hosts bool 
}

type muxEntry struct {
    explicit bool
    h        Handler
    pattern  string
}

ServeMux結構中最重要的字段爲m,這是一個map,key是一些url模式,value是一個muxEntry結構,後者裏定義存儲了具體的url模式和handler。

當然,所謂的ServeMux也實現了ServeHTTP接口,也算是一個handler,不過ServeMux的ServeHTTP方法不是用來處理request和respone,而是用來找到路由註冊的handler,後面再做解釋。

Server

除了ServeMux和Handler,還有一個結構Server需要了解。從http.ListenAndServe的源碼可以看出,它創建了一個server對象,並調用server對象的ListenAndServe方法:

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

查看server的結構如下:

type Server struct {
    Addr         string        
    Handler      Handler       
    ReadTimeout  time.Duration 
    WriteTimeout time.Duration 
    TLSConfig    *tls.Config   

    MaxHeaderBytes int

    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
    disableKeepAlives int32     nextProtoOnce     sync.Once 
    nextProtoErr      error     
}

server結構存儲了服務器處理請求常見的字段。其中Handler字段也保留Handler接口。如果Server接口沒有提供Handler結構對象,那麼會使用DefautServeMux做multiplexer,後面再做分析。

創建HTTP服務

創建一個http服務,大致需要經歷兩個過程,首先需要註冊路由,即提供url模式和handler函數的映射,其次就是實例化一個server對象,並開啓對客戶端的監聽。

再看gohttp服務的代碼

http.HandleFunc("/", indexHandler)

即是註冊路由。

http.ListenAndServe("127.0.0.1:8000", nil)

或者:

server := &Server{Addr: addr, Handler: handler}

server.ListenAndServe()

註冊路由

閱讀框架源碼是學習的好方式,通常閱讀也有兩個方法,一是不求甚解,框架的主要流程要清晰,別的細枝末節,如果尚不能理解作者的用意,可以先忽略,不必馬上深究;其次,庖丁解牛,對於作者想要表達的主要流程,一定要明確,執行的邏輯和結構。兩者看起來略矛盾,其實不然。大體而言就是對主流程要清晰,主流程以外的細節需要先忽略。最簡單實踐方式就是,看不懂的就先放一邊。直到所有的都看不懂,再回去看以前不懂的部分,搞懂爲止。下面就查看http是如何註冊路由。

net/http包暴露的註冊路由的api很簡單,http.HandleFunc選取了DefaultServeMux作爲multiplexer:

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

那麼什麼是DefaultServeMux呢?實際上,DefaultServeMux是ServeMux的一個實例。當然http包也提供了NewServeMux方法創建一個ServeMux實例,默認則創建一個DefaultServeMux:

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

注意,go創建實例的過程中,也可以使用指針方式,即
type Server struct{}
server := Server{}
和下面的一樣都可以創建Server的實例
var DefalutServer Server
var server = &DefalutServer

因此DefaultServeMux的HandleFunc(pattern, handler)方法實際是定義在ServeMux下的:

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

上述代碼中,HandlerFunc是一個函數類型(如果將它上面的註釋翻譯過來,就是HandleFunc會爲給定的模式註冊處理程序函數,我們就可以爲會根據路由地址註冊相應的服務),同時實現了Handler接口的ServeHTTP方法。使用HandlerFunc類型包裝一下路由定義的indexHandler函數,其目的就是爲了讓這個函數也實現ServeHTTP方法,即轉變成一個handler處理器(函數)。

type HandlerFunc func(ResponseWriter, *Request)

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

一旦這樣做了,就意味着我們的 indexHandler 函數也有了ServeHTTP方法。

此外,ServeMux的Handle方法,將會對pattern和handler函數做一個map映射:

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    if pattern == "" {
        panic("http: invalid pattern " + pattern)
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if mux.m[pattern].explicit {
        panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

    if pattern[0] != '/' {
        mux.hosts = true
    }

    // Helpful behavior:
    // If pattern is /tree/, insert an implicit permanent redirect for /tree.
    // It can be overridden by an explicit registration.
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // If pattern contains a host name, strip it and use remaining
        // path for redirect.
        path := pattern
        if pattern[0] != '/' {
            // In pattern, at least the last character is a '/', so
            // strings.Index can't be -1.
            path = pattern[strings.Index(pattern, "/"):]
        }
        url := &url.URL{Path: path}
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
    }
}

由此可見,Handle函數的主要目的在於把handler和pattern模式綁定到map[string]muxEntry的map上,其中muxEntry保存了更多pattern和handler的信息,還記得前面討論的Server結構嗎?Server的m字段就是map[string]muxEntry這樣一個map。

此時,pattern和handler的路由註冊完成。接下來就是如何開始server的監聽,以接收客戶端的請求。

開啓監聽

註冊好路由之後,啓動web服務還需要開啓服務器監聽。http的ListenAndServer方法中可以看到創建了一個Server對象,並調用了Server對象的同名方法:


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

func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServeTLS(certFile, keyFile)
}

Server的ListenAndServe方法中,會初始化監聽地址Addr,同時調用Listen方法設置監聽。最後將監聽的TCP對象傳入Serve方法:

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure

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

    srv.trackListener(l, true)
    defer srv.trackListener(l, false)

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(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", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

處理請求

監聽開啓之後,一旦客戶端請求到底,go就開啓一個協程處理請求,主要邏輯都在serve方法之中。

serve方法比較長,其主要職能就是,創建一個上下文對象,然後調用Listener的Accept方法用來 獲取連接數據並使用newConn方法創建連接對象。最後使用goroutein協程的方式處理連接請求。因爲每一個連接都開起了一個協程,請求的上下文都不同,同時又保證了go的高併發。serve也是一個長長的方法:

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

    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 {
            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{tlsConn, serverHandler{c.server}}
                fn(c.server, tlsConn, h)
            }
            return
        }
    }

    // HTTP/1.x from here on.

    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    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)
        }
        if err != nil {
            const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

            if 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
            }
            if isCommonNetReadError(err) {
                return // don't reply
            }

            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 {
            if w.conn.bufr.Buffered() > 0 {
                w.conn.r.closeNotifyFromPipelinedRequest()
            }
            w.conn.r.startBackgroundRead()
        }

        // 一個Http不能同時又多個處理請求,只有服務器響應了當前請求後才能處理其他請求.
        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{})
    }
}

儘管serve很長,裏面的結構和邏輯還是很清晰的,使用defer定義了函數退出時,連接關閉相關的處理。然後就是讀取連接的網絡數據,並處理讀取完畢時候的狀態。接下來就是調用serverHandler{c.server}.ServeHTTP(w, w.req)方法處理請求了。最後就是請求處理完畢的邏輯。serverHandler是一個重要的結構,它近有一個字段,即Server結構,同時它也實現了Handler接口方法ServeHTTP,並在該接口方法中做了一個重要的事情,初始化multiplexer路由多路複用器。如果server對象沒有指定Handler,則使用默認的DefaultServeMux作爲路由Multiplexer。並調用初始化Handler的ServeHTTP方法。

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

這裏DefaultServeMux的ServeHTTP方法其實也是定義在ServeMux結構中的,相關代碼如下:

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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) {

    // CONNECT requests are not canonicalized.
    if r.Method == "CONNECT" {
        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 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.
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

mux的ServeHTTP方法通過調用其Handler方法尋找註冊到路由上的handler函數,並調用該函數的ServeHTTP方法,本例則是IndexHandler函數。

mux的Handler方法對URL簡單的處理,然後調用handler方法,後者會創建一個鎖,同時調用match方法返回一個handler和pattern。

在match方法中,mux的m字段是map[string]muxEntry圖,後者存儲了pattern和handler處理器函數,因此通過迭代m尋找出註冊路由的patten模式與實際url匹配的handler函數並返回。

返回的結構一直傳遞到mux的ServeHTTP方法,接下來調用handler函數的ServeHTTP方法,即IndexHandler函數,然後把response寫到http.RequestWirter對象返回給客戶端。

上述函數運行結束即serverHandler{c.server}.ServeHTTP(w, w.req)運行結束。接下來就是對請求處理完畢之後上希望和連接斷開的相關邏輯。

至此,Golang中一個完整的http服務介紹完畢,包括註冊路由,開啓監聽,處理連接,路由處理函數。

web服務總結

多數的web應用基於HTTP協議,客戶端和服務器通過request-response的方式交互。一個server並不可少的兩部分莫過於路由註冊和連接處理。Golang通過一個ServeMux實現了的multiplexer路由多路複用器來管理路由。同時提供一個Handler接口提供ServeHTTP用來實現handler處理其函數,後者可以處理實際request並構造response。

ServeMux和handler處理器函數的連接橋樑就是Handler接口。ServeMux的ServeHTTP方法實現了尋找註冊路由的handler的函數,並調用該handler的ServeHTTP方法。ServeHTTP方法就是真正處理請求和構造響應的地方。

回顧go的http包實現http服務的流程,可見大師們的編碼設計之功力。學習有利提高自身的代碼邏輯組織能力。更好的學習方式除了閱讀,就是實踐,接下來,我們將着重討論來構建http服務。尤其是構建http中間件函數。

參考 Golang構建HTTP服務(一)--- net/http庫源碼筆記

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章