28 Case Study: A Simple Streaming Chat
Chat has become a must-have feature in many modern applications today. Whether it’s an online store website, an educational app, or a community platform—they all need real-time interaction to make users feel more engaged. In this article, I’ll walk through a simple case study: how to design and implement a streaming chat system using gRPC Streaming, a technology that has become popular in modern backend environments.
Case Study: Requirements & Architecture
Feature Requirements
- Users can send chat messages.
- All connected users can receive the latest chat messages in real time.
- Every user can see the chat session history when they join.
- The system is simple enough to run locally.
- Basic tolerance for multiple clients.
Architecture Diagram
flowchart TD
Client1 -->|Bidirectional Streaming| gRPCServer
Client2 -->|Bidirectional Streaming| gRPCServer
gRPCServer -->|Store Chat| DB[(Database)]
- The client opens a bidirectional streaming connection with the gRPC server.
- When a message is sent, the server stores it in a database (or in memory) and then broadcasts it to all active clients.
1. Preparation: Technology Stack
- Language: Go
- gRPC Framework:
google.golang.org/grpc - Protobuf Compiler:
protoc - Database: In-memory (slice/array) or SQLite/Redis (optional for persistence)
2. Protobuf Definition
chat.proto
1syntax = "proto3";
2
3package chat;
4
5message ChatMessage {
6 string username = 1;
7 string message = 2;
8 string timestamp = 3;
9}
10
11service ChatService {
12 rpc ChatStream(stream ChatMessage) returns (stream ChatMessage);
13}3. Implementing the gRPC Server in Go
1package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net"
8 "sync"
9 "time"
10
11 pb "yourmodule/chat"
12 "google.golang.org/grpc"
13)
14
15type chatServer struct {
16 pb.UnimplementedChatServiceServer
17 mu sync.Mutex
18 clients map[string]pb.ChatService_ChatStreamServer
19 msgs []pb.ChatMessage
20}
21
22func (s *chatServer) ChatStream(stream pb.ChatService_ChatStreamServer) error {
23 id := fmt.Sprintf("client-%d", time.Now().UnixNano())
24 s.mu.Lock()
25 s.clients[id] = stream
26 for _, msg := range s.msgs {
27 stream.Send(&msg)
28 }
29 s.mu.Unlock()
30
31 defer func() {
32 s.mu.Lock()
33 delete(s.clients, id)
34 s.mu.Unlock()
35 }()
36
37 for {
38 msg, err := stream.Recv()
39 if err != nil {
40 log.Printf("client %s disconnected", id)
41 return err
42 }
43
44 msg.Timestamp = time.Now().Format(time.RFC3339)
45 s.mu.Lock()
46 s.msgs = append(s.msgs, *msg)
47 for _, c := range s.clients {
48 if err := c.Send(msg); err != nil {
49 log.Println("send error:", err)
50 }
51 }
52 s.mu.Unlock()
53 }
54}
55
56func main() {
57 lis, err := net.Listen("tcp", ":50051")
58 if err != nil {
59 log.Fatalf("failed to listen: %v", err)
60 }
61 server := grpc.NewServer()
62 pb.RegisterChatServiceServer(server, &chatServer{
63 clients: make(map[string]pb.ChatService_ChatStreamServer),
64 })
65 log.Println("gRPC chat server started on :50051")
66 server.Serve(lis)
67}4. Simulating a gRPC Client
1package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "time"
8
9 pb "yourmodule/chat"
10 "google.golang.org/grpc"
11)
12
13func main() {
14 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
15 if err != nil {
16 log.Fatalf("fail connect: %v", err)
17 }
18 defer conn.Close()
19
20 client := pb.NewChatServiceClient(conn)
21 stream, err := client.ChatStream(context.Background())
22 if err != nil {
23 log.Fatalf("fail stream: %v", err)
24 }
25
26 // Receive stream
27 go func() {
28 for {
29 msg, err := stream.Recv()
30 if err != nil {
31 log.Fatal(err)
32 }
33 fmt.Printf("[%s] %s: %s\n", msg.Timestamp, msg.Username, msg.Message)
34 }
35 }()
36
37 // Send messages
38 for {
39 var text string
40 fmt.Print("> ")
41 fmt.Scanln(&text)
42 stream.Send(&pb.ChatMessage{
43 Username: "ihsan",
44 Message: text,
45 })
46 }
47}5. Table of Components and Their Functions
| Component | Function |
|---|---|
ChatService.ChatStream | The main gRPC endpoint for bidirectional streaming chat |
ChatMessage | The data structure for messages sent and received |
map[string]stream | The list of active client connections for broadcasting |
[]ChatMessage | The chat history resent when a client connects |
6. Process Sequence Diagram
sequenceDiagram
participant Client as Klien
participant Server as gRPC Server
Client->>Server: Buka Stream
Server-->>Client: Kirim histori pesan
loop Chat
Client->>Server: Kirim ChatMessage
Server->>Server: Simpan pesan
Server->>Client: Broadcast ke semua klien
end
7. Conclusion
By leveraging gRPC bidirectional streaming, we can build a real-time chat system that is lightweight, fast, and efficient for many clients. This approach fits a wide range of modern backend needs, from live support features to collaboration apps.
For further development, you can add authentication, persist messages to a real database, or integrate a pub/sub system like Redis so it can scale horizontally.
Happy coding, and I hope this is helpful! 🚀