Four call shapes from one stream — half-close and DATA frames on HTTP/2

Unary and streaming calls are the SAME HTTP/2 stream — the only difference is how many DATA frames flow each way and when each side half-closes — so streaming is a consequence of the transport, not a bolt-on.

Previously

Last scene, an RPC became just a stream of DATA frames on one connection. Once an RPC is only a COUNT of DATA frames in each direction, changing that count is free — so the same stream that carried one greet("Ada") can carry a whole flow of them.

Scene 05

Four call shapes from one stream

  1. Watch
  2. Try it
  3. Predict
  4. Capture
CALL SHAPEUnaryone → · one ←same HTTP/2 stream — only the DATA-frame pattern changesclientsends requestsserversends responsesSTREAM 1REQUEST FRAMES →← RESPONSE FRAMESDATAgreet("Ada")DATAHello Adahalf-closehalf-close1 →← 1One request frame, one response frame — the classic call.
unary call: one → , one ←
half-close: a side raises this once it's done sending
What to watch for

Here is the running example you've been carrying — greet("Ada") — drawn on one HTTP/2 stream (a stream is one RPC's lane down the shared connection; a frame is one chunk on it). First the simplest shape: the client sends ONE request DATA frame, the server sends back ONE response DATA frame ("Hello Ada"), and both sides drop a half-close flag — that's the whole call. When a client sends one request and gets exactly one response back, we call it a unary call — the plain function-call shape RPC started from. Now advance: the SAME stream re-draws with the server sending THREE ordered response frames ("Hello Ada", "Hi again Ada", "Bye Ada") before it half-closes. Nothing about the transport changed — it's one stream either way; the server just sent more DATA frames one direction. A call where one side (or both) sends many DATA frames instead of one is a streaming call. The pattern of arrows is the only thing that moved.

Continue unlocks when the animation finishes.
Implementation

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

Client.call
open one stream, send request frame(s), half-close
def call(method, requests):
stream = conn.newStream() # one RPC = one stream
stream.send(HEADERS, method, grpc_timeout)
for req in requests: # 1 frame, or many
stream.send(DATA, encode(req))
stream.send(END_STREAM) # half-close: done sending
for resp in stream.recv(): # frames arrive in order
yield decode(resp)
Server.handle
read request frame(s), write response frame(s), half-close
def handle(stream):
requests = [decode(f) for f in stream.recv()]
for resp in compute(requests): # 1 frame, or many
stream.send(DATA, encode(resp))
stream.send(END_STREAM) # half-close: done sending
Stream.sendFrame
the single transport routine every shape rides
def send(kind, payload=None):
if kind == END_STREAM:
self.flags |= END_STREAM # one direction closed
return
# gRPC length-prefix: 1 flag byte + 4-byte length
frame = lengthPrefix(payload)
conn.writeFrame(self.id, kind, frame)
# same stream id, same path — for all four shapes

Where this sits in Build a gRPC-style RPC framework

Scene 05 of 14, in the The transport act — HTTP/2 streams and the four shapes of a call.. Unary and the three streaming shapes are the same stream — only the number and direction of DATA frames differ. Streaming is a consequence of the transport, not a bolt-on.

Up next. Whether a call sends one frame or a thousand, it runs as long as it likes if nothing stops it — and a server happily computing for a client who already walked away is the most expensive bug in the whole stack.

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