Go 開發 HTTP 的另一個選擇 fasthttp

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

fasthttp 是 Go 的一款不同於標準庫 net/http 的 HTTP 實現。fasthttp 的效能可以達到標準庫的 10 倍,說明他魔性的實現方式。主要的點在於四個方面:

  • net/http 的實現是一個串連建立一個 goroutine;fasthttp 是利用一個 worker 複用 goroutine,減輕 runtime 調度 goroutine 的壓力
  • net/http 解析的請求資料很多放在 map[string]string(http.Header) 或 map[string][]string(http.Request.Form),有不必要的 []byte 到 string 的轉換,是可以規避的
  • net/http 解析 HTTP 要求每次產生新的 *http.Requesthttp.ResponseWriter; fasthttp 解析 HTTP 資料到 *fasthttp.RequestCtx,然後使用 sync.Pool 複用結構執行個體,減少對象的數量
  • fasthttp 會延遲解析 HTTP 要求中的資料,尤其是 Body 部分。這樣節省了很多不直接操作 Body 的情況的消耗

但是因為 fasthttp 的實現與標準庫差距較大,所以 API 的設計完全不同。使用時既需要理解 HTTP 的處理過程,又需要注意和標準庫的差別。

package mainimport (    "fmt"    "github.com/valyala/fasthttp")// RequestHandler 類型,使用 RequestCtx 傳遞 HTTP 的資料func httpHandle(ctx *fasthttp.RequestCtx) {    fmt.Fprintf(ctx, "hello fasthttp") // *RequestCtx 實現了 io.Writer}func main() {    // 一定要寫 httpHandle,否則會有 nil pointer 的錯誤,沒有處理 HTTP 資料的函數    if err := fasthttp.ListenAndServe("0.0.0.0:12345", httpHandle); err != nil {        fmt.Println("start fasthttp fail:", err.Error())    }}

路由

net/http 提供 http.ServeMux 實現路由服務,但是匹配規則簡陋,功能很簡單,基本不會使用。fasthttp 吸取教訓,預設沒有提供路由支援。因此使用第三方的 fasthttp 的路由庫 fasthttprouter 來輔助路由實現:

package mainimport (    "fmt"    "github.com/buaazp/fasthttprouter"    "github.com/valyala/fasthttp")// fasthttprouter.Params 是路由匹配得到的參數,如規則 /hello/:name 中的 :namefunc httpHandle(ctx *fasthttp.RequestCtx, _ fasthttprouter.Params) {    fmt.Fprintf(ctx, "hello fasthttp")}func main() {    // 使用 fasthttprouter 建立路由    router := fasthttprouter.New()    router.GET("/", httpHandle)    if err := fasthttp.ListenAndServe("0.0.0.0:12345", router.Handler); err != nil {        fmt.Println("start fasthttp fail:", err.Error())    }}

RequestCtx 操作

*RequestCtx 綜合 http.Requesthttp.ResponseWriter 的操作,可以更方便的讀取和返回資料。

首先,一個請求的基本資料是必然有的:

func httpHandle(ctx *fasthttp.RequestCtx) {    ctx.SetContentType("text/html") // 記得添加 Content-Type:text/html,否則都當純文字返回    fmt.Fprintf(ctx, "Method:%s <br/>", ctx.Method())    fmt.Fprintf(ctx, "URI:%s <br/>", ctx.URI())    fmt.Fprintf(ctx, "RemoteAddr:%s <br/>", ctx.RemoteAddr())    fmt.Fprintf(ctx, "UserAgent:%s <br/>", ctx.UserAgent())    fmt.Fprintf(ctx, "Header.Accept:%s <br/>", ctx.Request.Header.Peek("Accept"))}

fasthttp 還添加很多更方便的方法讀取基本資料,如:

func httpHandle(ctx *fasthttp.RequestCtx) {    ctx.SetContentType("text/html")    fmt.Fprintf(ctx, "IP:%s <br/>", ctx.RemoteIP())    fmt.Fprintf(ctx, "Host:%s <br/>", ctx.Host())    fmt.Fprintf(ctx, "ConnectTime:%s <br/>", ctx.ConnTime()) // 串連收到處理的時間    fmt.Fprintf(ctx, "IsGET:%v <br/>", ctx.IsGet())          // 類似有 IsPOST, IsPUT 等}

更詳細的 API 可以閱讀 godoc.org。

表單資料

RequestCtx 有同標準庫的 FormValue() 方法,還對 GET 和 POST/PUT 傳遞的參數進行了區分:

func httpHandle(ctx *fasthttp.RequestCtx) {    ctx.SetContentType("text/html")    // GET ?abc=abc&abc=123    getValues := ctx.QueryArgs()    fmt.Fprintf(ctx, "GET abc=%s <br/>",        getValues.Peek("abc")) // Peek 只擷取第一個值    fmt.Fprintf(ctx, "GET abc=%s <br/>",        bytes.Join(getValues.PeekMulti("abc"), []byte(","))) // PeekMulti 擷取所有值    // POST xyz=xyz&xyz=123    postValues := ctx.PostArgs()    fmt.Fprintf(ctx, "POST xyz=%s <br/>",        postValues.Peek("xyz"))    fmt.Fprintf(ctx, "POST xyz=%s <br/>",        bytes.Join(postValues.PeekMulti("xyz"), []byte(",")))}

可以看到輸出結果:

GET abc=abc GET abc=abc,123 POST xyz=xyz POST xyz=xyz,123 
Body 訊息體

fasthttp 提供比標準庫豐富的 Body 操作 API,而且支援解析 Gzip 過的資料:

func httpHandle(ctx *fasthttp.RequestCtx) {    body := ctx.PostBody() // 擷取到的是 []byte    fmt.Fprintf(ctx, "Body:%s", body)    // 因為是 []byte,解析 JSON 很簡單    var v interface{}    json.Unmarshal(body,&v)}func httpHandle2(ctx *fasthttp.RequestCtx) {    ungzipBody, err := ctx.Request.BodyGunzip()    if err != nil {        ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)        return    }    fmt.Fprintf(ctx, "Ungzip Body:%s", ungzipBody)}
上傳檔案

fasthttp 對檔案上傳的部分沒有做大修改,使用和 net/http 一樣:

func httpHandle(ctx *fasthttp.RequestCtx) {    // 這裡直接擷取到 multipart.FileHeader, 需要手動開啟檔案控制代碼    f, err := ctx.FormFile("file")    if err != nil {        ctx.SetStatusCode(500)        fmt.Println("get upload file error:", err)        return    }    fh, err := f.Open()    if err != nil {        fmt.Println("open upload file error:", err)        ctx.SetStatusCode(500)        return    }    defer fh.Close() // 記得要關    // 開啟儲存檔案控制代碼    fp, err := os.OpenFile("saveto.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)    if err != nil {        fmt.Println("open saving file error:", err)        ctx.SetStatusCode(500)        return    }    defer fp.Close() // 記得要關    if _, err = io.Copy(fp, fh); err != nil {        fmt.Println("save upload file error:", err)        ctx.SetStatusCode(500)        return    }    ctx.Write([]byte("save file successfully!"))}

上面的操作可以對比我寫的上一篇文章 Go 開發 HTTP,非常類似。多檔案上傳同樣使用 *RequestCtx.MultipartForm() 擷取到整個表單內容,各個檔案處理就可以。

返回內容

不像 http.ResponseWriter 那麼簡單,*RequestCtx*RequestCtx.Response 提供了豐富的 API 為 HTTP 返回資料:

func httpHandle(ctx *fasthttp.RequestCtx) {    ctx.WriteString("hello,fasthttp")    // 因為實現不同,fasthttp 的返回內容不是即刻返回的    // 不同於標準庫,添加返回內容後設定狀態代碼,也是有效    ctx.SetStatusCode(404)    // 返回的內容也是可以擷取的,不需要標準庫的用法,需要自己擴充 http.ResponseWriter    fmt.Println(string(ctx.Response.Body()))}

下載檔案也有直接的方法:

func httpHandle(ctx *fasthttp.RequestCtx) {    ctx.SendFile("abc.txt")}

可以閱讀 fasthttp.Response 的 API 文檔,有很多方法可以簡化操作。

RequestCtx 複用引發資料競爭

RequestCtxfasthttp 中使用 sync.Pool 複用。在執行完了 RequestHandler 後當前使用的 RequestCtx 就返回池中等下次使用。如果你的商務邏輯有跨 goroutine 使用 RequestCtx,那可能遇到:同一個 RequestCtxRequestHandler 結束時放回池中,立刻被另一次串連使用;業務 goroutine 還在使用這個 RequestCtx,讀取的資料發生變化。

為瞭解決這種情況,一種方式是給這次請求處理設定 timeout ,保證 RequestCtx 的使用時 RequestHandler 沒有結束:

func httpHandle(ctx *fasthttp.RequestCtx) {    resCh := make(chan string, 1)    go func() {        // 這裡使用 ctx 參與到耗時的邏輯中        time.Sleep(5 * time.Second)        resCh <- string(ctx.FormValue("abc"))    }()    // RequestHandler 阻塞,等著 ctx 用完或者逾時    select {    case <-time.After(1 * time.Second):        ctx.TimeoutError("timeout")    case r := <-resCh:        ctx.WriteString("get: abc = " + r)    }}

還提供 fasthttp.TimeoutHandler 協助封裝這類操作。

另一個角度,fasthttp 不推薦複製 RequestCtx。但是根據業務思考,如果只是收到請求資料立即返回,後續處理資料的情況,複製 RequestCtx.Request 是可以的,因此也可以使用:

func httpHandle(ctx *fasthttp.RequestCtx) {    var req fasthttp.Request    ctx.Request.CopyTo(&req)    go func() {        time.Sleep(5 * time.Second)        fmt.Println("GET abc=" + string(req.URI().QueryArgs().Peek("abc")))    }()    ctx.WriteString("hello fasthttp")}

需要注意 RequestCtx.Response 也是可以 Response.CopyTo 複製的。但是如果 RequestHandler 結束,RequestCtx.Response 肯定已發出返回內容。在別的 goroutine 修改複製的 Response,沒有作用的。

BytesBuffer

fasthttp 用了很多特殊的最佳化技巧來提高效能。一些方法也暴露出來可以使用,比如重用的 Bytes:

func httpHandle(ctx *fasthttp.RequestCtx) {    b := fasthttp.AcquireByteBuffer()    b.B = append(b.B, "Hello "...)    // 這裡是編碼過的 HTML 文本了,&gt;strong 等    b.B = fasthttp.AppendHTMLEscape(b.B, "<strong>World</strong>")    defer fasthttp.ReleaseByteBuffer(b) // 記得釋放    ctx.Write(b.B)}

原理就是簡單的把 []byte 作為複用的內容在池中存取。對於非常頻繁存取 BytesBuffer 的情況,可能同一個 []byte 不停地被使用 append,而頻繁存取導致沒有空閑時刻,[]byte 無法得到釋放,使用時需要注意一點。

fasthttp 的不足

兩個比較大的不足:

  • HTTP/2.0 不支援
  • WebSocket 不支援

嚴格來說 Websocket 通過 Hijack() 是可以支援的,但是 fasthttp 想自己提供直接操作的 API。那還需要等待開發。

總結

比較標準庫的粗獷,fasthttp 有更精細的設計,對 Go 網路並發編程的主要痛點做了很多工作,達到了很好的效果。目前,iris 和 echo 支援 fasthttp,效能上和使用 net/http 的別的 Web 架構對比有明顯的優勢。如果選擇 Web 架構,支援 fasthttp 可以看作是一個真好的賣點,值得注意。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.