Previous parts:
Server-side streaming allows you to send data to the client as it becomes available, rather than waiting for the entire response to be ready. This is particularly useful for applications that require reliable real-time updates, such as chat applications, live feeds, or large file transfers.
Streaming Server Response
This part is similar to the regular HTTP response streaming via http.ResponseWriter
.
We will stream data from the server as soon as it becomes available, without buffering. For this, we will use the http.Flusher
interface, which allows us to flush the response buffer to the client immediately. For reference on how to implement a basic HTTP/3 server, check out Writing HTTP3 Server.
mux := http.NewServeMux()
mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher) // check if the ResponseWriter supports flushing
if !ok {
http.Error(w, "streaming is not supported", http.StatusInternalServerError)
return
}
// responding that we have successfully received the request
w.WriteHeader(http.StatusOK)
// streaming response
for i := range 10 {
fmt.Fprintf(w, "data chunk #%d\n", i)
flusher.Flush()
time.Sleep(100 * time.Millisecond)
}
})
And run the server itself:
srv := &http3.Server{
// listen on the port 8080
Addr: "127.0.0.1:8080",
Handler: mux,
}
// path to generated cert and key
if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
panic(err)
}
The certificates are the same as we generated in the previous part.
Client
Now we need something to read our data. On the client side, we’ll load the server’s TLS certificate, as we did previously:
certPool := x509.NewCertPool()
certData, err := os.ReadFile("cert.pem")
if err != nil {
panic(err)
}
certPool.AppendCertsFromPEM(certData)
Next, create an HTTP client:
client := &http.Client{
Transport: &http3.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool, // use the cert pool with the server's cert
},
},
}
Then, call the server and print out the response it returns.
resp, err := client.Get("https://localhost:8080/stream")
if err != nil {
panic(err)
}
defer resp.Body.Close()
// read the response body
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
panic(err)
}
If you run all of this together, you’ll see a stream of data coming in with slight delays (because the server simulates work with a short sleep between messages):
$ go run ./responsestream.go
data chunk #0
data chunk #1
data chunk #2
data chunk #3
data chunk #4
...
You can find the complete code in responsestream.go.
Overtaking HTTP/3 Stream
In this section, we’ll take direct control of the HTTP/3 stream to send data to the client. This approach is helpful when you need more fine-grained control over the stream.
We’ll use http3.HTTPStreamer
, which is implemented by http.ResponseWriter
. When a stream is taken over, it’s the caller’s responsibility to close the stream, so don’t forget to close it with defer
.
mux := http.NewServeMux()
mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
// take over the HTTP/3 stream
streamer := w.(http3.HTTPStreamer)
http3Stream := streamer.HTTPStream()
defer http3Stream.Close()
...
Once you have the stream, you can send data like this:
mux := http.NewServeMux()
mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
// respond with OK 200 header
w.WriteHeader(http.StatusOK)
// take over the HTTP/3 stream
streamer := w.(http3.HTTPStreamer)
http3Stream := streamer.HTTPStream()
defer http3Stream.Close()
// send data to the stream
for i := range 10 {
fmt.Fprintf(http3Stream, "data chunk #%d\n", i)
time.Sleep(100 * time.Millisecond)
}
})
The full code is available in httpstreamer.go.
If you run this code, you’ll see the same output as before.
That’s it — you are amazing! 💅
Recap
In this post, we learned how to implement real-time streaming over HTTP/3. We covered setting up both the server and client, and sending data as it becomes available.
Next Parts: