【go語言學習】網絡編程之HTTP

一、go中HTTP服務處理流程

超文本傳輸協議(HTTP,Hyper Text Transfer Protocol)是互聯網上應用最為廣泛的一種網絡傳輸協議,所有的WWW文件都必須遵守這個標準。設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。

HTTP 協議從誕生到現在,發展從1.0,1.1到2.0也不斷在進步。除去細節,理解 HTTP 構建的網絡應用只要關注兩個端——客戶端(client)和服務端(server),兩個端的交互來自 client 的 request,以及server端的response。所謂的http服務器,主要在于如何接受 client 的 request,并向client返回response。接收request的過程中,最重要的莫過于路由(router),即實現一個Multiplexer器。

Go中既可以使用內置的 multiplexer - DefaultServeMux,也可以自定義。Multiplexer路由的目的就是為了找到處理器函數(handler),后者將對request進行處理,同時構建response。

二、構建一個簡單的http服務

代碼示例

package main

import "net/http"

func main() {
    // 1.設置路由
    // 訪問"/",調用indexHandleFunc函數處理
    http.HandleFunc("/", indexHandleFunc)
    // 訪問"/home",調用homeHandleFunc函數處理
    http.HandleFunc("/home", homeHandleFunc)
    // 2.開啟監聽
    http.ListenAndServe(":8080", nil)
}

func indexHandleFunc(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("index"))
}
func homeHandleFunc(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("home"))
}

運行程序

訪問http://localhost:8080/,頁面上顯示index
訪問http://localhost:8080/home/,頁面上顯示home

三、深入net/http包理解go語言http

1、http.Server

HTTP 服務器在 Go 語言中是由 http.Server 結構體對象實現的。參考 http.ListenAndServe() 的實現:

// src/net/http/server.go

// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
  server := &Server{Addr: addr, Handler: handler}
  return server.ListenAndServe()
}

可見過程是先實例化 Server 對象,再完成 ListenAndServe 。其中 Serve 對象就是表示 HTTP 服務器的對象。其結構如下 :

// src/net/http/server.go

type Server struct {
    Addr         string        // TCP 監聽地址, 留空為:":http"
    Handler      Handler       // 調用的 handler(路由處理器), 設為 nil 表示 http.DefaultServeMux
    ReadTimeout  time.Duration // 請求超時時長
    WriteTimeout time.Duration // 響應超時時長
    ...
}

實例化了 Server 對象后,調用其 func (srv *Server) ListenAndServe() error 方法。該方法會監聽 srv.Addr 指定的 TCP 地址,并通過 func (srv *Server) Serve(l net.Listener) error 方法接收瀏覽器端連接請求。Serve 方法會接收監聽器 l 收到的每一個連接,并為每一個連接創建一個新的服務協程goroutine。

該 go 協程會讀取請求,然后調用 srv.Handler 處理并響應。srv.Handler 通常會是 nil,這意味著需要調用 http.DefaultServeMux 來處理請求,這個 DefaultServeMux 是默認的路由,我們使用 http.HandleFunc 就是在 http.DefaultServeMux 上注冊方法。

看一下詳細過程(不完全摘錄go源碼):

step1:

初始化一個Server結構體實例后, 執行Server.ListenAndServer函數(其中主要調用net.Listen 和 Server.Serve函數)

func (srv *Server) ListenAndServe() error {
    // 調用net.Listen方法啟用監聽
    ln, err := net.Listen("tcp", addr)
    // 調用Server對象的Serve方法,將監聽對象作為參數傳入
    return srv.Serve(ln)
}

step2:

接著進入Server.Serve部分的代碼,看看詳細的處理過程,可以看到主要是在主goroutine中用for循環阻塞,不斷通過Accept函數讀取連接請求,然后調用Server.newConn()函數,創建一個連接實例c,然后每個連接實例啟動一個新的goroutine去執行處理,從這里也能看出, 如果并發請求很高的時候,會創建出海量的goroutine來并發處理請求。

這一步,調用newConn函數,會創建一個conn結構體的連接實例,其中的server成員變量是Server實例,即每個connection實例都保留了指向server的信息。

func (srv *Server) Serve(l net.Listener) error {
    for {
        // 循環讀取連接請求
        rw, err := l.Accept()
        // 創建一個新的連接實例 c := &conn{server: srv, rwc: rw}
        c := srv.newConn(rw)
        // 啟動一個新的goroutine去執行處理
        go c.serve(connCtx)
    }
}

step3

接著進入每個connection處理的goroutine,看具體做了什么工作:先調用connectionreadRequest方法構造一個response對象, 然后執行serverHandler{c.server}.ServeHTTP(w, w.req)進行實際的請求處理。

func (c *conn) serve(ctx context.Context) {
    for{
        ...
        // 調用connection的readRequest函數構造一個response對象
        w, err := c.readRequest(ctx)
        ...
        req := w.req
        ...
        // serverHandler{c.server}.ServeHTTP(w, w.req)進行實際的請求處理
        serverHandler{c.server}.ServeHTTP(w, w.req)
    }
}

step4

serverHandler是一個結構體,有一個成員屬性srv *Server,結合step 3可以看出,serverHandler{c.server}.ServeHTTP(w, w.req)就是實例化了一個serverHandler結構體,然后執行其成員方法 ServeHTTP,在ServeHTTP成員方法的定義中,可以看到,主要是調用了初始化server時的Handler成員方法,執行handler.ServeHTTP(rw, req)進行調用。

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)
}
2、http.Handler
// Handler 接口
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)

任何結構體,只要實現了ServeHTTP方法,這個結構就可以稱之為Handler對象。

代碼示例:

package main

import (
    "net/http"
)

type a struct{}

func (*a) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello world"))
}

func main() {
    // 從go的源碼中可以發現函數的第二個參數是一個Handler對象,a實現了Handler的方法
    // a就可以看做是一個Handler對象傳入。
    http.ListenAndServe(":8080", &a{})
}

運行程序

訪問http://localhost:8080/,頁面上顯示hello world

3、http.HandleFunc

http.HandleFunc注冊路由,從源碼中可以看到,http.HandleFunc的功能是向http包的DefaultServeMux加入新的處理路由。

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

DefaultServeMux 是一個 ServeMux 結構體的實例。

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

var defaultServeMux ServeMux
5、http.ServeMux

ServeMux是一個結構體,其中有個m屬性是一個map類型,存放了不同 URL pattern 的處理Handler。同時ServeMux 也實現了 ServeHTTP 函數,是 http.Handler 接口的實現。

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

type muxEntry struct {
    h       Handler
    pattern string
}

// ServeMux實現了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)
}

DefaultServeMux 調用 ServeMuxHandleFunc 方法

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

接著調用 ServeMuxHandle 方法向 ServeMux 結構體的 m 字段寫入信息 map[pattern] = muxEntry{h: handler, pattern: pattern}

func (mux *ServeMux) Handle(pattern string, handler Handler) {}

四、go語言http的其他實現方式

1、自定義server

創建一個Server對象,可以設置server的其他參數。

package main

import (
    "net/http"
    "time"
)

type a struct{}

func (*a) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello world"))
}

func main() {
    // 創建一個新的Server對象
    s := http.Server{
        // 監聽端口
        Addr: "localhost:8080",
        // 處理路由
        Handler: &a{},
        // 設置超時
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }
    s.ListenAndServe()
}
2、使用自定義的serveMux
package main

import (
    "net/http"
)

type a struct{}

func (*a) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello world"))
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("index"))
}
func page(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("page"))
}

func main() {
    mux := http.NewServeMux()
    // 通過Handle將a這個實現Handler接口的結構體,注冊到map表中
    mux.Handle("/", &a{})
    //通過HandleFunc方法去向mux中handler中注冊處理函數
    //其底層仍然是通過mux.Handle方法實現的
    mux.HandleFunc("/index", index)
    // 直接調用Handle方法去注冊處理函數,
    // 這里的http.HandleFunc 是一個函數類型,這里將page轉換為一個Handler接口的實現
    mux.Handle("/page", http.HandlerFunc(page))
    mux.HandleFunc("/home", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("home"))
    })

    http.ListenAndServe("localhost:8080", mux)
}

五、http的執行過程

1、傳入自己實現Handler接口的對象

http.ListenAndServe("localhost:8080", &a{}) 第二個參數傳入的是自己實現Handler接口的對象,http的server會調用這個對象的 ServeHTTP(w, r) 成員方法,實現 requestresponse

示例代碼:

package main

import (
    "fmt"
    "net/http"
)

type a struct{}

func (*a) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.URL.Path)
    switch r.URL.Path {
    case "/":
        w.Write([]byte("<h1>hello world</h1>"))
    case "/index":
        w.Write([]byte("<h1>index</h1>"))
    case "/home":
        w.Write([]byte(`<div style="color:red;font-size: 20px;">This is home page</div>`))
    default:
        w.Write([]byte(`<h1 style="color:red">404 NOT FOUND</h1>`))
    }
}

func main() {
    http.ListenAndServe("localhost:8080", &a{})
}
2、傳入自定義的ServeMux或默認的DefaultServeMux

http.ListenAndServe("localhost:8080", mux) 第二個參數傳入的是自定義的ServeMux或默認的DefaultServeMux(就是傳入 nil ),http的server會調用ServeMux的 ServeHTTP(w, r) 成員方法,實現 requestresponse ,詳細步驟如下(不完全摘錄go源碼):

step1:
調用 ServeHTTP(w, r){} 實現 requestresponse ,先調用 mux.Handler(r) 方法返回一個Handler對象,調用其 ServeHTTP(w, r) 方法

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

step2:在調用 mux.Handler(r *Request) 方法時,是接著調用
mux.handler(host, r.URL.Path) 方法

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    return mux.handler(host, r.URL.Path)
}

step3:在調用 mux.handler(host, path string) 方法,是接著調用mux.match(path) 方法

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
}

step4: 在調用 mux.match(path) 方法時,返回 mux.m[path].h 一個Handler對象,其實就是注冊的路由器處理函數,并且和 r.URL.Path 相匹配,返回的 mux.m[path].pattern 就是路由地址 r.URL.Path

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }
    return nil, ""
}

step5:回到step1調用 mux.m[path].hServeHTTP(w, r) 方法。

參考文章
https://my.oschina.net/u/943306/blog/151293
https://studygolang.com/articles/16179

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374