這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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.Request
和 http.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.Request
和 http.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 複用引發資料競爭
RequestCtx
在 fasthttp
中使用 sync.Pool
複用。在執行完了 RequestHandler
後當前使用的 RequestCtx
就返回池中等下次使用。如果你的商務邏輯有跨 goroutine 使用 RequestCtx
,那可能遇到:同一個 RequestCtx
在 RequestHandler
結束時放回池中,立刻被另一次串連使用;業務 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 文本了,>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
可以看作是一個真好的賣點,值得注意。