从Go服务器发送分块的HTTP响应

huangapple go评论116阅读模式
英文:

Send a chunked HTTP response from a Go server

问题

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

不幸的是,客户端(在这种情况下是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")
}

希望对你有帮助!

英文:

I am creating a test Go HTTP server, and I am sending a response header of Transfer-Encoding: chunked so I can continually send new data as I retrieve it. This server should write a chunk to this server every one second. The client should be able to receive them on demand.

Unfortunately, the client(curl in this case), receives all of the chunks at the end of the duration, 5 seconds, rather than receiving one chunk every one second. Also, Go seems to send the Content-Length for me. I want to send the Content-Length at the end, and I want the the header's value to be 0.

Here is the server code:

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")
}

答案1

得分: 37

这个技巧似乎是在每写入一个块后调用Flusher.Flush()。请注意,“Transfer-Encoding”标头将由写入器隐式处理,因此无需设置它。

你可以使用telnet进行验证:

<!-- language: lang-none -->

$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is &#39;^]&#39;.
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.ResponseWriter是否支持并发访问,以供多个goroutine使用。

此外,有关“X-Content-Type-Options”标头的更多信息,请参阅此问题:“X-Content-Type-Options”标头是什么意思

英文:

The trick appears to be that you simply need to call Flusher.Flush() after each chunk is written. Note also that the "Transfer-Encoding" header will be handled by the writer implicitly, so no need to set it.

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

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

You can verify by using telnet:

<!-- language: lang-none -->

$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is &#39;^]&#39;.
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

...

You might need to do some research to verify that http.ResponseWriters support concurrent access for use by multiple goroutines.

Also, see this question for more information about the "X-Content-Type-Options" header.

答案2

得分: 0

看起来httputil提供了一个NewChunkedReader函数。

英文:

It looks like httputil provides a NewChunkedReader function

答案3

得分: 0

我认为你的问题标题有点误导性。在你的场景中,答案必须满足等待一秒钟后再发送每个块的独特需求,而不是尽可能快地发送块,这可以通过使用io.Copyreq.Header.Set("Transfer-Encoding", "chunked")来实现。

有几件事情你做得是不必要的(不需要goroutine,不需要特定的头部)。但是,出于教育目的,我将展示让你的代码工作的最小方式,包括两个更改:

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 添加flusher
	flusher, ok := w.(http.Flusher)
	if !ok {
		panic("expected http.ResponseWriter to be an http.Flusher")
	}
    w.Header().Set("Connection", "Keep-Alive")
    // 不需要这些,因为手动刷新会设置这个头部
    // w.Header().Set("Transfer-Encoding", "chunked")
    
    // 确保设置了这个头部
	w.Header().Set("X-Content-Type-Options", "nosniff")

	ticker := time.NewTicker(time.Millisecond * 500)
	go func() {
		for t := range ticker.C {
            // #2 添加'\n'
			io.WriteString(w, "Chunk\n")
			fmt.Println("Tick at", t)
			flusher.Flush()
		}
	}()
	time.Sleep(time.Second * 5)
	ticker.Stop()
    // 不需要这个
    // fmt.Println("Finished: should return Content-Length: 0 here")
    // w.Header().Set("Content-Length", "0")
}
  1. 如@maetrics建议的那样,你需要使用flusher来强制ResponseWriter发送块,即使写入器的内部缓冲区尚未填满。如果你没有手动调用flush,它会在内部缓冲区填满2048字节后自动调用flush。

  2. HTTP Transfer-Encoding协议所建议的那样,你需要在要发送的每个块后添加\n。否则,flush调用将无法按预期工作,你将一次性接收到整个消息,而不是按照每秒一次的方式。

注意:

  • 如@morganbaz在评论中指出的,你需要确保设置了X-Content-Type-Options: nosniff头部。有关解释,请参阅@morganbaz的评论。

  • 由于你手动调用了flush,因此不需要设置Transfer-Encoding头部。

  • 当设置了Transfer-Encoded: chunked头部时,不应设置content-length头部。请参阅HTTP Transfer-Encoding协议

英文:

I think your question title is a bit misleading. The answer in your scenario has to cater to the unique need of waiting a second before sending each chunk rather than sending the chunks as fast as possible, which would be easy to do with io.Copy and req.Header.Set(&quot;Transfer-Encoding&quot;, &quot;chunked&quot;)

There are a couple of things that you are doing that are unnecessary (don't need a goroutine, don't need specific headers). But, for educational purposes, I'll show the minimum way to get your code working, which includes 2 changes:

package main

import (
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

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

func HandlePost(w http.ResponseWriter, r *http.Request) {
    // #1 add flusher
	flusher, ok := w.(http.Flusher)
	if !ok {
		panic(&quot;expected http.ResponseWriter to be an http.Flusher&quot;)
	}
    w.Header().Set(&quot;Connection&quot;, &quot;Keep-Alive&quot;)
    // Don&#39;t need these this bc manually flushing sets this header
    // w.Header().Set(&quot;Transfer-Encoding&quot;, &quot;chunked&quot;)
    
    // make sure this header is set
	w.Header().Set(&quot;X-Content-Type-Options&quot;, &quot;nosniff&quot;)

	ticker := time.NewTicker(time.Millisecond * 500)
	go func() {
		for t := range ticker.C {
            // #2 add &#39;\n&#39;
			io.WriteString(w, &quot;Chunk\n&quot;)
			fmt.Println(&quot;Tick at&quot;, t)
			flusher.Flush()
		}
	}()
	time.Sleep(time.Second * 5)
	ticker.Stop()
    // Don&#39;t need this
    // fmt.Println(&quot;Finished: should return Content-Length: 0 here&quot;)
    // w.Header().Set(&quot;Content-Length&quot;, &quot;0&quot;)
}
  1. As @maetrics suggested, you need to use a flusher to force the ResponseWriter to send the chunk, even though the writer's internal buffer isn't filled yet. If you didn't call flush manually, it would call flush internally once the 2048 byte internal buffer is full.

  2. As the HTTP Transfer-Encoding protocol suggests. You need to add a \n to each chunk you want to send. Otherwise, the flush call won't work as expected, and you will receive the whole message together rather than in 1-second increments, as intended.

Notes:

  • As @morganbaz noted in the comments, you need to make sure the X-Content-Type-Options: nosniff header is set. See @morganbaz's comment for the explanation.

  • Since you are manually calling flush, there is no need to set the Transfer-Encoding header.

  • You shouldn't set the content-length header when the Transfer-Encoded: chunked header is set. See the HTTP Transfer-Encoding Protocol.

huangapple
  • 本文由 发表于 2014年11月6日 08:15:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/26769626.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定