跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • 让你的异步 io 库插上 http1.1 解析的翅膀。 httparser 来也。
未分類
27 1 月 2021

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

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

資深大佬 : guonaihong 3

httparser

让你的异步 io 库插上 http1.1 解析的翅膀。 httparser 来也。 让你的异步 io 库插上 http1.1 解析的翅膀。 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.1rn" +    "Host: github.comrn" +    "DNT: 1rn" +    "Accept-Encoding: gzip, deflate, sdchrn" +    "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4rn" +    "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.36rn" +    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9," +    "image/webp,*/*;q=0.8rn" +    "Referer: https://github.com/joyent/http-parserrn" +    "Connection: keep-alivern" +    "Transfer-Encoding: chunkedrn" +    "Cache-Control: max-age=0rnrnbrnhello worldrn0rn")   var setting = httparser.Setting{   MessageBegin: func() {    //解析器开始工作    fmt.Printf("beginn")   },   URL: func(buf []byte) {    //url 数据    fmt.Printf("url->%sn", buf)   },   Status: func([]byte) {    // 响应包才需要用到   },   HeaderField: func(buf []byte) {    // http header field    fmt.Printf("header field:%sn", buf)   },   HeaderValue: func(buf []byte) {    // http header value    fmt.Printf("header value:%sn", buf)   },   HeadersComplete: func() {    // http header 解析结束    fmt.Printf("header completen")   },   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:%vn", success, err) 

response

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
大佬有話說 (25)

  • 資深大佬 : keepeye

    先 star 了,虽然还不知道应用场景

  • 資深大佬 : shyling

    有木有和别的 http_parser 的性能对比

  • 資深大佬 : oxromantic

    既然是 http 1.1 了,必须要支持连接复用的数据吧

  • 資深大佬 : abersheeran

    @oxromantic 这个看起来是不带实际 IO 实现的,复用链接需要自己处理。

  • 資深大佬 : Ib3b

    解析不都是计算型的吗?异不异步有区别?

  • 主 資深大佬 : guonaihong

    @shyling 标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。

  • 資深大佬 : julyclyde

    @guonaihong 那我觉得你应该直接去把标准库改掉啊

  • 資深大佬 : lesismal

    大概看了下,不确定是否准确:
    1. “粘包”可能有问题,不只是一个包可能拆成多段被应用层分多次读取到,也可能是多个包的数据放一块、被应用层从任意中间位置分多次读取到,比如 3 个包被两次读到、两次分别读到前 1.5 个和后 1.5 个包
    2. 好像只是解析一个完整包的功能,并没有返回一个 Request/Response 类似的结构,所以 header 、body 之类的还是要业务层自己解析一道,这样的话业务层仍需要重复解析一次长度相关、比较浪费

    建议也解析 header 、body 相关内容,一个完整包解析完之后返回一个 Request/Response 给业务层处理,在这基础之上 parser 内置 buf 的缓存,一个段落或者一个完整包后剩余的 half 部分由 parse 自己存上,有新数据来了加一块继续解析,这样业务层不必通过 success 再截断数据跟下次数据放一块,也免去重复解析 half 的浪费

  • 資深大佬 : lesismal

    还想要 TLS 之类的支持,都搞细搞全了,也是个大工程。。。
    我之前也想写一份 httpparser 来着,细想了下,没时间,放弃了。。。

  • 主 資深大佬 : guonaihong

    @lesismal 设计的时候支持分段传入,内部是一个状态机。

  • 資深大佬 : lesismal

    @guonaihong “标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。” —— 这么说不太公平,标准库的是返回了 Request 、url header body 各段落字段都做了解析的

  • 資深大佬 : lesismal

    @guonaihong “设计的时候支持分段传入,内部是一个状态机。”—— 试一下一次读 1.5 个包的内容

    var data = []byte(
    “POST /joyent/http-parser HTTP/1.1rn” +
    “Host: github.comrn” +
    “DNT: 1rn” +
    “Accept-Encoding: gzip, deflate, sdchrn” +
    “Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4rn” +
    “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.36rn” +
    “Accept: text/html,application/xhtml+xml,application/xml;q=0.9,” +
    “image/webp,*/*;q=0.8rn” +
    “Referer: https://github.com/joyent/http-parserrn” +
    “Connection: keep-alivern” +
    “Transfer-Encoding: chunkedrn” +
    “Cache-Control: max-age=0rnrnbrnhello worldrn0rn” +

    “POST /joyent/http-parser HTTP/1.1rn” +
    “Host: github.comrn” +
    “DNT: 1rn” +
    “Accept-Encoding: gzip, deflate, sdchrn” +
    “Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4rn” +
    “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.36rn” +
    “Accept: text/html,application/xhtml+xml,application/xml;q=0.9,” +
    “image/webp,*/*;q=0.8rn” +
    “Referer: https://github.com/joyent/http-parserrn” +
    “Connection: keep-alivern” +
    “Transfer-Encoding: chunkedrn” +
    “Cache-Control: max-age=0rnrnbrnhello worldrn0rn”)

    p := httparser.New( httparser.REQUEST)
    fmt.Printf(“req_len=%dn”, 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

    我尝试了上一的 1.5 个包,没法返回单个包给业务层。算是 bug

  • 資深大佬 : lesismal

    只是解析出一个个包、不解析包内各段落具体字段相对简单,但是对实际工程帮助也不大,所以离工程使用还有很长距离

  • 主 資深大佬 : guonaihong

    @lesismal 。。。? httparser 也返回了各 header 字段。以及 body or chunked body 。
    我不知道你开火的焦点是?如果是数据没有返回,答:都返回了。

  • 資深大佬 : lesismal

    @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

    @lesismal 你的用法,和我的设计还不一样,我一开始的方案,是一个 Request 包解析完成之后,手动调用下 Reset()。所以不调用 Reset()。第二个 Request 包是不解析的,这时候对于解析器是 MessageDone 的状态。这块可以再优化下使用体验。

    从打印你也可以看到,哪怕是粘包,第一个 Request 也是完整的拿出来了。

  • 資深大佬 : lesismal

    上一打错字,”第二次发剩下的 1.5″ 应该是 “第二次发剩下的 0.5”

  • 主 資深大佬 : guonaihong

    @lesismal 我觉得你和我讨论技术是挺好的,这块可以放到 github issue 上面。

  • 資深大佬 : lesismal

    @guonaihong 你试下我 12 和 16 的代码,两个 Post,我这里测,只打印了一组 begin/complete,不知道是不是我测试代码写错了,如果写错了主给指正下我再试试,如果没写错应该算是丢了个请求

  • 主 資深大佬 : guonaihong

    @lesismal end 打印的是空行,修改下 fmt.Printf 就可以看到。是否复制我的 example 代码,

    MessageComplete: func() {
    // 消息解析结束
    fmt.Printf(“n”)
    },

  • 資深大佬 : lesismal

    @guonaihong 好,我 new 个 issue

  • 主 資深大佬 : guonaihong

    @lesismal good 。这样有一些好的讨论别人也可以看到。

  • 資深大佬 : eudore

    1 、不完全认可你这个 300m/s vs 124m/s 的结果,因为你没创建*http.Request 对象,创建是额外需要一定资源的,没创建易用性很差。

    2 、Parse 函数长。。。

  • 主 資深大佬 : guonaihong

    @eudore 2.Parse 长,没办法,如果 go 里面有宏替换,或者手动内联优化,也不需要写这么长了。这么写只是为了减少进 stack 出 stack 的成本。

    1.哪怕使用内存分配比官方库快也是很容易的。分配可以保存 http header 内存+浅引用指向 field 和 value+惰性解析。

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具