文档章节

Go 开发 HTTP 的另一个选择 fasthttp

傅小黑
 傅小黑
发布于 2016/09/30 22:03
字数 1868
阅读 505
收藏 3

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

<!--more-->

路由

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.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.ResponseAPI 文档,有很多方法可以简化操作。

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 网络并发编程的主要痛点做了很多工作,达到了很好的效果。目前,irisecho 支持 fasthttp,性能上和使用 net/http 的别的 Web 框架对比有明显的优势。如果选择 Web 框架,支持 fasthttp 可以看作是一个真好的卖点,值得注意。

© 著作权归作者所有

共有 人打赏支持
傅小黑
粉丝 154
博文 7
码字总数 13850
作品 1
厦门
高级程序员
私信 提问
Gem —— Go 语言编写的 Web 框架

Gem 是一个用 Go(golang)语言编写的简单而又快速的 Web 框架,用于构建 restful 或 web 应用,基于 fasthttp。

达尔文
2016/12/04
44
0
在Github中stars数最多的Go Web框架集合

Project Name Stars Forks Description beego 12195 2810 beego is an open-source, high-performance web framework for the Go programming language. gin 11602 1357 Gin is a HTTP web f......

终于19岁
2017/09/07
0
0
Hprose for Go 2.0.0 发布,微服务首选引擎

Hprose 2.0 for Golang 终于发布了。这是一个里程碑版本,针对开发者进行了多项改进。 Hprose 2.0 for Golang 新增了许多特征: 更快更稳定的序列化。 增加了数据推送的支持。 oneway 调用支...

andot
2016/10/31
3.6K
18
用wrk测试nginx/nodejs/golang

硬件环境 被测试服务端均只返回“hello world”的body体。 客户端使用wrk,在本机测试。 第一名:wrk -> go-fasthttp: 40w TPS 第二名:wrk -> nginx(openresty+echo-nginx-module): 29w TP...

chuqq
2017/11/16
0
0
开源 | Service Mesh 数据平面 SOFAMosn 深层揭秘

小蚂蚁说: 本文是基于作者在 Service Mesh Meetup #2 北京的主题分享《蚂蚁金服 Service Mesh 数据平面 SOFAMosn 深层解密》部分内容所整理,完整内容见文末的直播回放。 本文作者:朵晓东,...

兔子酱
2018/08/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

看过上百部片子的这个人教你视频标签算法解析

本文由云+社区发表 随着内容时代的来临,多媒体信息,特别是视频信息的分析和理解需求,如图像分类、图像打标签、视频处理等等,变得越发迫切。目前图像分类已经发展了多年,在一定条件下已经...

腾讯云加社区
36分钟前
2
0
2. 红黑树

定义:红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树(Binary Search Tree)。 要理解红黑树,先要了解什么是二叉查找树。在上一章中,我们学习了什么是二叉树,以及二叉树...

火拳-艾斯
37分钟前
2
0
input的button类型,点击页面跳转

一、input type=button 不做任何操作 例如: <input type="button" class="btn btn-primary" style="width: 30%" value="返回" onclick="window.location.href='/users/list'"></input> onc......

Sunki
43分钟前
1
0
踩坑:js 小数运算出现精度问题

背景 在学习小程序商城源码时发现了这个问题,单价可能出现小数,小数之间运算结果会莫名其妙多出一大串数字,比如下面这样👇。 在此之前我是知道 js 中著名的 0.1 + 0.2 != 0.3 的问题的,...

dkvirus
48分钟前
2
0
zookeeper和HBASE总结

zookeeper快速上手 zookeeper的基本功能和应用场景 zookeeper的整体运行机制 zookeeper的数据存储机制 数据存储形式 zookeeper中对用户的数据采用kv形式存储 只是zk有点特别: key:是以路径...

瑞查德-Jack
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部