본문으로 건너뛰기 gRPC and Protocol Buffers in C++: From .proto to Production

gRPC and Protocol Buffers in C++: From .proto to Production

gRPC and Protocol Buffers in C++: From .proto to Production

Why gRPC Over REST for Service-to-Service?

REST with JSON works well for browser-facing APIs. For internal service-to-service communication, gRPC has practical advantages:

  • Strong typing: the .proto schema is the contract — no undocumented JSON shapes
  • Binary encoding: Protocol Buffers typically encode 3-10x smaller than equivalent JSON
  • HTTP/2 multiplexing: many RPCs share one connection with flow control
  • Built-in streaming: server streaming, client streaming, and bidirectional streaming are first-class
  • Code generation: client and server stubs in C++, Go, Python, Java, and others from one .proto file

1. Protocol Buffers Basics

Define messages and service interfaces in .proto files:

// user_service.proto
syntax = "proto3";

package myapp;

// Messages define the data structures
message GetUserRequest {
  int32 user_id = 1;  // field number = 1, must never change
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;  // Unix timestamp
  repeated string roles = 5;  // array of strings
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

// Service defines the RPC methods
service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
  rpc CreateUser(CreateUserRequest) returns (UserResponse);

  // Streaming variants (covered later)
  rpc ListUsers(ListUsersRequest) returns (stream UserResponse);
}

Field numbers are permanent — the number (not the name) identifies a field in the binary encoding. Never reuse a field number for a different field; mark removed fields as reserved instead:

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  reserved 3;  // was "phone", removed — prevents accidental reuse
  string email = 4;
}

Generate C++ Code

# Install protoc and grpc_cpp_plugin (varies by platform)
# Ubuntu: apt install protobuf-compiler-grpc libgrpc++-dev
# macOS: brew install grpc

# Generate C++ stubs
protoc \
  --cpp_out=./generated \
  --grpc_out=./generated \
  --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \
  user_service.proto

# Creates:
# generated/user_service.pb.h        — message classes
# generated/user_service.pb.cc
# generated/user_service.grpc.pb.h   — service/stub classes
# generated/user_service.grpc.pb.cc

2. gRPC C++ Server

// server.cc
#include <grpcpp/grpcpp.h>
#include "generated/user_service.grpc.pb.h"
#include <memory>
#include <iostream>

class UserServiceImpl final : public myapp::UserService::Service {
public:
    grpc::Status GetUser(
        grpc::ServerContext* ctx,
        const myapp::GetUserRequest* req,
        myapp::UserResponse* resp) override
    {
        // Validate input
        if (req->user_id() <= 0) {
            return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                "user_id must be positive");
        }

        // Simulate database lookup
        if (req->user_id() == 1) {
            resp->set_user_id(1);
            resp->set_name("Alice");
            resp->set_email("alice@example.com");
            resp->set_created_at(1700000000);
            resp->add_roles("admin");
            resp->add_roles("user");
            return grpc::Status::OK;
        }

        return grpc::Status(grpc::StatusCode::NOT_FOUND,
            "user " + std::to_string(req->user_id()) + " not found");
    }

    grpc::Status CreateUser(
        grpc::ServerContext* ctx,
        const myapp::CreateUserRequest* req,
        myapp::UserResponse* resp) override
    {
        if (req->name().empty() || req->email().empty()) {
            return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                "name and email are required");
        }

        // Create user in database...
        resp->set_user_id(42);
        resp->set_name(req->name());
        resp->set_email(req->email());
        return grpc::Status::OK;
    }
};

void RunServer() {
    std::string address = "0.0.0.0:50051";
    UserServiceImpl service;

    grpc::ServerBuilder builder;
    builder.AddListeningPort(address, grpc::InsecureServerCredentials());
    builder.RegisterService(&service);

    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << address << '\n';
    server->Wait();
}

int main() {
    RunServer();
}

3. gRPC C++ Client

// client.cc
#include <grpcpp/grpcpp.h>
#include "generated/user_service.grpc.pb.h"
#include <memory>
#include <iostream>

class UserServiceClient {
    std::unique_ptr<myapp::UserService::Stub> stub_;

public:
    explicit UserServiceClient(std::shared_ptr<grpc::Channel> channel)
        : stub_(myapp::UserService::NewStub(channel)) {}

    std::optional<myapp::UserResponse> GetUser(int32_t userId) {
        myapp::GetUserRequest request;
        request.set_user_id(userId);

        myapp::UserResponse response;
        grpc::ClientContext ctx;

        // Set deadline — always set deadlines on client calls
        ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));

        grpc::Status status = stub_->GetUser(&ctx, request, &response);

        if (status.ok()) {
            return response;
        }

        std::cerr << "GetUser failed: ["
            << status.error_code() << "] "
            << status.error_message() << '\n';
        return std::nullopt;
    }
};

int main() {
    // Create a channel — reuse it across calls, don't create per-call
    auto channel = grpc::CreateChannel("localhost:50051",
        grpc::InsecureChannelCredentials());

    UserServiceClient client(channel);

    auto user = client.GetUser(1);
    if (user) {
        std::cout << "User: " << user->name()
                  << " (" << user->email() << ")\n";
        for (const auto& role : user->roles()) {
            std::cout << "  role: " << role << '\n';
        }
    }
}

4. Streaming RPCs

gRPC has three streaming modes in addition to basic unary RPC:

Server Streaming (server sends multiple responses)

// Server sends a stream of UserResponse
rpc ListUsers(ListUsersRequest) returns (stream UserResponse);
// Server implementation
grpc::Status ListUsers(
    grpc::ServerContext* ctx,
    const myapp::ListUsersRequest* req,
    grpc::ServerWriter<myapp::UserResponse>* writer) override
{
    // Fetch and stream rows from database
    for (const auto& user : db.getAllUsers()) {
        myapp::UserResponse resp;
        resp.set_user_id(user.id);
        resp.set_name(user.name);

        if (!writer->Write(resp)) {
            break;  // client disconnected or cancelled
        }

        if (ctx->IsCancelled()) {
            return grpc::Status(grpc::StatusCode::CANCELLED, "cancelled");
        }
    }
    return grpc::Status::OK;
}

// Client usage
grpc::ClientContext ctx;
myapp::ListUsersRequest req;
auto reader = stub_->ListUsers(&ctx, req);

myapp::UserResponse resp;
while (reader->Read(&resp)) {
    std::cout << resp.name() << '\n';
}

grpc::Status status = reader->Finish();
if (!status.ok()) { /* handle error */ }

Client Streaming (client sends multiple messages)

// Client sends a stream of records to import
rpc BulkImport(stream ImportRecord) returns (ImportResult);
// Client usage
grpc::ClientContext ctx;
myapp::ImportResult result;
auto writer = stub_->BulkImport(&ctx, &result);

for (const auto& record : localData) {
    myapp::ImportRecord req;
    req.set_data(record.serialize());
    if (!writer->Write(req)) break;  // server closed early
}

writer->WritesDone();
grpc::Status status = writer->Finish();

5. Error Handling

gRPC status codes map to common error categories:

Status codeMeaningExample use
OKSuccess
INVALID_ARGUMENTBad client inputMissing required field
NOT_FOUNDResource doesn’t existUser ID not in database
ALREADY_EXISTSDuplicateEmail already registered
PERMISSION_DENIEDAuth failedInvalid token
UNAVAILABLEServer temporarily downDatabase connection failed
DEADLINE_EXCEEDEDTimeoutSlow query
RESOURCE_EXHAUSTEDRate limit or quotaToo many requests
// Server — return meaningful status codes
if (!db.userExists(req->user_id())) {
    return grpc::Status(grpc::StatusCode::NOT_FOUND,
        "user " + std::to_string(req->user_id()) + " not found");
}

// Client — check and handle each code
grpc::Status status = stub_->GetUser(&ctx, request, &response);
switch (status.error_code()) {
    case grpc::StatusCode::OK:
        break;
    case grpc::StatusCode::NOT_FOUND:
        std::cerr << "User not found\n";
        break;
    case grpc::StatusCode::DEADLINE_EXCEEDED:
        std::cerr << "Request timed out — retry?\n";
        break;
    default:
        std::cerr << "Error " << status.error_code()
                  << ": " << status.error_message() << '\n';
}

6. TLS for Production

Never use InsecureChannelCredentials in production. Use TLS:

// Server with TLS
grpc::SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_key_cert_pairs.push_back({
    ReadFile("server.key"),
    ReadFile("server.crt"),
});
// Optionally add CA cert for mutual TLS
ssl_opts.pem_root_certs = ReadFile("ca.crt");

builder.AddListeningPort("0.0.0.0:443",
    grpc::SslServerCredentials(ssl_opts));

// Client with TLS
grpc::SslCredentialsOptions ssl_client_opts;
ssl_client_opts.pem_root_certs = ReadFile("ca.crt");  // CA to verify server

auto channel = grpc::CreateChannel("service.example.com:443",
    grpc::SslCredentials(ssl_client_opts));

7. Production Checklist

  • Always set deadlines: ctx.set_deadline(std::chrono::system_clock::now() + timeout) — missing deadlines cause requests to hang indefinitely
  • Reuse channels and stubs: creating a channel per request is expensive (TLS handshake, HTTP/2 setup) — create once and share
  • Check IsCancelled() in streaming handlers: clients may disconnect; don’t keep writing to a closed stream
  • Handle UNAVAILABLE with retry: transient failures (network blip, pod restart) should trigger retry with exponential backoff
  • Use reserved for removed fields: prevents accidental reuse of field numbers in future .proto changes
  • Health checks: implement the gRPC health checking protocol for load balancer integration
  • Message size limits: large payloads hit the default 4MB limit — set grpc.max_receive_message_length in ChannelArguments

Key Takeaways

  • .proto files define the schema — field numbers are permanent identifiers, not the names
  • protoc generates typed C++ classes; the client stub and server base class come from grpc_cpp_plugin
  • Every unary RPC returns grpc::Status — always check status.ok() before using the response
  • Streaming modes (server, client, bidi) are first-class in gRPC — use them for large payloads or live feeds
  • Always set deadlines on client calls — no deadline means the call may wait indefinitely
  • Reuse channels — they are expensive to create; create one per target service and share it
  • Use TLS (SslServerCredentials, SslCredentials) in any non-local environment

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Build C++ gRPC microservices with Protocol Buffers: .

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, gRPC, Protocol Buffers, RPC, Microservices.