Table of Contents
- Introduction
- What is gRPC?
- Understanding Protocol Buffers
- How gRPC and Protobuf Work Together
- gRPC Service Methods
- Implementing Bidirectional gRPC Streaming
- Running the Project Locally
- Bidirectional Streaming in Action
- Why gRPC Is A Better Choice ?
- References
Introduction
As systems growing to be the distributed,efficient communication becomes critical.While REST has long been the default, it often falls short in performance and flexibility—especially for real-time, service-to-service communication.
That's where gRPC comes in to play !
Developed by Google, gRPC offers fast, reliable communication using Protocol Buffers, with support for features like streaming and strong API contracts.
In this blog, we’ll briefly explore gRPC and Protobuf, compare them to traditional approaches, and demonstrate how bidirectional streaming can power real-time interactions in modern backend and cloud-native systems.
What is gRPC?
Modern applications often needs to communicate across services, languages, or machines. Traditional RESTAPI's can fall a bit short when its about- Low latency, high throughput, and efficient serialization
Built on top of HTTP/2 and using Protocol Buffers for data serialization, gRPC allows for fast, efficient, and strongly typed communication between services.
in gRPC, a client application written in a language (for example, Go) can directly call a method from the server residing on other machine and maybe written in another language than client, as if that method is its own local method.
Understanding Protocol Buffers
Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.Protocol buffers are the most commonly-used data format at Google. They are used extensively in inter-server communications as well as for archival storage of data on disk.
similar to JSON, but it is smaller and faster.. plus it generates native language bindings !
At its core, protobuff lets you define your data's structure once using a .proto
file and then automatically generate code to read and write that data in various languages.
How do protocol buffer works ?
image credits : https://protobuf.dev/overview/
The code generated by protocol buffers provides utility methods to retrieve data from files and streams, extract individual values from the data, check if data exists, serialize data back to a file or stream, and other useful functions.
Let’s say you want to define a simple User object that holds basic information like name and age. In Protocol Buffers, you would first create a .proto
file to define this structure:
Step 1: Define the Data Structure
// person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
}
This defines a Person
message with a name and age.
Step 2: Compile the .proto
Use protoc
to generate code
protoc --go_out=. person.proto
Here i have compiled .proto
file for a Go code (as have used --go_out
), but protobuff
facilitates compiling into your required language.
How gRPC and Protobuf Work Together
By default, gRPC uses Protocol Buffers, Google’s mature open source mechanism for serializing structured data (although it can be used with other data formats such as JSON).
Defining Your Data
The first step when working with Protocol Buffers is to define the structure for the data you want to serialize in a .proto
file. This is an ordinary text file that serves as your data contract. Protocol buffer data is structured as messages
, where each message is a small logical record of information containing a series of name-value
pairs called fields
.
Here's one example :
message User {
string user_id = 1;
string name = 2;
string email = 3;
}
Once you’ve specified your data structures, you use the protocol buffer compiler, protoc
, to generate data access classes in your preferred language (e.g., Python, Java, Go) from your .proto
definition. These classes provide simple accessors for each field, like get_username()
and set_username()
, as well as methods to serialize the entire structure to and from raw bytes. So, if your chosen language is Python
, running the compiler on the example above will generate a class called UserProfile
.
Defining a Service
You define gRPC
services in the same .proto
files, specifying RPC
methods with their parameter and return types as protocol buffer
messages
. This is where gRPC
and Protobuf
come together to define a complete API contract
.
Here's an implementation of a service :
// The service definition for managing users.
service UserManagement {
// Retrieves a user's profile by their ID.
rpc GetUser(GetUserRequest) returns (User) {}
}
// The request message containing the user's ID.
message GetUserRequest {
string user_id = 1;
}
As you can see, the GetUser
RPC method takes a GetUserRequest
as its input and directly returns the User message we defined earlier as its output.
For gRPC
, you use protoc
with a special gRPC
plugin
. This process generates not only the regular protocol buffer code for your messages (User and GetUserRequest
) but also the generated gRPC client
and server
code. This includes the client "stubs" you'll use to make remote calls and the server interfaces you'll implement to bring your service to life.
gRPC Service Methods
gRPC provides 4 kinds of service methods as listed below -
1. Unary RPC
This is the classic and most common type of RPC, working just like a standard function call.
How it works: The client sends a single request message to the server and waits for the response. The server processes the request and returns a single response message.
Example: rpc GetUser(GetUserRequest) returns (User) {}
2. Server Streaming RPC
This method is useful when the server needs to send a large amount of data or a series of notifications to the client.
How it works: The client sends a single request message to the server. In response, the server sends back a stream of messages. The client can read from the stream until there are no more messages.
Example: rpc ListUsers(ListUsersRequest) returns (stream User) {}
The stream
keyword before the response type (User)
indicates this is a server stream.
3. Client Streaming RPC
This is the reverse of server streaming and is ideal for sending large datasets from the client to the server.
How it works: The client sends a stream of messages to the server. Once the client has finished writing the messages, it waits for the server to process them and return a single response.
Example: rpc CreateUsersInBulk(stream CreateUserRequest) returns (BulkCreateResponse) {}
The stream
keyword before the request type (CreateUserRequest)
indicates this is a client stream.
4. Bidirectional Streaming RPC
This is the most flexible method, allowing for a full-duplex conversation between the client and server.
How it works: The client initiates the call and both the client and server can send a stream of messages to each other. The two streams operate independently, so the client and server can read and write in any order they like. It's often used for things like live chat or real-time data exchange.
Example: rpc LiveStatusUpdate(stream StatusRequest) returns (stream StatusResponse) {}
The stream
keyword is used on both the request
and the response
types.
Implementing Bidirectional gRPC Streaming
This project is a real-time, terminal-based chat application built to demonstrate the language-agnostic capabilities of gRPC. It consists of three main components:
A Go-based Server (server/main.go): This is the central hub of the application. It listens for client
connections and uses a bidirectional gRPC stream to manage the flow of chat messages. When a message is received from one client, the server broadcasts it to all other connected clients.
package main
import (
"log"
"net"
"sync"
pb "grpc-chat/proto"
"google.golang.org/grpc"
)
type chatServer struct {
pb.UnimplementedChatServiceServer
mu sync.Mutex
streams map[string]pb.ChatService_ChatStreamServer
}
func newChatServer() *chatServer {
return &chatServer{
streams: make(map[string]pb.ChatService_ChatStreamServer),
}
}
func (s *chatServer) ChatStream(stream pb.ChatService_ChatStreamServer) error {
var user string
for {
msg, err := stream.Recv()
if err != nil {
log.Printf("Client disconnected or error: %v", err)
s.mu.Lock()
delete(s.streams, user)
s.mu.Unlock()
return err
}
user = msg.User
log.Printf("[%s]: %s", user, msg.Message)
s.mu.Lock()
s.streams[user] = stream
for u, st := range s.streams {
if u != user {
_ = st.Send(msg) // ignore error for simplicity
}
}
s.mu.Unlock()
}
}
func main() {
lis, err := net.Listen("tcp", ":50053")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterChatServiceServer(grpcServer, newChatServer())
log.Println("Chat server running on port 50053...")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
A Go-based Client (client/main.go): This is theclient
application that communicates with the Go
server
. It establishes a connection and opens a stream to both send user input and receive messages
from the server
.
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"time"
pb "grpc-chat/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("localhost:50053", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewChatServiceClient(conn)
stream, err := client.ChatStream(context.Background())
if err != nil {
log.Fatalf("Failed to open stream: %v", err)
}
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your name: ")
user, _ := reader.ReadString('\n')
user = user[:len(user)-1]
// Receiving goroutine
go func() {
for {
msg, err := stream.Recv()
if err != nil {
log.Printf("Receive error: %v", err)
break
}
fmt.Printf("\n[%s]: %s\n", msg.User, msg.Message)
}
}()
// Sending loop
for {
fmt.Print("You: ")
text, _ := reader.ReadString('\n')
stream.Send(&pb.ChatMessage{
User: user,
Message: text,
Timestamp: time.Now().Unix(),
})
}
}
A Python-based Client (client.py): This is the python
client, created to prove the core concept. It connects to the exact same Go server and performs the same functions as the Go client.
import grpc
import threading
import time
# Import the classes generated from your .proto file
import chat_pb2
import chat_pb2_grpc
def listen_for_messages(stub, response_iterator):
"""
This function runs in a separate thread, continuously listening for messages
from the server and printing them to the console.
"""
try:
# The 'for' loop will block and wait for new messages from the server
for message in response_iterator:
# When a message is received, print it and then re-print the input prompt
print(f"\n[{message.user}]: {message.message.strip()}")
print("You: ", end="", flush=True)
except grpc.RpcError as e:
# Handle cases where the server connection is lost
print(f"\nConnection lost. Error receiving message: {e}")
def run():
"""
Main function to set up the gRPC channel and manage the chat session.
"""
# Establish a connection to the gRPC server (your existing Go server)
with grpc.insecure_channel('localhost:50053') as channel:
# Create a client "stub"
stub = chat_pb2_grpc.ChatServiceStub(channel)
user = input("Enter your name: ")
def message_iterator():
"""
An iterator that yields messages from the user's input.
This is passed to the ChatStream method.
"""
while True:
text = input("You: ")
message = chat_pb2.ChatMessage(
user=user,
message=text,
timestamp=int(time.time())
)
yield message
# Call the ChatStream RPC. This returns an iterator for server responses.
response_iterator = stub.ChatStream(message_iterator())
# Start a new thread to listen for incoming messages from the server.
# This allows you to type messages while simultaneously receiving them.
threading.Thread(target=listen_for_messages, args=(stub, response_iterator), daemon=True).start()
# Keep the main thread alive to allow the background thread to continue running.
# The program will exit if the main thread finishes.
try:
while threading.main_thread().is_alive():
time.sleep(1)
except KeyboardInterrupt:
print("\nExiting chat.")
if __name__ == '__main__':
run()
The entire system is defined by a single Protobuf
file (proto/chat.proto)
, which acts as a universal contract
for the service's API
and message
structure. The key takeaway of this project is to show that as long as both the client and server adhere to this contract, their underlying programming languages don't matter, enabling seamless, high-performance communication in a mixed-language environment.
syntax = "proto3";
package chat;
option go_package = "grpc-chat/proto";
service ChatService {
rpc ChatStream(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user = 1;
string message = 2;
int64 timestamp = 3;
}
Project Directory :
Running the Project Locally
Before you start, make sure you have the following installed on your computer:
Go: Version 1.20 or later.
Python: Version 3.6 or later.
Install Dependencies & Generate Code
You'll need to install the dependencies for both Go
and Python
and then generate the Python-specific gRPC code from the .proto
file.
Go Dependencies
Open your terminal in the root directory and run the following command. This will download the Go modules (like gRPC and Protobuf) listed in your go.mod file.
go mod tidy
Python Dependencies & Code Generation
Next, set up the Python environment.
# 1. Create and activate a Python virtual environment
python -m venv venv
source venv/bin/activate # On Windows, use: .\\venv\\Scripts\\activate
# 2. Install the required Python packages
pip install grpcio grpcio-tools
# 3. Generate the Python gRPC code from the .proto file
python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. ./proto/chat.proto
After running the last command, you will see two new files in your project's root directory: chat_pb2.py
and chat_pb2_grpc.py
.
Start the Go Server (Terminal 1)
go run server/main.go
# Expected output:
# [DATE] [TIME] Chat server running on port 50053...
Run the Clients (Terminal 2 and beyond)
To run the Python
client
, open a new terminal, activate the virtual environment, and run:
# Make sure you are in the venv
# source venv/bin/activate
python client.py
To run the original Go client, open another new terminal and run:
go run client/main.go
Bidirectional Streaming in Action
You can start as many clients as you like. Messages sent from any client will appear in all other connected clients, demonstrating that your Go server is successfully communicating with clients written in different languages.
Why gRPC Is A Better Choice ?
Here's why many teams pick gRPC over other options for their services.
1. It's Fast. Really Fast. gRPC uses a modern protocol (HTTP/2)
and sends data in a compact binary format. This means less data over the wire and faster communication compared to sending bulky JSON
text.
2. Clear Rules, Fewer Bugs. The .proto
file acts as a strict contract
between the server
and client
. It gets rid of guesswork about data types and structures, so you spend less time debugging mismatches.
3. Built for Real-Time. Unlike traditional APIs, gRPC is designed for streaming. It can handle continuous data flows in either direction, which is perfect for live updates, chat apps, or IoT devices.
4. Works With Any Language. As you saw in this project, you can have a server in Go, a client in Python, and another service in Java, and they all talk to each other without a hitch. It gives teams the freedom to use the best tool for the job.
5.Built-in Deadline and Cancellation Control. gRPC lets clients specify how long they are willing to wait for a response. If the deadline is exceeded, the request is automatically canceled on both the client and server. This "cancellation propagation" prevents wasted resources on abandoned requests, making the entire system more resilient and predictable.
References
https://grpc.io/docs/
https://protobuf.dev/overview/
I'd love to hear your thoughts in the comments below. What has your experience been with building polyglot systems?
Thanks for following along, and happy coding!
Top comments (1)
Heyyy remember me? The other person in this project? Yeah, still waiting on that ZIP file you’ve been hiding like national treasure
Some comments have been hidden by the post's author - find out more