高性能 http 1.1 解析器,为你的异步 io 库插上解析的翅膀,目前每秒可以处理 300MB/s 流量[从零实现]
https://github.com/antlabs/httparser
本来想基于异步 io 库写些好玩的代码,发现没有适用于这些库的 http 解析库,索性就自己写个,弥补 golang 生态一小片空白领域。
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)
如果你不确定数据包是请求还是响应,可看下面的例子
request or response
如果需要修改这两个表,可以到_cmd 目录下面修改生成代码的代码
make gen
make example
make example.run
1
keepeye 2021-02-01 11:42:20 +08:00
先 star 了,虽然还不知道应用场景
|
2
shyling 2021-02-01 11:57:09 +08:00
有木有和别的 http_parser 的性能对比
|
3
oxromantic 2021-02-01 12:55:08 +08:00
既然是 http 1.1 了,必须要支持连接复用的数据吧
|
4
abersheeran 2021-02-01 13:27:29 +08:00
@oxromantic 这个看起来是不带实际 IO 实现的,复用链接需要自己处理。
|
5
Ib3b 2021-02-01 14:30:42 +08:00
解析不都是计算型的吗?异不异步有区别?
|
6
guonaihong OP @shyling 标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。
|
7
julyclyde 2021-02-01 15:38:22 +08:00
@guonaihong 那我觉得你应该直接去把标准库改掉啊
|
8
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 的浪费 |
9
lesismal 2021-02-01 15:44:31 +08:00
还想要 TLS 之类的支持,都搞细搞全了,也是个大工程。。。
我之前也想写一份 httpparser 来着,细想了下,没时间,放弃了。。。 |
10
guonaihong OP @lesismal 设计的时候支持分段传入,内部是一个状态机。
|
11
lesismal 2021-02-01 15:51:43 +08:00
@guonaihong "标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。" —— 这么说不太公平,标准库的是返回了 Request 、url header body 各段落字段都做了解析的
|
12
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() |
13
lesismal 2021-02-01 15:57:24 +08:00
我尝试了上一楼的 1.5 个包,没法返回单个包给业务层。算是 bug
|
14
lesismal 2021-02-01 15:59:08 +08:00
只是解析出一个个包、不解析包内各段落具体字段相对简单,但是对实际工程帮助也不大,所以离工程使用还有很长距离
|
15
guonaihong OP @lesismal 。。。? httparser 也返回了各 header 字段。以及 body or chunked body 。
我不知道你开火的焦点是?如果是数据没有返回,答:都返回了。 |
16
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,如果我看错了你解释就好了 技术交流,心态平和,需要豁达,不要火大 ^_^ |
17
guonaihong OP @lesismal 你的用法,和我的设计还不一样,我一开始的方案,是一个 Request 包解析完成之后,手动调用下 Reset()。所以不调用 Reset()。第二个 Request 包是不解析的,这时候对于解析器是 MessageDone 的状态。这块可以再优化下使用体验。
从打印你也可以看到,哪怕是粘包,第一个 Request 也是完整的拿出来了。 |
18
lesismal 2021-02-01 16:20:15 +08:00
上一楼打错字,"第二次发剩下的 1.5" 应该是 "第二次发剩下的 0.5"
|
19
guonaihong OP @lesismal 我觉得你和我讨论技术是挺好的,这块可以放到 github issue 上面。
|
20
lesismal 2021-02-01 16:22:37 +08:00
@guonaihong 你试下我 12 楼和 16 楼的代码,两个 Post,我这里测,只打印了一组 begin/complete,不知道是不是我测试代码写错了,如果写错了楼主给指正下我再试试,如果没写错应该算是丢了个请求
|
21
guonaihong OP @lesismal end 打印的是空行,修改下 fmt.Printf 就可以看到。是否复制我的 example 代码,
MessageComplete: func() { // 消息解析结束 fmt.Printf("\n") }, |
22
lesismal 2021-02-01 16:34:16 +08:00
@guonaihong 好,我 new 个 issue
|
23
guonaihong OP @lesismal good 。这样有一些好的讨论别人也可以看到。
|
24
eudore 2021-02-02 09:32:57 +08:00
1 、不完全认可你这个 300m/s vs 124m/s 的结果,因为你没创建*http.Request 对象,创建是额外需要一定资源的,没创建易用性很差。
2 、Parse 函数长。。。 |
25
guonaihong OP @eudore 2.Parse 长,没办法,如果 go 里面有宏替换,或者手动内联优化,也不需要写这么长了。这么写只是为了减少进 stack 出 stack 的成本。
1.哪怕使用内存分配比官方库快也是很容易的。分配可以保存 http header 内存+浅引用指向 field 和 value+惰性解析。 |