https://github.com/valyala/fasthttp
是 golang 中一个标志性的高性能 HTTP库, 主要用于 webserver 开发, 以及 web client / proxy 等.
Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http
Fast HTTP implementation for Go.
Currently fasthttp is successfully used by VertaMedia in a production serving up to 200K rps from more than 1.5M concurrent keep-alive connections per physical server.
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 的处理过程,又需要注意和标准库的差别。
1.1 HTTP 1.x 协议简述
简单来说, HTTP 1.x 协议, 是一个被动式短连接的 client (请求 request ) - server ( 响应 response) 交互的规范:
协议一般来说, 以 TCP 通讯协议为基础 ( 不谈 QUIC 这个以 udp 为底层实现的变异)
web client 通过 DNS 把域名转换成 IP 地址后, 与 web server 握手连接, 连接成功后, web client 客户端向 web server 服务端发出请求, 服务端收到请求后, 向 client 客户端应答
通过 URL / URI 进行导址, 同时, URL/URI 中包含部分数据
URL 形式如 http://192.168.1.1:8088/rpc/schedule 其中 http://192.168.1.1:8080 这部分是通讯协议, 服务器 IP 地址与端口号, 这是前面 TCP 通讯的依据
web 服务器端在 http://192.168.1.1:8080 这个地址上监听, 随时准备接收 web client 请求并应答
web 客户端通过 http://192.168.1.1:8080 这个地址所指定的 web 服务器进行 tcp 连接, 连接成功后, web 客户端向服务器发出 请求数据, web 服务端应答 响应数据
特别注意, 请求数据, 与响应数据, 遵从 HTTP 协议规定的统一格式
在 HTTP 1.x 协议中规定的传输( 请求/应答) 数据格式, 一般称为 HyperText, 是一种文本数据格式, 当然了, 在 TCP 传输时还是二进制数据块 ( 这是使用 fasthttp 的关键点) . 具体数据格式见 1.2 小节
HTTP 协议规定了一些信令, 如下描述, 来区分不同的交互操作
根据HTTP标准,HTTP请求可以使用多种请求方法:
HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
由于 HTTP 协议相关的 MIME 规范, HTTP 1.x 也可以传输图像/音乐/视频等其他数据格式,但这些被传输的真正有效数据都被封装在 http payload 这一部分里, http header 还保留( 只是字段多少, 以及字段中的值不同) ———这是另一个与 fasthttp 关联的另一个要点
HTTP 1.x 几个基础点:
HTTP 1.x 通过 tcp 进行通讯
请求与响应的格式, 数据数据的格式是一样的
特别注意请求数据中的第一行,第二行 特别注意 HTTP header 与 HTTP payload 的那空行分隔
注意 URL/URI 中也包含有数据, 换个话说,在 http://192.168.1.1:3001/schedule?user_id=eq.2098735545843717147 中, 其他部分 /schedule?user_id=eq.2098735545843717147 看做请求数据的一部分
从 HTTP 1.x 协议, 可以总结 web 开发的要点
处理 tcp 通讯, 包括:
通过 dns 转化域名得到 IP 地址, 包括 ip4 / ip6 地址
对 tcp 进行通讯重写或优化, 长连接或短连接, 都在这里了
或对 tcp 进行转发 ( 这是 proxy ) 或劫持, 在 tcp 通讯最底层进行一些特殊操作
对 URL /URI 进行处理, 这是路由寻址
按 URI 及相关数据特征进行拦截处理, 这是反向代理与缓存
进行一些 URI 转换, 例如 302 的重定向
在 URI 中携带小部分数据的组装与处理
HTTP 数据处理
对 HTTP header / HTTP payload 进行处理, 这是变化最多的部分, 按业务/功能的不同, 即简单也复杂
fasthttp 的性能优化思路
重写了在 tcp 之上进行 HTTP 握手/连接/通讯的 goroutine pool 实现
对 http 数据基本按传输时的二进制进行延迟处理, 交由开发者按需决定
对二进制的数据进行了缓存池处理, 需要开发者手工处理以达到零内存分配
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 main
import (
“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 main
import (
“fmt”
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp" )
// fasthttprouter.Params 是路由匹配得到的参数,如规则 /hello/:name 中的 :name
func 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
”, ctx.Method())
fmt.Fprintf(ctx, “URI:%s
”, ctx.URI())
fmt.Fprintf(ctx, “RemoteAddr:%s
”, ctx.RemoteAddr())
fmt.Fprintf(ctx, “UserAgent:%s
”, ctx.UserAgent())
fmt.Fprintf(ctx, “Header.Accept:%s
”, ctx.Request.Header.Peek(“Accept”))
}
fasthttp 还添加很多更方便的方法读取基本数据,如:
func httpHandle(ctx *fasthttp.RequestCtx) {
ctx.SetContentType(“text/html”)
fmt.Fprintf(ctx, “IP:%s
”, ctx.RemoteIP())
fmt.Fprintf(ctx, “Host:%s
”, ctx.Host())
fmt.Fprintf(ctx, “ConnectTime:%s
”, ctx.ConnTime()) // 连接收到处理的时间
fmt.Fprintf(ctx, “IsGET:%v
”, 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, “World”)
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 可以看作是一个真好的卖点,值得注意。