One RPC, one stream, one pipe for many

HTTP/2 multiplexes many independent streams over one long-lived TCP connection, and gRPC maps exactly one RPC to one stream — so a hundred concurrent calls share a single pipe instead of 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

  1. Watch
  2. Try it
  3. Predict
  4. Capture
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
What to watch for

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

Highlighted lines are the ones running in the diagram right now.

Channel.getConnection
what a new call rides on under each transport
def getConnection():
if self.http2:
# one long-lived conn, opened lazily once
if self.conn is None:
self.conn = tcp.dial(self.target)
return self.conn # every call shares it
else: # HTTP/1.1
# an in-flight call needs its own socket
sock = tcp.dial(self.target)
self.sockets.append(sock)
return sock
Client.startCall
one RPC opens exactly one stream
def startCall(method, message):
conn = channel.getConnection()
# client-initiated streams get odd ids 1,3,5,...
stream = conn.openStream(id = next_odd_id())
stream.send(HEADERS, {
':path': method,
'grpc-timeout': deadline.remaining(),
})
stream.send(DATA, frame(message))
return stream # one call == one stream
Stream.frame
the scene-2 length prefix wrapping the scene-3 bytes
def frame(message):
payload = protobuf.encode(message)
# gRPC wire prefix: 1 flag byte + 4-byte big-endian len
prefix = bytes([0]) + uint32_be(len(payload))
return prefix + payload # carried inside a DATA frame

Where this sits in Build a gRPC-style RPC framework

Scene 04 of 14, in the The transport act — HTTP/2 streams and the four shapes of a call.. 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.

Up next. Now that an RPC is just a flow of DATA frames, the number and direction of those frames is a free design knob — which quietly gives us four different shapes of call for the price of one transport.

All 14 scenes in Build a gRPC-style RPC framework · Every curriculum

Built with Arqly
Every scene in Build a gRPC-style RPC framework builds on the one before it.All 14 Build a gRPC-style RPC framework scenes