让你的异步 io 库插上 http1.1 解析的翅膀。 httparser 来也。

2021-02-01 11:34:50 +08:00
 guonaihong

httparser

高性能 http 1.1 解析器,为你的异步 io 库插上解析的翅膀,目前每秒可以处理 300MB/s 流量[从零实现]

仓库位置

https://github.com/antlabs/httparser

出发点

本来想基于异步 io 库写些好玩的代码,发现没有适用于这些库的 http 解析库,索性就自己写个,弥补 golang 生态一小片空白领域。

特性

parser request

	var data = []byte(
		"POST /joyent/http-parser HTTP/1.1\r\n" +
			"Host: github.com\r\n" +
			"DNT: 1\r\n" +
			"Accept-Encoding: gzip, deflate, sdch\r\n" +
			"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n" +
			"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) " +
			"AppleWebKit/537.36 (KHTML, like Gecko) " +
			"Chrome/39.0.2171.65 Safari/537.36\r\n" +
			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9," +
			"image/webp,*/*;q=0.8\r\n" +
			"Referer: https://github.com/joyent/http-parser\r\n" +
			"Connection: keep-alive\r\n" +
			"Transfer-Encoding: chunked\r\n" +
			"Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n")

	var setting = httparser.Setting{
		MessageBegin: func() {
			//解析器开始工作
			fmt.Printf("begin\n")
		},
		URL: func(buf []byte) {
			//url 数据
			fmt.Printf("url->%s\n", buf)
		},
		Status: func([]byte) {
			// 响应包才需要用到
		},
		HeaderField: func(buf []byte) {
			// http header field
			fmt.Printf("header field:%s\n", buf)
		},
		HeaderValue: func(buf []byte) {
			// http header value
			fmt.Printf("header value:%s\n", buf)
		},
		HeadersComplete: func() {
			// http header 解析结束
			fmt.Printf("header complete\n")
		},
		Body: func(buf []byte) {
			fmt.Printf("%s", buf)
			// Content-Length 或者 chunked 数据包
		},
		MessageComplete: func() {
			// 消息解析结束
			fmt.Printf("\n")
		},
	}

	p := httparser.New( httparser.REQUEST)
	success, err := p.Execute(&setting, data)

	fmt.Printf("success:%d, err:%v\n", success, err)

response

response

request or response

如果你不确定数据包是请求还是响应,可看下面的例子
request or response

编译

生成 unhex 表和 tokens 表

如果需要修改这两个表,可以到_cmd 目录下面修改生成代码的代码

make gen

编译 example

make example

运行示例

make example.run

return value

吞吐量

2097 次点击
所在节点    Go 编程语言
25 条回复
keepeye
2021-02-01 11:42:20 +08:00
先 star 了,虽然还不知道应用场景
shyling
2021-02-01 11:57:09 +08:00
有木有和别的 http_parser 的性能对比
oxromantic
2021-02-01 12:55:08 +08:00
既然是 http 1.1 了,必须要支持连接复用的数据吧
abersheeran
2021-02-01 13:27:29 +08:00
@oxromantic 这个看起来是不带实际 IO 实现的,复用链接需要自己处理。
Ib3b
2021-02-01 14:30:42 +08:00
解析不都是计算型的吗?异不异步有区别?
guonaihong
2021-02-01 15:05:56 +08:00
@shyling 标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。
julyclyde
2021-02-01 15:38:22 +08:00
@guonaihong 那我觉得你应该直接去把标准库改掉啊
lesismal
2021-02-01 15:42:59 +08:00
大概看了下,不确定是否准确:
1. "粘包"可能有问题,不只是一个包可能拆成多段被应用层分多次读取到,也可能是多个包的数据放一块、被应用层从任意中间位置分多次读取到,比如 3 个包被两次读到、两次分别读到前 1.5 个和后 1.5 个包
2. 好像只是解析一个完整包的功能,并没有返回一个 Request/Response 类似的结构,所以 header 、body 之类的还是要业务层自己解析一道,这样的话业务层仍需要重复解析一次长度相关、比较浪费

建议也解析 header 、body 相关内容,一个完整包解析完之后返回一个 Request/Response 给业务层处理,在这基础之上 parser 内置 buf 的缓存,一个段落或者一个完整包后剩余的 half 部分由 parse 自己存上,有新数据来了加一块继续解析,这样业务层不必通过 success 再截断数据跟下次数据放一块,也免去重复解析 half 的浪费
lesismal
2021-02-01 15:44:31 +08:00
还想要 TLS 之类的支持,都搞细搞全了,也是个大工程。。。
我之前也想写一份 httpparser 来着,细想了下,没时间,放弃了。。。
guonaihong
2021-02-01 15:46:30 +08:00
@lesismal 设计的时候支持分段传入,内部是一个状态机。
lesismal
2021-02-01 15:51:43 +08:00
@guonaihong "标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。" —— 这么说不太公平,标准库的是返回了 Request 、url header body 各段落字段都做了解析的
lesismal
2021-02-01 15:56:18 +08:00
@guonaihong “设计的时候支持分段传入,内部是一个状态机。”—— 试一下一次读 1.5 个包的内容

var data = []byte(
"POST /joyent/http-parser HTTP/1.1\r\n" +
"Host: github.com\r\n" +
"DNT: 1\r\n" +
"Accept-Encoding: gzip, deflate, sdch\r\n" +
"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n" +
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/39.0.2171.65 Safari/537.36\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9," +
"image/webp,*/*;q=0.8\r\n" +
"Referer: https://github.com/joyent/http-parser\r\n" +
"Connection: keep-alive\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n" +

"POST /joyent/http-parser HTTP/1.1\r\n" +
"Host: github.com\r\n" +
"DNT: 1\r\n" +
"Accept-Encoding: gzip, deflate, sdch\r\n" +
"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n" +
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/39.0.2171.65 Safari/537.36\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9," +
"image/webp,*/*;q=0.8\r\n" +
"Referer: https://github.com/joyent/http-parser\r\n" +
"Connection: keep-alive\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n")

p := httparser.New( httparser.REQUEST)
fmt.Printf("req_len=%d\n", len(data)/2)
data1, data2 := data[:600], data[600:]
sucess, err := p.Execute(&setting, data1)
if err != nil {
panic(err.Error())
}
if sucess != len(data1) {
panic(fmt.Sprintf("sucess 111 length size:%d", sucess))
}

sucess, err = p.Execute(&setting, data2)
if err != nil {
panic(err.Error())
}
if sucess != len(data2) {
panic(fmt.Sprintf("sucess 222 length size:%d", sucess))
}

p.Reset()
lesismal
2021-02-01 15:57:24 +08:00
我尝试了上一楼的 1.5 个包,没法返回单个包给业务层。算是 bug
lesismal
2021-02-01 15:59:08 +08:00
只是解析出一个个包、不解析包内各段落具体字段相对简单,但是对实际工程帮助也不大,所以离工程使用还有很长距离
guonaihong
2021-02-01 16:01:06 +08:00
@lesismal 。。。? httparser 也返回了各 header 字段。以及 body or chunked body 。
我不知道你开火的焦点是?如果是数据没有返回,答:都返回了。
lesismal
2021-02-01 16:18:39 +08:00
@guonaihong 楼主先淡定点,不是开火的意思

我说没返回是指标准库返回了完整的 Request 结构体,Request 内已经把 URL/Header 各字段之类的解析好了,楼主的 httpparser 虽然 setting 里可以设置回调,但也是业务层自己需要二次加工,如果是对比性能,标准库相当于比你默认的 bench 代码多做了每个字段的解析,这样 bench 对比对标准库是不公平的

另外 1.5 个包的问题,比如我在 12 楼的测试代码,两个 http post 的数据,第一次发 1.5 个,第二次发剩下的 1.5,比如 setting 的回调这样:
var setting = httparser.Setting{
MessageBegin: func() {
fmt.Println("---- begin")
},
HeadersComplete: func() {
fmt.Println("---- complete")
},
}

只打印了一组
---- begin
---- complete

我没有去做更完整的测试和调试、不敢确定,提出来你看下算不算 bug,如果我看错了你解释就好了

技术交流,心态平和,需要豁达,不要火大 ^_^
guonaihong
2021-02-01 16:18:55 +08:00
@lesismal 你的用法,和我的设计还不一样,我一开始的方案,是一个 Request 包解析完成之后,手动调用下 Reset()。所以不调用 Reset()。第二个 Request 包是不解析的,这时候对于解析器是 MessageDone 的状态。这块可以再优化下使用体验。

从打印你也可以看到,哪怕是粘包,第一个 Request 也是完整的拿出来了。
lesismal
2021-02-01 16:20:15 +08:00
上一楼打错字,"第二次发剩下的 1.5" 应该是 "第二次发剩下的 0.5"
guonaihong
2021-02-01 16:21:51 +08:00
@lesismal 我觉得你和我讨论技术是挺好的,这块可以放到 github issue 上面。
lesismal
2021-02-01 16:22:37 +08:00
@guonaihong 你试下我 12 楼和 16 楼的代码,两个 Post,我这里测,只打印了一组 begin/complete,不知道是不是我测试代码写错了,如果写错了楼主给指正下我再试试,如果没写错应该算是丢了个请求

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/750218

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX