gRPC is a high-performance, open-source universal RPC framework that can connect services in any language.
Let’s build a simple "Hello, World!" gRPC service. We’ll create a Greeter service that accepts a name and returns a greeting.
First, we need to define our service using Protocol Buffers (protobuf). This .proto file acts as our contract, specifying the services and messages.
syntax = "proto3";
package greeter;
// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
Save this as greeter.proto.
Now, we need to generate code from this .proto file for both our server and client. We’ll use the protoc compiler with the Go plugins. Make sure you have protoc and the Go plugins installed.
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
greeter.proto
This command generates two Go files: greeter.pb.go (for message serialization/deserialization) and greeter_grpc.pb.go (for the gRPC service interfaces and client stubs).
Next, let’s build the server. We’ll implement the Greeter service defined in our .proto file.
package main
import (
"context"
"fmt"
"log"
"net"
pb "your_module_path/greeter" // Replace with your module path
"google.golang.org/grpc"
)
// server is used to implement greeter.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements greeter.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
This server starts listening on port 50051. It registers our server implementation with the gRPC server.
Now, let’s create the client that will call our SayHello RPC.
package main
import (
"context"
"log"
"time"
pb "your_module_path/greeter" // Replace with your module path
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
address = "localhost:50051"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print its response.
name := "gRPC"
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
This client connects to the server, creates a GreeterClient, and calls the SayHello method with the name "gRPC".
To run this:
- Save the
.protofile. - Generate Go code.
- Save the server code (e.g.,
server/main.go). - Save the client code (e.g.,
client/main.go). - Run the server:
go run server/main.go - In a separate terminal, run the client:
go run client/main.go
You should see the server log "Received: gRPC" and the client log "Greeting: Hello gRPC".
The most surprising true thing about gRPC is that its performance advantage isn’t solely due to HTTP/2, but heavily influenced by Protocol Buffers’ efficient binary serialization.
The fundamental problem gRPC solves is making inter-service communication predictable, efficient, and language-agnostic. It abstracts away the network complexities, allowing developers to focus on business logic. Internally, gRPC uses Protocol Buffers for defining service contracts and serializing messages. This means instead of sending verbose JSON or XML over the wire, you’re sending compact binary data. The HTTP/2 connection provides features like multiplexing, header compression, and server push, further enhancing performance. When a client calls a gRPC method, the client library serializes the request message using protobuf, sends it over an HTTP/2 connection, and the server deserializes it, processes the request, serializes the response, and sends it back. The generated Go code provides the necessary stubs and interfaces to handle this seamlessly.
When you use grpc.Dial with insecure.NewCredentials(), you are explicitly telling gRPC not to bother with TLS encryption. This is fine for local development or trusted networks, but for production, you’ll want to use TLS. The insecure.NewCredentials() function is a convenience for testing and development environments where security is not a primary concern. In production, you would typically use credentials.NewClientTLSFromFile("ca.crt", "client.crt", "client.key") on the client and grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromFile("server.crt", "server.key"))) on the server, ensuring encrypted and authenticated communication.
The next concept to explore is how to handle streaming RPCs in gRPC, which allows for more complex interactions beyond simple request-response.