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 · 2085 次点击
    这是一个创建于 1181 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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   ·   我们的愿景   ·   实用小工具   ·   864 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 21:14 · PVG 05:14 · LAX 14:14 · JFK 17:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.