
高性能 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+惰性解析。 |