24 Implementing Bidirectional Streaming RPC
Bidirectional Streaming RPC is one of the most powerful features in gRPC, the open-source framework created by Google for service-to-service communication. In modern applications, we often need real-time two-way communication—for example in chat applications, chunked file transfers, monitoring, or even collaborative editing. In this article we will dive into how Bidirectional Streaming RPC is implemented in gRPC using Go—complete with code examples, a simulation, and flow diagrams to clarify how this architecture works.
What Is Bidirectional Streaming in gRPC?
Bidirectional streaming, or duplex streaming, means that both the client and the server can send many messages simultaneously and independently for as long as the connection stays open. This is clearly more flexible than unary RPC (1:1 request-response), server streaming (1:N), or client streaming (N:1).
A simple illustration of the data flow in Bidirectional Streaming:
flowchart LR
Client -- Pesan 1, 2, 3 --> Server
Server -- Respon A, B, C --> Client
Client -- Pesan 4 --> Server
Server -- Respon D --> Client
In the diagram above, there is no strict rule about who must send a message first. Both sides can exchange data at the same time.
When Do You Need Bidirectional Streaming RPC?
Bidirectional streaming is useful in many use cases, such as:
- Real-time Chat: The client and server exchange messages asynchronously.
- Data Synchronization: For example, collaborative editing of a document.
- Telemetry/Metrics Monitoring: The client streams metric data, and the server can stream feedback right back.
Example Use Case: A Simple Chat System
To make things easy to follow, we will implement a simple chat system using gRPC Bidirectional Streaming in Go.
1. Defining the gRPC Protocol in .proto
1// chat.proto
2syntax = "proto3";
3
4package chat;
5
6service ChatService {
7 rpc Chat(stream ChatMessage) returns (stream ChatMessage);
8}
9
10message ChatMessage {
11 string user = 1;
12 string message = 2;
13 int64 timestamp = 3;
14}Here:
ChatServicehas aChatmethod that is bidirectional streaming.ChatMessagerepresents the chat message format.
2. Generate the gRPC Code
Run the following to generate the Go code:
1protoc --go_out=. --go-grpc_out=. chat.proto3. Server Implementation
File server.go:
1package main
2
3import (
4 "log"
5 "net"
6 "sync"
7
8 pb "yourrepo/chat"
9 "google.golang.org/grpc"
10)
11
12type chatServer struct {
13 pb.UnimplementedChatServiceServer
14 mu sync.Mutex
15 streams map[string]pb.ChatService_ChatServer
16}
17
18func newServer() *chatServer {
19 return &chatServer{
20 streams: make(map[string]pb.ChatService_ChatServer),
21 }
22}
23
24func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
25 var user string
26 for {
27 msg, err := stream.Recv()
28 if err != nil {
29 log.Printf("user %s left: %v", user, err)
30 s.mu.Lock()
31 delete(s.streams, user)
32 s.mu.Unlock()
33 return err
34 }
35 user = msg.User
36
37 // Register stream
38 s.mu.Lock()
39 s.streams[user] = stream
40 s.mu.Unlock()
41
42 // Broadcast to everyone (simulate group chat)
43 s.mu.Lock()
44 for u, sstr := range s.streams {
45 if u != user {
46 sstr.Send(msg)
47 }
48 }
49 s.mu.Unlock()
50 }
51}
52
53func main() {
54 lis, err := net.Listen("tcp", ":50051")
55 if err != nil {
56 log.Fatalf("failed to listen: %v", err)
57 }
58 s := grpc.NewServer()
59 pb.RegisterChatServiceServer(s, newServer())
60 log.Println("Chat server running on :50051")
61 if err := s.Serve(lis); err != nil {
62 log.Fatalf("failed to serve: %v", err)
63 }
64}Explanation:
- Every user that connects is registered in the
streamsmap. - When a message is received, the server broadcasts it to every other client.
- The streaming connection stays open for as long as the client is connected.
4. Client Implementation
File client.go:
1package main
2
3import (
4 "bufio"
5 "context"
6 "log"
7 "os"
8 "time"
9
10 pb "yourrepo/chat"
11 "google.golang.org/grpc"
12)
13
14func main() {
15 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
16 if err != nil { log.Fatalf("fail to dial: %v", err) }
17 defer conn.Close()
18
19 client := pb.NewChatServiceClient(conn)
20 stream, err := client.Chat(context.Background())
21 if err != nil { log.Fatalf("fail to open stream: %v", err) }
22
23 // Goroutine to receive messages
24 go func() {
25 for {
26 resp, err := stream.Recv()
27 if err != nil {
28 log.Printf("Disconnected: %v", err)
29 return
30 }
31 log.Printf("%s: %s", resp.User, resp.Message)
32 }
33 }()
34
35 // Send messages from stdin
36 reader := bufio.NewReader(os.Stdin)
37 user := os.Getenv("USER")
38 for {
39 text, _ := reader.ReadString('\n')
40 stream.Send(&pb.ChatMessage{
41 User: user,
42 Message: text,
43 Timestamp: time.Now().Unix(),
44 })
45 }
46}Workflow Simulation
Let’s say there are two users: Alice and Bob.
| Step | Alice (client1) | Server | Bob (client2) |
|---|---|---|---|
| 1 | Connect & stream | Register stream | |
| 2 | Connect & stream | ||
| 3 | “Hi Bob!” | Receive, broadcast | Receive broadcast |
| 4 | “Hi Alice!” | ||
| 5 | Receive, broadcast | Receive broadcast |
Sequence Diagram
sequenceDiagram Alice->>Server: Open stream Bob->>Server: Open stream Alice->>Server: Send "Hai Bob!" Server->>Bob: Send "Hai Bob!" (broadcast) Bob->>Server: Send "Hai Alice!" Server->>Alice: Send "Hai Alice!" (broadcast)
Table: RPC Types in gRPC
| RPC Type | Client Request | Server Response | When to Use |
|---|---|---|---|
| Unary RPC | Single | Single | CRUD, standard APIs |
| Server Streaming | Single | Multiple (stream) | Notifications, file downloads |
| Client Streaming | Multiple | Single | Chunked file upload, batch insert |
| Bidirectional Streaming | Multiple | Multiple (stream) | Chat, collaborative, telemetry |
Challenges & Best Practices
Challenges
- State management on the server is more involved—you need to map between users and streams.
- You must handle errors and dropped connections carefully to avoid memory leaks.
- Data synchronization becomes necessary if you need persistence.
Best Practices
- Use a mutex/lock when accessing the stream map for thread safety.
- Close the stream when a client disconnects to avoid resource leaks.
- Don’t send too many large messages or send too frequently without flow control.
- Logging is important for monitoring traffic/latency.
Conclusion
Implementing Bidirectional Streaming RPC in gRPC makes applications that need real-time two-way communication far easier to build. With a network layer that is already optimized and a declarative interface via proto files, developers ‘only’ need to focus on the application logic.
And don’t forget: this bidirectional streaming feature must be implemented with proper attention to resource management and concurrency so that your application stays scalable and robust.
The full source code and demo can be found in this GitHub repo .
Thanks for reading. If you have any questions or suggestions, feel free to leave a comment or DM me on Medium!