【问题标题】:Send a chunked HTTP response from a Go server从 Go 服务器发送分块的 HTTP 响应
【发布时间】:2021-11-19 04:38:04
【问题描述】:

我正在创建一个测试 Go HTTP 服务器,并且我正在发送一个 Transfer-Encoding: 分块的响应标头,因此我可以在检索新数据时不断发送新数据。该服务器应该每秒向该服务器写入一个块。客户应该能够按需接收它们。

不幸的是,客户端(在本例中为 curl)在持续时间结束时(5 秒)接收所有块,而不是每秒接收一个块。此外,Go 似乎为我发送了 Content-Length。我想在最后发送 Content-Length,并且我希望标头的值为 0。

这是服务器代码:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/test", HandlePost);
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func HandlePost(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Connection", "Keep-Alive")
    w.Header().Set("Transfer-Encoding", "chunked")
    w.Header().Set("X-Content-Type-Options", "nosniff")

    ticker := time.NewTicker(time.Second)
    go func() {
        for t := range ticker.C {
            io.WriteString(w, "Chunk")
            fmt.Println("Tick at", t)
        }
    }()
    time.Sleep(time.Second * 5)
    ticker.Stop()
    fmt.Println("Finished: should return Content-Length: 0 here")
    w.Header().Set("Content-Length", "0")
}

【问题讨论】:

  • Go 团队的 Andrew Gerrand 前几天发布了类似的内容...我会尽力为您找到。
  • 抱歉,它不太一样 - 它来自请求的客户端。不过,您可能会从中学到一些东西:github.com/nf/dl/blob/master/dl.go
  • 未来读者:如果这对你不起作用,我犯的错误是使用w.WriteHeader() before 设置 Connection: Keep-AliveTransfer-Encoding: chunked 标头。使用w.WriteHeader() 推送标头,之后您将无法再设置其他标头,因此客户端永远不会获取 Connection: Keep-AliveTransfer-Encoding: chunked 标头并处理响应作为未分块

标签: go


【解决方案1】:

诀窍似乎是您只需在写入每个块后调用Flusher.Flush()。另请注意,“Transfer-Encoding”标头将由作者隐式处理,因此无需设置。

func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
      panic("expected http.ResponseWriter to be an http.Flusher")
    }
    w.Header().Set("X-Content-Type-Options", "nosniff")
    for i := 1; i <= 10; i++ {
      fmt.Fprintf(w, "Chunk #%d\n", i)
      flusher.Flush() // Trigger "chunked" encoding and send a chunk...
      time.Sleep(500 * time.Millisecond)
    }
  })

  log.Print("Listening on localhost:8080")
  log.Fatal(http.ListenAndServe(":8080", nil))
}

您可以使用 telnet 进行验证:

$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1

HTTP/1.1 200 OK
Date: Tue, 02 Jun 2015 18:16:38 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

9
Chunk #1

9
Chunk #2

...

您可能需要做一些研究来验证 http.ResponseWriters 是否支持并发访问以供多个 goroutine 使用。

此外,有关"X-Content-Type-Options" header 的更多信息,请参阅此问题。

【讨论】:

  • 您的 flusher.Flush() 版本对我有用,但只有在我添加了 OP 示例中的 nosniff 标头之后。您的版本在 telnet 和 wget 中运行良好,但我猜浏览器正在尝试更智能地缓冲内容,因此除非您指定 nosniff,否则 Chrome 和 Firefox 似乎都拒绝处理数据,直到数据全部到达。
  • 对于那些想知道的人,请参阅this answer。基本上发生的情况是,一些浏览器会尝试推断页面的 Content-Type,即使 服务器明确告诉客户端它是,例如 text/html。使用X-Content-Type-Options: nosniff 将从上述浏览器中禁用此功能。
【解决方案2】:

貌似httputil提供了NewChunkedReader函数

【讨论】:

    【解决方案3】:

    我认为您的问题标题有点误导。您的方案中的答案必须满足在发送每个块之前等待一秒钟而不是尽可能快地发送块的独特需求,这对于 io.Copyreq.Header.Set("Transfer-Encoding", "chunked") 来说很容易做到

    您正在做的一些事情是不必要的(不需要 goroutine,不需要特定的标头)。但是,出于教育目的,我将展示让您的代码正常工作的最小方法,其中包括 2 个更改:

    package main
    
    import (
        "fmt"
        "io"
        "log"
        "net/http"
        "time"
    )
    
    func main() {
        http.HandleFunc("/test", HandlePost)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    func HandlePost(w http.ResponseWriter, r *http.Request) {
        // #1 add flusher
        flusher, ok := w.(http.Flusher)
        if !ok {
            panic("expected http.ResponseWriter to be an http.Flusher")
        }
        w.Header().Set("Connection", "Keep-Alive")
        // Don't need these this bc manually flushing sets this header
        // w.Header().Set("Transfer-Encoding", "chunked")
        
        // make sure this header is set
        w.Header().Set("X-Content-Type-Options", "nosniff")
    
        ticker := time.NewTicker(time.Millisecond * 500)
        go func() {
            for t := range ticker.C {
                // #2 add '\n'
                io.WriteString(w, "Chunk\n")
                fmt.Println("Tick at", t)
                flusher.Flush()
            }
        }()
        time.Sleep(time.Second * 5)
        ticker.Stop()
        // Don't need this
        // fmt.Println("Finished: should return Content-Length: 0 here")
        // w.Header().Set("Content-Length", "0")
    }
    
    1. 正如@maetics 建议的那样,您需要使用刷新器来强制 ResponseWriter 发送块,即使写入器的内部缓冲区尚未填充。 如果你没有手动调用flush,一旦2048字节的内部缓冲区满了,它就会在内部调用flush。

    2. 正如HTTP Transfer-Encoding protocol 所建议的那样。您需要为要发送的每个块添加\n。否则,刷新调用将无法按预期工作,您将一起收到整个消息,而不是按预期以 1 秒为增量。

    注意事项:

    • 正如@morganbaz 在 cmets 中指出的那样,您需要确保设置了 X-Content-Type-Options: nosniff 标头。有关解释,请参阅 @morganbaz 的评论。
    • 由于是手动调用flush,所以不需要设置Transfer-Encoding头。
    • 当设置Transfer-Encoded: chunked header 时,不应设置内容长度标头。请参阅HTTP Transfer-Encoding Protocol

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-12
      • 1970-01-01
      • 2017-01-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多