Build a gRPC-style RPC framework (14 scenes)
Scene 04 · One RPC, one stream, one pipe for many
HTTP/2 multiplexes many independent streams over one long-lived TCP connection; gRPC maps one RPC to one stream. A hundred calls share one pipe, not a hundred sockets.
Previously

Our encoded `Greeting{name="Ada"}` bytes would crawl one-at-a-time over a raw socket, exactly the HTTP/1.1 trap — so gRPC rides HTTP/2, which lets a hundred calls interleave down a single connection.

Scene 04
One RPC, one stream, one pipe for many
Diagram
A single horizontal TCP connection runs left to right. Each RPC opens its own STREAM (a distinct color), and a stream is a sequence of FRAMES: a HEADERS frame opens it and carries metadata, then a DATA frame carries the payload. The inset zooms one DATA frame to show our scene-3 `Greeting{name="Ada"}` bytes still wearing their scene-2 length prefix INSIDE the frame. Two meters track what matters: how many streams are in flight, and how many TCP connections that costs.
TRANSPORT · HTTP/23 concurrent RPCsTCP connections1streams ↑, connections stay flatclient3 RPCsserverone listenerONE TCP CONNECTION · 3 of 3 streams shownH1H3H5D1D3D51·Greet3·Greet5·GreetDATA frame[len=12]Greeting{name="Ada"}HTTP/2 · 3 RPCs = 3 streams on 1 connectionHEADERS opens a stream and carries metadata; DATA carries the payload; RST_STREAM abruptly ends one stream (its cancel role comes…
one stream per RPC ↓
frames: HEADERS opens, DATA carries the payload →
inset: scene-3 bytes inside scene-2 length prefix
gRPC doesn't run on raw TCP or HTTP/1.1 — it runs on HTTP/2. Here's why that matters. Picture one long-lived TCP connection (the pipe) that several calls share at once. Each call gets its own lane down that pipe: that lane is a *stream*. Watch three RPCs — three `greet("Ada")` calls — each open a stream and send their bytes down the one connection. The bytes travel in small labeled blocks called *frames*: a HEADERS frame opens each stream, then a DATA frame carries the payload. Notice the inset: that DATA frame still contains the scene-3 `Greeting{name="Ada"}` bytes, still wrapped in the scene-2 length prefix — every layer you built is nested inside this one. The streams interleave: a frame from one call, then a frame from another, all woven down the single pipe.
Implementation
Channel.getConnection
what a new call rides on under each transport
1def getConnection():
2 if self.http2:
3 # one long-lived conn, opened lazily once
4 if self.conn is None:
5 self.conn = tcp.dial(self.target)
6 return self.conn # every call shares it
7 else: # HTTP/1.1
8 # an in-flight call needs its own socket
9 sock = tcp.dial(self.target)
10 self.sockets.append(sock)
11 return sock
Client.startCall
one RPC opens exactly one stream
1def startCall(method, message):
2 conn = channel.getConnection()
3 # client-initiated streams get odd ids 1,3,5,...
4 stream = conn.openStream(id = next_odd_id())
5 stream.send(HEADERS, {
6 ':path': method,
7 'grpc-timeout': deadline.remaining(),
8 })
9 stream.send(DATA, frame(message))
10 return stream # one call == one stream
Stream.frame
the scene-2 length prefix wrapping the scene-3 bytes
1def frame(message):
2 payload = protobuf.encode(message)
3 # gRPC wire prefix: 1 flag byte + 4-byte big-endian len
4 prefix = bytes([0]) + uint32_be(len(payload))
5 return prefix + payload # carried inside a DATA frame
Not sure what to ask? Tap a question — the staff engineer answers in the chat panel.