Skip to content

Day 116 — Server & Client Streaming

Month 5 · Week 1 · ⬅ Day 115 · Day 117 ➡ · Journal index

🎯 Learning Objective

Implement the two half-duplex streaming RPC shapes — server-streaming and client-streaming — and understand the Send/Recv/io.EOF protocol that drives them.

📚 Topics

  • stream X in the .proto; generated *_Server / *_Client stream interfaces
  • Send / Recv / SendAndClose / CloseAndRecv, io.EOF as end-of-stream

📖 Reading / Sources

📝 Notes

  • A stream is just a sequence of self-delimiting frames over one HTTP/2 stream. Each gRPC message is a 5-byte length-prefixed frame, so the receiver always knows where one message ends → [[framing]].
  • Server streaming (returns (stream User)): client sends one request, server Sends many messages then returns. The client loops Recv() until it gets io.EOF, which signals a clean end (not an error).
  • Client streaming (rpc X(stream User) returns (Summary)): client Sends many, then CloseAndRecv() to get the single reply. The server loops Recv() until io.EOF, then calls SendAndClose(resp).
  • io.EOF is the only sentinel that means "stream done"; any other non-nil error from Recv is a real failure (often a status error). Treat them differently → [[error-handling]].
  • Streams inherit the call's context: cancel or deadline-expire it and both sides see the stream terminate. Always check stream.Context().Err() in long server loops.
  • Concurrency axiom: it is safe for one goroutine to Send and a different goroutine to Recv on the same stream, but it is never safe for two goroutines to Send (or two to Recv) concurrently → [[channel-axioms]] (the same one-writer discipline as channels).
  • Streaming reduces latency and memory for large/continuous data — you process each message as it arrives instead of buffering the whole collection.

💻 Code Examples

// Server streaming: one request in, many messages out.
func (s *server) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
    for _, u := range s.users {
        if err := stream.Send(u); err != nil { // each Send = one framed message
            return err
        }
    }
    return nil // returning closes the stream cleanly -> client sees io.EOF
}

// Client side of a server stream:
stream, _ := client.ListUsers(ctx, &userv1.ListUsersRequest{})
for {
    u, err := stream.Recv()
    if errors.Is(err, io.EOF) {
        break // clean end of stream
    }
    if err != nil {
        return err
    }
    fmt.Println(u.GetName())
}
// Client streaming: many messages in, one summary out.
func (s *server) CreateUsers(stream userv1.UserService_CreateUsersServer) error {
    var count int32
    for {
        u, err := stream.Recv()
        if errors.Is(err, io.EOF) {
            return stream.SendAndClose(&userv1.CreateUsersSummary{Created: count})
        }
        if err != nil {
            return err
        }
        _ = u
        count++
    }
}

The length-prefixed framing that makes any stream possible, rebuilt with the stdlib: examples/month-05/framing · Run: go run ./examples/month-05/framing

🏋️ Exercises / Practice

Exercise Status Link
Implement WriteFrame/ReadFrame/ReadAll (stream framing) exercises/month-05/week-1/frames

🐛 Mistakes Made

  • Treated io.EOF from Recv as an error and logged it as a failure. It's the success terminator for a stream; check it with errors.Is(err, io.EOF) first.
  • Fanned out Send calls across goroutines and got interleaved/garbled frames. Serialized all sends through one goroutine.

❓ Open Questions

  • Is there backpressure on Send? (Yes — HTTP/2 flow control blocks Send when the peer's window is full; that's the natural backpressure.)

🧠 Active Recall (answer without looking)

  1. Q: How does the client of a server-streaming RPC know the stream is finished?
    A

Recv() returns io.EOF. That sentinel means a clean end of stream; any other non-nil error is a genuine failure (usually a status error). 2. Q: In a client-streaming RPC, which two methods bookend the exchange?

A

The client repeatedly calls Send(...) then CloseAndRecv() to read the single reply. The server loops Recv() until io.EOF, then calls SendAndClose(resp).

🪶 Feynman Reflection

A streaming RPC is the unary idea stretched in one direction. Underneath, an HTTP/2 stream carries a run of length-prefixed protobuf frames; the generated Send/Recv methods just push and pull those frames. The only protocol you must remember is "io.EOF means done", and the only rule is "one goroutine per direction".

🕳️ Knowledge Gaps

  • HTTP/2 flow-control windows and how they translate to Send blocking.
  • Choosing message-size limits and MaxRecvMsgSize.

✅ Summary

I can implement server-streaming and client-streaming RPCs, drive them with the Send/Recv/io.EOF protocol, and explain that a stream is a sequence of framed messages over one connection.

⏭️ Next Steps / Prep for Tomorrow

  • Day 117: bidirectional streaming and concurrent send/recv.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): server and client streaming (day 116)