让你的异步 io 库插上 http1.1 解析的翅膀。 httparser 来也。 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
guonaihong
V2EX    Go 编程语言

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

  •  
  •   guonaihong
    guonaihong 2021-02-01 11:34:50 +08:00 2525 次点击
    这是一个创建于 1758 天前的主题,其中的信息可能已经有所发展或是发生改变。

    httparser

    Go codecov

    高性能 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

    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) 只解析部分数据,未解析的数据需再送一次

    吞吐量

    25 条回复    2021-02-02 11:03:13 +08:00
    keepeye
        1
    keepeye  
       2021-02-01 11:42:20 +08:00
    先 star 了,虽然还不知道应用场景
    shyling
        2
    shyling  
       2021-02-01 11:57:09 +08:00
    有木有和别的 http_parser 的性能对比
    oxromantic
        3
    oxromantic  
       2021-02-01 12:55:08 +08:00
    既然是 http 1.1 了,必须要支持连接复用的数据吧
    abersheeran
        4
    abersheeran  
       2021-02-01 13:27:29 +08:00
    @oxromantic 这个看起来是不带实际 IO 实现的,复用链接需要自己处理。
    Ib3b
        5
    Ib3b  
       2021-02-01 14:30:42 +08:00
    解析不都是计算型的吗?异不异步有区别?
    guonaihong
        6
    guonaihong  
    OP
       2021-02-01 15:05:56 +08:00
    @shyling 标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。
    julyclyde
        7
    julyclyde  
       2021-02-01 15:38:22 +08:00
    @guonaihong 那我觉得你应该直接去把标准库改掉啊
    lesismal
        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 的浪费
    lesismal
        9
    lesismal  
       2021-02-01 15:44:31 +08:00
    还想要 TLS 之类的支持,都搞细搞全了,也是个大工程。。。
    我之前也想写一份 httpparser 来着,细想了下,没时间,放弃了。。。
    guonaihong
        10
    guonaihong  
    OP
       2021-02-01 15:46:30 +08:00
    @lesismal 设计的时候支持分段传入,内部是一个状态机。
    lesismal
        11
    lesismal  
       2021-02-01 15:51:43 +08:00
    @guonaihong "标准库的 http.ReadRequest,每秒只能处理 124MB 。相比之下 httparser 可以 300MB,性能还是可以的。" 这么说不太公平,标准库的是返回了 Request 、url header body 各段落字段都做了解析的
    lesismal
        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()
    lesismal
        13
    lesismal  
       2021-02-01 15:57:24 +08:00
    我尝试了上一楼的 1.5 个包,没法返回单个包给业务层。算是 bug
    lesismal
        14
    lesismal  
       2021-02-01 15:59:08 +08:00
    只是解析出一个个包、不解析包内各段落具体字段相对简单,但是对实际工程帮助也不大,所以离工程使用还有很长距离
    guonaihong
        15
    guonaihong  
    OP
       2021-02-01 16:01:06 +08:00
    @lesismal 。。。? httparser 也返回了各 header 字段。以及 body or chunked body 。
    我不知道你开火的焦点是?如果是数据没有返回,答:都返回了。
    lesismal
        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,如果我看错了你解释就好了

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

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

    MessageComplete: func() {
    // 消息解析结束
    fmt.Printf("\n")
    },
    lesismal
        22
    lesismal  
       2021-02-01 16:34:16 +08:00
    @guonaihong 好,我 new 个 issue
    guonaihong
        23
    guonaihong  
    OP
       2021-02-01 16:36:33 +08:00
    @lesismal good 。这样有一些好的讨论别人也可以看到。
    eudore
        24
    eudore  
       2021-02-02 09:32:57 +08:00
    1 、不完全认可你这个 300m/s vs 124m/s 的结果,因为你没创建*http.Request 对象,创建是额外需要一定资源的,没创建易用性很差。

    2 、Parse 函数长。。。
    guonaihong
        25
    guonaihong  
    OP
       2021-02-02 11:03:13 +08:00
    @eudore 2.Parse 长,没办法,如果 go 里面有宏替换,或者手动内联优化,也不需要写这么长了。这么写只是为了减少进 stack 出 stack 的成本。

    1.哪怕使用内存分配比官方库快也是很容易的。分配可以保存 http header 内存+浅引用指向 field 和 value+惰性解析。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1024 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 18:15 PVG 02:15 LAX 10:15 JFK 13:15
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86