Server Stream gRPC Services​
In addition to standard Services which gRPC Refers to as Unary RPC, i.e. where clients sends a single request to the server and gets a single response back. Another very useful communication style supported by gRPC is Server streaming:
INFO
the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. gRPC guarantees message ordering within an individual RPC call
StreamServerEvents​
This communication channel is especially useful in Server Events which operates in a similar style with clients connecting to a long-lived HTTP connection that streams back "real-time Events" over the light and efficient SSE standard natively supported in modern browsers.
Although as HTTP Requests are not normally used for maintaining long-lived connections they're susceptible to issues like buffering from App Servers, middleware and proxies and require implementing a bespoke health-check and auto-reconnect solution in order to maintain interrupted service.
As a first class supported communication channel clients can instead leverage gRPC's library infrastructure which is perfectly suited for
streaming real-time Server Events over an efficient persistent HTTP/2 channel that's available from the StreamServerEvents
gRPC Service:
rpc ServerStreamServerEvents(StreamServerEvents) returns (stream StreamServerEventsResponse) {}
Which gives all protoc
supported languages a Typed Client for consuming your Server Events.
GrpcServiceClient Streams​
When using the generic GrpcServiceClient
you're able to take advantage of C#'s 8 new await foreach
syntax sugar for consuming gRPC Server Streams.
Its usage is analogous to all Server Events clients where your initial connection contains the channels you want to subscribe to receive notifications from, e.g:
var stream = client.StreamAsync(new StreamServerEvents {
Channels = new[] { "todos" }
});
Then you can use await foreach
to consume an endless stream of Server Events. Use Selector
to identify the type of Server Event
whilst the complex-type body of each event message can be parsed from its JSON body, e.g:
await foreach (var msg in stream)
{
if (msg.Selector.StartsWith("todos.")) //custom todos.* events
{
var obj = JSON.parse(msg.Json); //body of message in JSON
if (obj is Dictionary<string, object> map)
{
//todos.create + todos.update properties
var id = map["id"];
var title = map["title"];
$"EVENT {msg.Selector} [{msg.Channel}]: #{id} {title}".Print();
}
else
{
//todos.delete id
$"EVENT {msg.Selector} [{msg.Channel}]: {obj}".Print();
}
}
else
{
// general server events, e.g cmd.onConnect, cmd.onJoin, cmd.onLeave
$"EVENT {msg.Selector} [{msg.Channel}]: #{msg.UserId} {msg.DisplayName}".Print();
}
}
If connected whilst running the TodoWorld CRUD Example this stream will output something similar to:
EVENT cmd.onConnect []: #-1 user1
EVENT cmd.onJoin [todos]: #-1 user1
EVENT todos.create [todos]: #1 ServiceStack
EVENT todos.update [todos]: #1 gRPC
EVENT todos.delete [todos]: 1
protoc Dart Streams​
Other protoc
languages will require using their own language constructs for consuming gRPC Streams,
here's the example for Dart that also has a pleasant API for consuming Server Streams:
var stream = client.serverStreamServerEvents(StreamServerEvents()..channels.add('todos'));
await for (var r in stream) {
var obj = jsonDecode(r.json);
if (r.selector.startsWith('todos')) {
if (obj is Map) {
print('EVENT ${r.selector} [${r.channel}]: #${obj['id']} ${obj['title']}');
} else {
print('EVENT ${r.selector} [${r.channel}]: ${obj}');
}
} else {
print('EVENT ${r.selector} ${r.channels}: #${obj['userId']} ${obj['displayName']}');
}
}