Golang Go语言中让你的异步io库插上http1.1解析的翅膀。httparser来也。
Golang Go语言中让你的异步io库插上http1.1解析的翅膀。httparser来也。
httparser
高性能 http 1.1 解析器,为你的异步 io 库插上解析的翅膀,目前每秒可以处理 300MB/s 流量[从零实现]
仓库位置
https://github.com/antlabs/httparser
出发点
本来想基于异步 io 库写些好玩的代码,发现没有适用于这些库的 http 解析库,索性就自己写个,弥补 golang 生态一小片空白领域。
特性
- url 解析
- request or response header field 解析
- request or response header value 解析
- Content-Length 数据包解析
- chunked 数据包解析
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
request or response
如果你不确定数据包是请求还是响应,可看下面的例子
request or response
编译
生成 unhex 表和 tokens 表
如果需要修改这两个表,可以到_cmd 目录下面修改生成代码的代码
make gen
编译 example
make example
运行示例
make example.run
return value
- err != nil 错误
- sucess== len(data) 所有数据成功解析
- sucess < len(data) 只解析部分数据,未解析的数据需再送一次
吞吐量
- 测试仓库 https://github.com/junelabs/httparser-benchmark
- Benchmark result: 8192.00 mb | 315.08 mb/s | 637803.27 req/sec | 26.00 s
更多关于Golang Go语言中让你的异步io库插上http1.1解析的翅膀。httparser来也。的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
先 star 了,虽然还不知道应用场景
更多关于Golang Go语言中让你的异步io库插上http1.1解析的翅膀。httparser来也。的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
有木有和别的 http_parser 的性能对比
既然是 http 1.1 了,必须要支持连接复用的数据吧
这个看起来是不带实际 IO 实现的,复用链接需要自己处理。
解析不都是计算型的吗?异不异步有区别?
标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。
那我觉得你应该直接去把标准库改掉啊
大概看了下,不确定是否准确:
1. "粘包"可能有问题,不只是一个包可能拆成多段被应用层分多次读取到,也可能是多个包的数据放一块、被应用层从任意中间位置分多次读取到,比如 3 个包被两次读到、两次分别读到前 1.5 个和后 1.5 个包
2. 好像只是解析一个完整包的功能,并没有返回一个 Request/Response 类似的结构,所以 header 、body 之类的还是要业务层自己解析一道,这样的话业务层仍需要重复解析一次长度相关、比较浪费
建议也解析 header 、body 相关内容,一个完整包解析完之后返回一个 Request/Response 给业务层处理,在这基础之上 parser 内置 buf 的缓存,一个段落或者一个完整包后剩余的 half 部分由 parse 自己存上,有新数据来了加一块继续解析,这样业务层不必通过 success 再截断数据跟下次数据放一块,也免去重复解析 half 的浪费
还想要 TLS 之类的支持,都搞细搞全了,也是个大工程。。。
我之前也想写一份 httpparser 来着,细想了下,没时间,放弃了。。。
设计的时候支持分段传入,内部是一个状态机。
“标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。” —— 这么说不太公平,标准库的是返回了 Request 、url header body 各段落字段都做了解析的
“设计的时候支持分段传入,内部是一个状态机。”—— 试一下一次读 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()
我尝试了上一楼的 1.5 个包,没法返回单个包给业务层。算是 bug
只是解析出一个个包、不解析包内各段落具体字段相对简单,但是对实际工程帮助也不大,所以离工程使用还有很长距离
。。。? httparser 也返回了各 header 字段。以及 body or chunked body 。
我不知道你开火的焦点是?如果是数据没有返回,答:都返回了。
楼主先淡定点,不是开火的意思
我说没返回是指标准库返回了完整的 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,如果我看错了你解释就好了
技术交流,心态平和,需要豁达,不要火大 ^_^
你的用法,和我的设计还不一样,我一开始的方案,是一个 Request 包解析完成之后,手动调用下 Reset()。所以不调用 Reset()。第二个 Request 包是不解析的,这时候对于解析器是 MessageDone 的状态。这块可以再优化下使用体验。
从打印你也可以看到,哪怕是粘包,第一个 Request 也是完整的拿出来了。
上一楼打错字,“第二次发剩下的 1.5” 应该是 “第二次发剩下的 0.5”
我觉得你和我讨论技术是挺好的,这块可以放到 github issue 上面。
你试下我 12 楼和 16 楼的代码,两个 Post,我这里测,只打印了一组 begin/complete,不知道是不是我测试代码写错了,如果写错了楼主给指正下我再试试,如果没写错应该算是丢了个请求
end 打印的是空行,修改下 fmt.Printf 就可以看到。是否复制我的 example 代码,
MessageComplete: func() {
// 消息解析结束
fmt.Printf("\n")
},
好,我 new 个 issue
good 。这样有一些好的讨论别人也可以看到。
2.Parse 长,没办法,如果 go 里面有宏替换,或者手动内联优化,也不需要写这么长了。这么写只是为了减少进 stack 出 stack 的成本。
1.哪怕使用内存分配比官方库快也是很容易的。分配可以保存 http header 内存+浅引用指向 field 和 value+惰性解析。
在Golang中,处理异步IO与HTTP协议解析是构建高性能网络应用的关键。提及为你的异步IO库插上HTTP/1.1解析的翅膀,httparser
是一个值得关注的库。这里有几个关键点可以帮助你理解并高效利用它:
-
高效解析:
httparser
是一个用C语言编写的HTTP请求/响应解析器,通过Golang的cgo机制调用,能在保持Go语言简洁性的同时,利用C语言的性能优势,快速解析HTTP/1.1协议。 -
异步IO集成:将
httparser
与Go的异步IO(如net
包中的net.Conn
或通过第三方库如goroutines
和channels
实现的异步处理)结合,可以显著提升处理大量并发HTTP请求的能力。通过非阻塞IO和事件驱动模型,资源利用率和响应速度都能得到优化。 -
灵活性与扩展性:
httparser
不仅支持基本的HTTP解析,还提供了足够的灵活性来处理自定义HTTP头部或特殊请求格式,这对于构建特定用途的HTTP服务器或代理非常有用。 -
性能考量:虽然
httparser
通过cgo带来了性能提升,但也引入了CGo调用的复杂性和潜在的内存管理问题。因此,在引入前需权衡性能提升与代码复杂度之间的平衡。
总之,httparser
为Golang开发者提供了一个强大的工具,用于在保持语言特性的同时,提升HTTP/1.1协议解析的性能。在设计和实现时,需充分考虑其异步IO集成方式及可能带来的性能与复杂性权衡。