23 Implementing Client-side Streaming RPC
In the world of modern microservices, gRPC has become one of the leading choices for building efficient communication between services, especially when performance and flexibility are top priorities. One of gRPC’s powerful features is its support for streaming RPC. This time, let’s focus on Client-side Streaming RPC, where the client can send many messages sequentially to the server before ultimately receiving a single aggregate response from the server.
This article will cover the concept, usage scenarios, and provide an example implementation of Client-side Streaming RPC using Go (golang). To make things clearer, we’ll include a flowchart diagram and a communication simulation so you get a complete picture of this feature.
What Is Client-side Streaming RPC?
In Client-side Streaming RPC, the communication flow roughly works as follows:
- The client opens a connection and progressively sends several messages to the server using a stream.
- Once all the data has been sent, the client closes the stream.
- The server processes all the data and sends a single response back to the client.
Unlike Unary RPC (one request-one response) or Server Streaming (one request-progressive responses), in client-side streaming the client controls how many messages are sent before requesting a response from the server.
Example Use Cases
| Use case | Description |
|---|---|
| Progressive file upload | The client sends parts of a file one by one to the server |
| IoT batch data transmission | An IoT device sends sensor data to the server in stages |
| Client-side logging | The client sends many log messages then receives a summary |
Architecture and Flow
Let’s take a look at the interaction diagram using Mermaid:
sequenceDiagram
participant Client
participant Server
Client->>Server: Buka stream
loop Satu per satu
Client->>Server: Kirim message (chunk data)
end
Client-->>Server: Tutup stream (Done)
Server-->>Client: Kirim respon hasil agregasi
Essentially, streaming begins when the client opens a stream, sends all the messages (this can be a loop, for example when a user uploads a 10MB file delivered in 512KB chunks progressively), and finally closes the stream with CloseAndRecv. The server only begins processing after the stream is closed.
Protobuf Schema for Client-streaming
Suppose we want to implement a progressive file upload feature like Google Drive. We can define the gRPC service in an upload.proto file as follows:
1syntax = "proto3";
2
3service FileService {
4 // UploadFile: Client streaming, server hanya kirim satu hasil.
5 rpc UploadFile (stream Chunk) returns (UploadStatus);
6}
7
8message Chunk {
9 bytes content = 1;
10}
11
12message UploadStatus {
13 bool success = 1;
14 string message = 2;
15}Explanation:
- Chunk: The message structure sent by the client. It typically contains a portion of the file (bytes).
- UploadStatus: The final status response from the server.
Implementing Client-side Streaming RPC
1. Server Implementation (Go/gRPC)
On the server, we need to handle the data progressively until the client closes the stream, then reply with a single message.
1// file_service.go
2type FileServiceServer struct {
3 pb.UnimplementedFileServiceServer
4}
5
6func (s *FileServiceServer) UploadFile(stream pb.FileService_UploadFileServer) error {
7 fmt.Println("Start receiving file chunks...")
8 var totalBytes int64
9 for {
10 chunk, err := stream.Recv()
11 if err == io.EOF {
12 // Selesai, balas ke client
13 return stream.SendAndClose(&pb.UploadStatus{
14 Success: true,
15 Message: fmt.Sprintf("Upload selesai: %d bytes diterima.", totalBytes),
16 })
17 }
18 if err != nil {
19 return err
20 }
21 // Simulasi write ke disk
22 totalBytes += int64(len(chunk.Content))
23 }
24}Explanation:
stream.Recv(): Retrieves each chunk from the client.- When the stream reaches EOF (the client closes the stream), the server replies with the upload status.
2. Client Implementation (Go/gRPC)
The client side opens the stream and then sends the data progressively.
1func uploadFile(client pb.FileServiceClient, filePath string) {
2 ctx := context.Background()
3 stream, err := client.UploadFile(ctx)
4 if err != nil {
5 log.Fatalf("Gagal membuka stream: %v", err)
6 }
7
8 file, err := os.Open(filePath)
9 if err != nil {
10 log.Fatalf("Gagal buka file: %v", err)
11 }
12 defer file.Close()
13
14 buf := make([]byte, 512*1024) // Chunk 512KB
15 for {
16 n, err := file.Read(buf)
17 if n > 0 {
18 if err := stream.Send(&pb.Chunk{Content: buf[:n]}); err != nil {
19 log.Fatalf("Gagal kirim chunk: %v", err)
20 }
21 }
22 if err == io.EOF {
23 break
24 } else if err != nil {
25 log.Fatalf("Gagal baca file: %v", err)
26 }
27 }
28
29 status, err := stream.CloseAndRecv()
30 if err != nil {
31 log.Fatalf("Gagal menerima status upload: %v", err)
32 }
33 fmt.Printf("Server reply: %v\n", status.Message)
34}Communication Simulation
Let’s simulate sending a 2MB file in 512KB chunks:
| Chunk Order | Size (KB) | Event on the Server |
|---|---|---|
| 1 | 512 | Receiving chunk 1 (accumulated: 512KB) |
| 2 | 512 | Receiving chunk 2 (1MB) |
| 3 | 512 | Receiving chunk 3 (1.5MB) |
| 4 | 512 | Receiving chunk 4 (2MB) |
| EOF | - | Stream closed, send status |
Server response:
When Should You Use Client-side Streaming?
Use Client-side Streaming RPC when:
- The data from the client is large/abundant and you want to avoid a single giant payload.
- You have realtime data that you want to send progressively before the aggregate result is processed.
- The client has data in batches, for example updating thousands of records at once.
Comparison (Table)
| Technique | How It Works | Advantages | Disadvantages |
|---|---|---|---|
| Unary RPC | One request-one response | Simple, easy to implement | Limited payload |
| Server streaming | Server sends a stream | Continuous responses | Client can only send one request |
| Client streaming | Client sends a stream | Client can batch data | Somewhat complex |
| Bi-directional | Two-way stream | Full-duplex, very flexible | Complex communication |
Conclusion
Client-side Streaming RPC is extremely useful for efficient large-scale or batch data communication without burdening the server or network with a single large payload. With the support of the gRPC protocol, the implementation is relatively simple and scalable. Hopefully this article helps you understand and implement this pattern!
If you’d like to discuss further or see examples in other languages, feel free to leave a comment!
Happy Streaming! 🚀