gRPC has emerged as a powerful framework for building high-performance, cross-platform APIs. At its core, it leverages HTTP/2 for transport and Protocol Buffers as its interface definition language, enabling efficient and strongly-typed communication between microservices. While many developers start with the basic request-response pattern, the true power of gRPC lies in its four distinct communication models. Understanding these patterns is key to designing robust and scalable systems.
In this article, we'll explore each of the four gRPC communication patterns—Unary, Server Streaming, Client Streaming, and Bidirectional Streaming—by implementing a simple chat service in Go. We will walk through the .proto
definition, server-side handlers, and client-side logic for each pattern.
Before we dive in, make sure you have the Go programming language and the Protocol Buffers compiler (protoc
) installed. You'll also need the Go plugins for gRPC and Protobufs:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
First, let's define our service in a .proto
file. We'll create a ChatService
with a method for each of the four communication types.
chat/chat.proto
syntax = "proto3";
package chat;
option go_package = "[github.com/your-repo/grpc-blog/chat](https://github.com/your-repo/grpc-blog/chat)";
// The request message containing the user's name and message.
message Message {
string user = 1;
string content = 2;
}
// The service definition.
service ChatService {
// Unary RPC: Sends a single message and gets a single response.
rpc SayHello(Message) returns (Message) {}
// Server Streaming RPC: Sends a single message and gets a stream of responses.
rpc BroadcastMessages(Message) returns (stream Message) {}
// Client Streaming RPC: Sends a stream of messages and gets a single response.
rpc UploadStream(stream Message) returns (Message) {}
// Bidirectional Streaming RPC: Sends a stream of messages and gets a stream of responses.
rpc ChatStream(stream Message) returns (stream Message) {}
}
After defining the service, we generate the Go code using protoc
:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
chat/chat.proto
This command generates chat/chat.pb.go
and chat/chat_grpc.pb.go
, which contain the necessary interfaces and structs for our server and client.
The Unary RPC is the simplest pattern, mirroring a traditional function call. The client sends a single request message to the server and receives a single response message back.
On the server, we implement the SayHello
method defined in our ChatServiceServer
interface. It takes a context and the request message as arguments and returns a response message and an error.
// server/main.go
type server struct {
chat.UnimplementedChatServiceServer
}
func (s *server) SayHello(ctx context.Context, in *chat.Message) (*chat.Message, error) {
log.Printf("Received Unary message from %s: %s", in.User, in.Content)
return &chat.Message{
User: "Server",
Content: "Hello, " + in.User + "! Thanks for your message.",
}, nil
}
The client creates a connection, instantiates a new client stub, and calls the remote method just like a local function.
// client/main.go
c := chat.NewChatServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := c.SayHello(ctx, &chat.Message{User: "Marko", Content: "This is a unary call."})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Unary Response from %s: %s", res.User, res.Content)
Use Cases: Perfect for simple, self-contained operations like creating a resource, fetching a user profile, or performing a status check.
In a Server Streaming RPC, the client sends a single request, but the server responds with a stream of messages. The server keeps the connection open and sends multiple messages back until it has no more data to send.
The server method receives the request and a stream object. It can then use the stream's Send()
method multiple times to send back responses.
// server/main.go
func (s *server) BroadcastMessages(in *chat.Message, stream chat.ChatService_BroadcastMessagesServer) error {
log.Printf("Received Broadcast request from %s", in.User)
messages := []chat.Message{
{User: "Server", Content: "Welcome to the broadcast!"},
{User: "Server", Content: "Here is message #1."},
{User: "Server", Content: "And here is message #2."},
{User: "Server", Content: "Broadcast finished."},
}
for _, msg := range messages {
if err := stream.Send(&msg); err != nil {
return err
}
time.Sleep(500 * time.Millisecond) // Simulate work
}
return nil
}
The client calls the method and receives a stream object in return. It then enters a loop, calling Recv()
to read messages from the stream until it receives an io.EOF
error, which signals that the stream has been closed by the server.
// client/main.go
stream, err := c.BroadcastMessages(ctx, &chat.Message{User: "Marko"})
if err != nil {
log.Fatalf("could not open broadcast stream: %v", err)
}
for {
msg, err := stream.Recv()
if err == io.EOF {
// Stream finished
break
}
if err != nil {
log.Fatalf("failed to receive broadcast message: %v", err)
}
log.Printf("Broadcast from %s: %s", msg.User, msg.Content)
}
Use Cases: Ideal for scenarios where a server needs to push updates to a client, such as stock tickers, live sports scores, or progress notifications for a long-running task.
The Client Streaming RPC is the inverse of server streaming. The client sends a sequence of messages to the server using a stream. Once the client has finished writing messages, it waits for the server to process them and return a single response.
The server's handler receives a stream object. It uses a loop with Recv()
to read all incoming messages from the client. When the client closes the stream, Recv()
returns an io.EOF
error. The server can then perform its final calculations and send a single response using SendAndClose()
.
// server/main.go
func (s *server) UploadStream(stream chat.ChatService_UploadStreamServer) error {
var messageCount int32
var lastUser string
for {
msg, err := stream.Recv()
if err == io.EOF {
log.Printf("Upload stream finished. Received %d messages.", messageCount)
return stream.SendAndClose(&chat.Message{
User: "Server",
Content: fmt.Sprintf("Finished receiving stream from %s. Got %d messages.", lastUser, messageCount),
})
}
if err != nil {
return err
}
lastUser = msg.User
messageCount++
log.Printf("Received message from client stream: %s", msg.Content)
}
}
The client obtains a stream object by calling the method. It then uses a loop to send multiple messages with Send()
. After finishing, it calls CloseAndRecv()
to notify the server that it's done and to receive the final summary response.
// client/main.go
stream, err := c.UploadStream(ctx)
if err != nil {
log.Fatalf("could not open upload stream: %v", err)
}
messages := []chat.Message{
{User: "Marko", Content: "Log entry 1"},
{User: "Marko", Content: "Log entry 2"},
{User: "Marko", Content: "Log entry 3"},
}
for _, msg := range messages {
if err := stream.Send(&msg); err != nil {
log.Fatalf("failed to send message to stream: %v", err)
}
time.Sleep(300 * time.Millisecond)
}
res, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("failed to receive response from upload stream: %v", err)
}
log.Printf("Upload Stream Response: %s", res.Content)
Use Cases: Excellent for large data uploads, such as sending log files, IoT device metrics, or chunks of a video file.
The Bidirectional Streaming RPC is the most flexible model. Both the client and server can send a stream of messages to each other independently over a single gRPC connection. The order of reads and writes is not guaranteed and depends on the application logic.
The server handler receives a stream that can be used for both reading and writing. It can read a message with Recv()
and write a response with Send()
at any time.
// server/main.go
func (s *server) ChatStream(stream chat.ChatService_ChatStreamServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
log.Printf("Received message in ChatStream from %s: %s", in.User, in.Content)
// Echo the message back
if err := stream.Send(&chat.Message{User: "Server", Content: "Echo: " + in.Content}); err != nil {
return err
}
}
}
The client-side logic is more complex as it needs to handle sending and receiving concurrently. A common pattern is to use two separate goroutines: one for sending messages and one for receiving them. A channel or sync.WaitGroup
is used to coordinate and signal completion.
// client/main.go
stream, err := c.ChatStream(context.Background()) // Use a background context
if err != nil {
log.Fatalf("could not open chat stream: %v", err)
}
var wg sync.WaitGroup
wg.Add(2)
// Goroutine for sending messages
go func() {
defer wg.Done()
messages := []chat.Message{
{User: "Marko", Content: "Hi!"},
{User: "Marko", Content: "How are you?"},
{User: "Marko", Content: "I'm using a bidi stream!"},
}
for _, msg := range messages {
log.Printf("Sending message: %s", msg.Content)
if err := stream.Send(&msg); err != nil {
log.Fatalf("failed to send message: %v", err)
}
time.Sleep(time.Second)
}
stream.CloseSend() // IMPORTANT: Close the sending side of the stream
}()
// Goroutine for receiving messages
go func() {
defer wg.Done()
for {
res, err := stream.Recv()
if err == io.EOF {
return // Server closed the stream
}
if err != nil {
log.Fatalf("failed to receive message: %v", err)
}
log.Printf("Received from server: %s", res.Content)
}
}()
wg.Wait()
log.Println("Bidirectional stream finished.")
Use Cases: The foundation for complex, interactive applications like real-time chat services, collaborative document editing, or live gaming sessions.
By mastering gRPC's four communication patterns, you unlock the ability to build highly efficient and specialized APIs tailored to your exact needs. While Unary RPCs cover many standard use cases, the streaming patterns provide the power to handle real-time data flow, large datasets, and fully interactive services. Choosing the right pattern is a critical step in designing a resilient and performant microservices architecture with Go and gRPC.