用代码详解Go语言HTTP请求流式写入body

背景

最近在开发一个功能时,需要通过 http 协议上报大量的日志内容,但是在 Go 标准库里的 http client 的 API 是这样的:

http.NewRequest(method, url string, body io.Reader)

body 是通过 io.Reader 接口来传递,并没有暴露一个 io.Writer 接口来提供写入的办法,先来看看正常情况下怎么写入一个 body ,示例:

需要先把要写

buf := bytes.NewBuffer([]byte("hello"))
http.Post("localhost:8099/report","text/pain",buf)

入的数据放在 Buffer 中,放内存缓存着,但是我需要写入 大量 的数据,如果都放内存里肯定要 OOM 了,http client 并没有提供 流式写入 的方法,我这么大的数据量直接用 Buffer 肯定是不行的,最后在 google 了一番之后找到了解决办法。

使用 io.pipe

调用 io.pipe() 方法会返回 ReaderWriter 接口实现对象,通过 Writer 写数据, Reader 就可以读到,利用这个特性就可以实现流式的写入,开一个协程来写,然后把 Reader 传递到方法中,就可以实现 http client body 的流式写入了。

代码示例:

pr, rw := io.Pipe()
// 开协程写入大量数据
go func(){
 for i := 0; i < 100000; i++ {
 rw.Write([]byte(fmt.Sprintf("line:%d\r\n", i)))
 }
 rw.Close()
}()
// 传递Reader
http.Post("localhost:8099/report","text/pain",buf)

源码阅读 目的

了解 go 中 http client 对于 body 的传输是如何处理的。

开始

在构建 Request 的时候,会断言 body 参数的类型,当类型为 *bytes.Buffer*bytes.Reader*strings.Reader 的时候,可以直接通过 Len() 方法取出长度,用于 Content-Length 请求头,相关代码net/http/request.go#L872-L914 :

if body != nil {
 switch v := body.(type) {
 case *bytes.Buffer:
 req.ContentLength = int64(v.Len())
 buf := v.Bytes()
 req.GetBody = func() (io.ReadCloser, error) {
  r := bytes.NewReader(buf)
  return ioutil.NopCloser(r), nil
 }
 case *bytes.Reader:
 req.ContentLength = int64(v.Len())
 snapshot := *v
 req.GetBody = func() (io.ReadCloser, error) {
  r := snapshot
  return ioutil.NopCloser(&r), nil
 }
 case *strings.Reader:
 req.ContentLength = int64(v.Len())
 snapshot := *v
 req.GetBody = func() (io.ReadCloser, error) {
  r := snapshot
  return ioutil.NopCloser(&r), nil
 }
 default:
 }
 if req.GetBody != nil && req.ContentLength == 0 {
 req.Body = NoBody
 req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
 }
}

在链接建立的时候,会通过 body 和上一步中得到的 ContentLength 来进行判断,如果 body!=nil 并且 ContentLength==0 时,可能就会启用 Chunked 编码进行传输,相关代码 net/http/transfer.go#L82-L96  :

case *Request:
 if rr.ContentLength != 0 && rr.Body == nil {
 return nil, fmt.Errorf("http: Request.ContentLength=%d with nil Body", rr.ContentLength)
 }
 t.Method = valueOrDefault(rr.Method, "GET")
 t.Close = rr.Close
 t.TransferEncoding = rr.TransferEncoding
 t.Header = rr.Header
 t.Trailer = rr.Trailer
 t.Body = rr.Body
 t.BodyCloser = rr.Body
 // 当body为非nil,并且ContentLength==0时,这里返回-1
 t.ContentLength = rr.outgoingLength()
 // TransferEncoding没有手动设置,并且请求方法为PUT、POST、PATCH时,会启用chunked编码传输
 if t.ContentLength < 0 && len(t.TransferEncoding) == 0 && t.shouldSendChunkedRequestBody() {
 t.TransferEncoding = []string{"chunked"}
 }

验证(一)

按照对源码的理解,可以得知在使用 io.pipe() 方法进行流式传输时,会使用 chunked 编码进行传输,通过以下代码进行验证:

服务端

func main(){
 http.HandleFunc("/report", func(writer http.ResponseWriter, request *http.Request) {

 })
 http.ListenAndServe(":8099", nil)
}

客户端

func main(){
 pr, rw := io.Pipe()
 go func(){
 for i := 0; i < 100; i++ {
  rw.Write([]byte(fmt.Sprintf("line:%d\r\n", i)))
 }
 rw.Close()
 }()
 http.Post("localhost:8099/report","text/pain",buf)
}

先运行服务端,然后运行客户端,并且使用 WireShake 进行抓包分析,结果如下:

用代码详解Go语言HTTP请求流式写入body

可以看到和预想的结果一样。

验证(二)

在数据量大的时候 chunked 编码会增加额外的开销,包括编解码和额外的报文开销,能不能不用 chunked 编码来进行 流式传输 呢?通过源码可以得知,当 ContentLength 不为 0 时,如果能预先计算出待传输的 body size ,是不是就能避免 chunked 编码呢?思路就到这,接着就是写代码验证:

服务端

func main(){
 http.HandleFunc("/report", func(writer http.ResponseWriter, request *http.Request) {

 })
 http.ListenAndServe(":8099", nil)
}

客户端

count := 100
line := []byte("line\r\n")
pr, rw := io.Pipe()
go func() {
 for i := 0; i < count; i++ {
 rw.Write(line)
 }
 rw.Close()
}()
// 构造request对象
request, err := http.NewRequest("POST", "http://localhost:8099/report", pr)
if err != nil {
 log.Fatal(err)
}
// 提前计算出ContentLength
request.ContentLength = int64(len(line) * count)
// 发起请求
http.DefaultClient.Do(request)

抓包结果:

用代码详解Go语言HTTP请求流式写入body

可以看到确实直接使用的 Content-Length 进行传输,没有进行 chunked 编码了。

总结

本文的目的主要是记录 go 语言中 http client 如何进行流式的写入,并通过阅读源码了解 http client 内部对 body 的写入是如何进行处理的,通过两个验证可以得知,如果能提前计算出 ContentLength 并且对性能要求比较苛刻的情况下,可以通过手动设置 ContentLength 来优化性能。

阅读剩余 73%

原创文章,作者:WVUEI,如若转载,请注明出处:https://www.beidanyezhu.com/a/26633.html

(0)
WVUEI的头像WVUEI
上一篇 2025-01-04
下一篇 2025-01-05

相关推荐

  • 线性代数中矩阵的秩详解(秩怎么求)

    看到有小伙伴在问,矩阵的秩是什么,做了那么多题目,对于矩阵的秩还没系统的总结过,今天我就结合一下实际例题,来回答一下矩阵的秩是什么。 矩阵的秩 在线性代数中,一个矩阵A的列秩是A的…

  • 语言(逅怎么读)

    引 言吴语是汉语族的一个重要分支,分布在江苏南部、浙江省大部、上海市全境,安徽南部及福建、江西的小部分地区。学术界将其分爲太湖片、台州片、东瓯片、婺州片、处衢片、宣州片六个小片。其…

  • 语言的谋略(洛邑怎么读)

    第十七辑 国家政策辩论的谋略与技巧文/钟百超 每个人对一定事物或做法都有自己的主见,而这个主见的形成与个人的信仰、理念、知识、修养,乃至利益都有密切相关。一个人能否提出一个有利于国…

  • 一文详解(欢迎客户下单的句子)

    编辑导读:零食,自古以来一直颇受欢迎。如今的零食花样种类越来越多,口感丰富、包装精美、还有顶流代言,售价也越来越贵。这其中,少不了营销传播。本文作者对新品牌零食的渠道营销进行分析,…

  • 怎么使用Go语言实现数据转发功能

    首先,我们需要考虑数据实体的格式。在许多情况下,使用JSON格式最为方便。Go语言中有一个标准库“encoding/json”可以提供JSON编解码功能。我们可以使用它来将我们的数…

  • 怎么以正确的方式替换Go语言程序自身

    Go语言是一门越来越受欢迎的编程语言,其致力于简化开发过程并提高编译速度。与其他编程语言相比,Go语言具有更好的并发性和易读性。在Go语言中,替换自己是一个常见的模式。在此文中,我…

  • Go语言中interface类型怎么使用

    Go语言中interface类型的定义 在Go语言中,interface类型是一个抽象的类型,它是一组方法签名的集合,只要某个类型实现了这些方法,它就属于该interface类型。…

  • Go语言中怎么实现代码跳转

    一、基本概念 在Go语言中,代码跳转是指从一个位置跳转到另一个位置。一般而言,我们可以通过函数调用进行代码跳转,也可以通过goto语句进行跳转。不同的跳转方式有不同的适用场景。 二…

  • Go语言中slice的反转方法怎么使用

    1.使用for循环和切片的索引 最基本的反转方法是使用for循环和切片的索引,遍历切片并将每个元素逆序放置。具体实现代码如下: func reverseSlice1(s&…

  • 怎么在Go语言中实现锁机制

    Go语言的锁 在Go语言中,最常用的锁是互斥锁(Mutex)。互斥锁是一种特殊的二进制信号量,用于控制对共享资源的访问。Go语言通过标准库中的"sync"包提供…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

分享本页
返回顶部