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 Xin the.proto; generated*_Server/*_Clientstream interfacesSend/Recv/SendAndClose/CloseAndRecv,io.EOFas end-of-stream
📖 Reading / Sources¶
- grpc.io — Basics tutorial: streaming
- grpc.io — Core concepts: RPC life cycle
- pkg.go.dev — grpc ServerStream / ClientStream
📝 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, serverSends many messages thenreturns. The client loopsRecv()until it getsio.EOF, which signals a clean end (not an error). - Client streaming (
rpc X(stream User) returns (Summary)): clientSends many, thenCloseAndRecv()to get the single reply. The server loopsRecv()untilio.EOF, then callsSendAndClose(resp). io.EOFis the only sentinel that means "stream done"; any other non-nil error fromRecvis a real failure (often astatuserror). Treat them differently → [[error-handling]].- Streams inherit the call's
context: cancel or deadline-expire it and both sides see the stream terminate. Always checkstream.Context().Err()in long server loops. - Concurrency axiom: it is safe for one goroutine to
Sendand a different goroutine toRecvon the same stream, but it is never safe for two goroutines toSend(or two toRecv) 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.EOFfromRecvas an error and logged it as a failure. It's the success terminator for a stream; check it witherrors.Is(err, io.EOF)first. - Fanned out
Sendcalls 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 blocksSendwhen the peer's window is full; that's the natural backpressure.)
🧠 Active Recall (answer without looking)¶
- 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
Sendblocking. - 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)