gRPC streaming is a powerful technology for building real-time, high-performance applications. Unlike traditional request/response APIs, gRPC streaming enables continuous data exchange between client and server. This makes it ideal for scenarios such as live dashboards or IoT telemetry, where clients need to receive updates as soon as they are available, reducing latency and greatly improving user experience.
To get started, you first define your service in a protobuf file. In this example, I created a service called PatientMonitor
that retrieves a patient's heartbeat, temperature, and SpO2 levels. The server returns a stream, allowing the client to receive multiple responses over time. Here’s the protobuf definition:
message VitalRequest {
string patientId = 1;
}
message VitalResponse {
string timestamp = 1;
double heartRate = 2;
double spo2 = 3;
double temperature = 4;
}
service PatientMonitorService {
rpc StreamVitals (VitalRequest) returns (stream VitalResponse);
}
With this definition, you can generate the code needed to implement both the server and client.
Server-side Implementation
On the server side, I used C# to implement the gRPC streaming endpoint. The .NET gRPC library makes it straightforward to define streaming services and handle asynchronous data flows. By adding Grpc.Tools
to your .csproj
file, the necessary client and server code is generated automatically via the <Protobuf />
elements. Here’s an example project file snippet:
<Project Sdk="Microsoft.NET.Sdk.Web">
...
<ItemGroup>
<Protobuf Include="../protos/patient_monitor.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
...
<PackageReference Include="Grpc.Tools" Version="2.54.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
Once the base implementation is generated, you can override the methods to provide your custom logic. Below is an example implementation for the StreamVitals
endpoint. Here, a loop sends updates of the patient's vitals every 5 seconds, and the stream ends only when the client cancels the request.
using Grpc.Core;
using PatientMonitoring;
public class PatientMonitorServiceImpl : PatientMonitorService.PatientMonitorServiceBase
{
private readonly Random _rand = new();
public override async Task StreamVitals(VitalRequest request,
IServerStreamWriter<VitalResponse> responseStream,
ServerCallContext context)
{
var HEALTHY_HEARTBEAT = new List<int>([
72, 73, 71, 74, 72, 70, 71, 73, 74, 72, 73, 72
]);
int count = 0;
while (!context.CancellationToken.IsCancellationRequested)
{
var vitals = new VitalResponse
{
Timestamp = DateTimeOffset.UtcNow.ToString(),
HeartRate = HEALTHY_HEARTBEAT[count % HEALTHY_HEARTBEAT.Count],
Spo2 = Math.Round(95 + _rand.NextDouble() * 5, 1),
Temperature = Math.Round(36 + _rand.NextDouble() * 1.5, 1)
};
await responseStream.WriteAsync(vitals);
await Task.Delay(5000);
count++;
}
}
}
Frontend Visualization
For the frontend, I chose JavaScript and React to build a dynamic, real-time UI.
Instead of using the standard gRPC JavaScript library (grpc-web), which hasn’t been updated in almost two years, I opted for the Connect RPC implementation. Connect RPC offers a modern, robust, and developer-friendly experience for gRPC in the browser, making it easy to consume streaming endpoints and integrate them seamlessly into React components.
First, install the following npm packages. With them is possible to generate both client and server code with buf
command, which is similar to gRPC protoc
.
npm install -D @bufbuild/buf @bufbuild/protobuf @bufbuild/protoc-gen-es
npm install -D @connectrpc/connect @connectrpc/connect-web
Next, create a buf.gen.yaml
file to configure code generation (for example, generating TypeScript for the frontend).
version: v2
plugins:
- local: ./node_modules/.bin/protoc-gen-es
out: src/gen
opt: target=ts
inputs:
- proto_file: '../protos/patient_monitor.proto'
Then execute buf generate
at the buf.gen.yaml
folder. This command will create typescript client implementation at src/gen
local folder.
You can set up the gRPC client using the createClient
function providing generated metadata for the server and the transport type. Connect RPC supports both standard gRPC and its own protocol. It is worth to mention that Connect RPC does not provide a server implementation for .NET of its own protocol.
import { createClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { PatientMonitorService } from "./gen/patient_monitor_pb";
...
const transport = createGrpcWebTransport({
baseUrl: '',
});
return createClient(
PatientMonitorService,
transport,
);
Once the client is configured, you can call the streaming endpoint. This returns an async generator that yields values as they arrive. You can iterate over this stream using for await
. Here’s an example integrated with React state:
export default function App() {
const [vitals, setVitals] = useState<VitalResponse[]>([]);
const [latestVital, setLatestVital] = useState<VitalResponse | null>(null);
...
useEffect(() => {
const streamVitals = async () => {
const req = { patientId: "12345" } as VitalRequest;
const stream = client.streamVitals(req);
for await (const item of stream) {
setVitals(prev => {
if (prev.length >= config.heartTrendLength) {
return [...prev.slice(1), item];
}
return [...prev, item];
});
setLatestVital(item);
}
};
streamVitals().catch(console.error);
}, []);
With this setup, you can visualize the patient's vital signals in real time.
For more details, check out the code at https://github.com/dmo2000/grpc-streaming-dot-net.
Top comments (0)