Let's create a simple gRPC-based calculator application in C++ using CMake. We'll have a CalculatorService
that performs basic operations like addition and subtraction. The server will expose this service, and the client will call the service to request operations.
Installation has been done with vcpk
. Please check the corresponding files.
The content of vcpkg.json
:
{
"name": "microservices",
"version-string": "1.1.0",
"dependencies": [
{ "name": "grpc" }
]
}
and CMakeLists.txt
:
cmake_minimum_required(VERSION 3.14)
project(microservices CXX)
if (NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE PATH "toolchain file")
endif()
message("toolchain file: ${CMAKE_TOOLCHAIN_FILE}")
# Find Protobuf and gRPC packages
find_package(Protobuf CONFIG REQUIRED)
find_package(gRPC CONFIG REQUIRED)
message("gRPC_FOUND: "${gRPC_FOUND})
message("gRPC_VERSION: "${gRPC_VERSION})
message("Protobuf_FOUND: "${Protobuf_FOUND})
message("Protobuf_VERSION: "${Protobuf_VERSION})
Now run:
cmake -S . -B build -G "Ninja Multi-Config" -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake
.
├── CMakeLists.txt
├── proto
│ └── calculator.proto
├── src
│ ├── client.cpp
│ └── server.cpp
└── vcpkg.json
This .proto
file defines the service and message types that gRPC will use to generate code for both the server and client.
calculator.proto:
syntax = "proto3";
package calculator;
// The request message containing two numbers.
message CalcRequest {
double number1 = 1;
double number2 = 2;
}
// The response message containing the result.
message CalcResponse {
double result = 1;
}
// The Calculator service definition.
service CalculatorService {
// Performs addition of two numbers.
rpc Add(CalcRequest) returns (CalcResponse);
// Performs subtraction of two numbers.
rpc Subtract(CalcRequest) returns (CalcResponse);
}
- message CalcRequest: This message defines two numbers (
number1
andnumber2
) for the input. - message CalcResponse: This message will hold the result of the calculation.
- CalculatorService: Defines two RPC methods,
Add
andSubtract
, which accept aCalcRequest
and return aCalcResponse
.
Using the .proto
file, you will generate C++ classes for gRPC. Assuming you have the protoc
and gRPC plugins available, run the following commands in your terminal:
The vcpkg
build the protoc
in build/vcpkg_installed/x64-linux/tools/protobuf
, and grpc_cpp_plugin
in build/vcpkg_installed/x64-linux/tools/grpc/
so from the root of the project, add them to the path:
export PATH=$PWD/build/vcpkg_installed/x64-linux/tools/protobuf:$PWD/build/vcpkg_installed/x64-linux/tools/grpc/:$PATH
Then go to proto
directory
cd proto
protoc -I=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` calculator.proto
protoc -I=. --cpp_out=. calculator.proto
This will generate calculator.grpc.pb.cc
, calculator.grpc.pb.h
, calculator.pb.cc
, and calculator.pb.h
files, which we will use in our client and server code.
copy these file into your generated
directory:
.
├── CMakeLists.txt
├── generated
│ ├── calculator.grpc.pb.cc
│ ├── calculator.grpc.pb.h
│ ├── calculator.pb.cc
│ └── calculator.pb.h
├── proto
│ └── calculator.proto
├── src
│ ├── client.cpp
│ └── server.cpp
└── vcpkg.json
server.cpp:
// Implementation of the Calculator Service
class CalculatorServiceImpl final : public CalculatorService::Service {
Status Add(ServerContext* context, const CalcRequest* request, CalcResponse* response) override {
double sum = request->number1() + request->number2();
response->set_result(sum);
return Status::OK;
}
Status Subtract(ServerContext* context, const CalcRequest* request, CalcResponse* response) override {
double diff = request->number1() - request->number2();
response->set_result(diff);
return Status::OK;
}
};
void RunServer() {
std::string server_address("0.0.0.0:50051");
CalculatorServiceImpl service;
ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait();
}
int main() {
RunServer();
return 0;
}
- CalculatorServiceImpl: Implements the service defined in the
proto
file. It overrides theAdd
andSubtract
methods to provide logic for the operations. - RunServer: Sets up and starts the gRPC server on port
50051
.
client.cpp:
class CalculatorClient {
public:
CalculatorClient(std::shared_ptr<Channel> channel)
: stub_(CalculatorService::NewStub(channel)) {}
double Add(double num1, double num2) {
CalcRequest request;
request.set_number1(num1);
request.set_number2(num2);
CalcResponse response;
ClientContext context;
Status status = stub_->Add(&context, request, &response);
if (status.ok()) {
return response.result();
} else {
std::cout << "RPC failed" << std::endl;
return 0.0;
}
}
double Subtract(double num1, double num2) {
CalcRequest request;
request.set_number1(num1);
request.set_number2(num2);
CalcResponse response;
ClientContext context;
Status status = stub_->Subtract(&context, request, &response);
if (status.ok()) {
return response.result();
} else {
std::cout << "RPC failed" << std::endl;
return 0.0;
}
}
private:
std::unique_ptr<CalculatorService::Stub> stub_;
};
int main() {
CalculatorClient client(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));
double num1 = 10.0;
double num2 = 5.0;
std::cout << "Add: " << client.Add(num1, num2) << std::endl;
std::cout << "Subtract: " << client.Subtract(num1, num2) << std::endl;
return 0;
}
- CalculatorClient: Defines a client that connects to the server and calls the
Add
andSubtract
methods. It sets up an RPC request, sends it to the server, and retrieves the result.
Here's how you can set up the CMake build file.
set(GENERATED_DIR "${CMAKE_SOURCE_DIR}/generated" )
set(PROTO_SRCS "${GENERATED_DIR}/calculator.pb.cc" )
set(GRPC_SRCS "${GENERATED_DIR}/calculator.grpc.pb.cc")
include_directories(${GENERATED_DIR})
add_executable(server src/server.cpp ${PROTO_SRCS} ${GRPC_SRCS})
add_executable(client src/client.cpp ${PROTO_SRCS} ${GRPC_SRCS})
target_link_libraries(server PRIVATE gRPC::grpc++ protobuf::libprotobuf)
target_link_libraries(client PRIVATE gRPC::grpc++ protobuf::libprotobuf)
In the root of the project, run:
cmake -S . -B build -G "Ninja Multi-Config" -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake
Start the server in one terminal:
./server
Run the client in another terminal:
./client
Let's reimplement the previous microservices example using gRPC in C++. We'll define service interfaces using Protocol Buffers (.proto
files), generate the necessary C++ code, and implement each service.
gRPC is a high-performance, open-source RPC framework that uses Protocol Buffers as its interface definition language and data serialization mechanism. It allows defining services, and the method parameters and return types are specified as Protocol Buffer message types.
We'll recreate the four services:
- User Service: Provides user information.
- Product Service: Provides product catalog information.
- Order Service: Creates orders by communicating with the User and Product services.
- Payment Service: Processes payments.
- C++ Compiler: Supports C++11 or higher.
- Protocol Buffers Compiler (
protoc
): To compile.proto
files. - gRPC C++ Libraries: Installed and configured.
- CMake: For building the project.
We'll start by creating .proto
files for each service.
syntax = "proto3";
package userservice;
// The user service definition.
service UserService {
rpc GetUserInfo(UserRequest) returns (UserResponse);
}
// The request message containing the user ID.
message UserRequest {
string user_id = 1;
}
// The response message containing user information.
message UserResponse {
string name = 1;
string email = 2;
}
syntax = "proto3";
package productservice;
service ProductService {
rpc GetProductCatalog(ProductRequest) returns (ProductResponse);
}
message ProductRequest {
}
message Product {
string name = 1;
double price = 2;
}
message ProductResponse {
repeated Product products = 1;
}
syntax = "proto3";
package orderservice;
service OrderService {
rpc CreateOrder(OrderRequest) returns (OrderResponse);
}
message OrderRequest {
string user_id = 1;
repeated string product_names = 2;
}
message OrderResponse {
string order_id = 1;
bool success = 2;
}
syntax = "proto3";
package paymentservice;
service PaymentService {
rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);
}
message PaymentRequest {
double amount = 1;
}
message PaymentResponse {
bool success = 1;
string message = 2;
}
Use the protoc
compiler with the gRPC plugin to generate C++ code.
# Generate code for User Service
protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` user_service.proto
protoc -I . --cpp_out=. user_service.proto
# Generate code for Product Service
protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` product_service.proto
protoc -I . --cpp_out=. product_service.proto
# Generate code for Order Service
protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` order_service.proto
protoc -I . --cpp_out=. order_service.proto
# Generate code for Payment Service
protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` payment_service.proto
protoc -I . --cpp_out=. payment_service.proto
This will produce .pb.h
and .pb.cc
files for each service.
Implementation: Returns static user information.
#ifndef USER_SERVICE_IMPL_H
#define USER_SERVICE_IMPL_H
#include "user_service.grpc.pb.h"
class UserServiceImpl final : public userservice::UserService::Service {
public:
grpc::Status GetUserInfo(grpc::ServerContext* context, const userservice::UserRequest* request,
userservice::UserResponse* response) override {
// For demonstration, return static user info
response->set_name("John Doe");
response->set_email("[email protected]");
return grpc::Status::OK;
}
};
#endif // USER_SERVICE_IMPL_H
#include "user_service_impl.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
void RunServer() {
std::string server_address("0.0.0.0:50051");
UserServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "User Service listening on " << server_address << std::endl;
server->Wait();
}
int main() {
RunServer();
return 0;
}
Implementation: Returns a static product catalog.
#ifndef PRODUCT_SERVICE_IMPL_H
#define PRODUCT_SERVICE_IMPL_H
#include "product_service.grpc.pb.h"
class ProductServiceImpl final : public productservice::ProductService::Service {
public:
grpc::Status GetProductCatalog(grpc::ServerContext* context, const productservice::ProductRequest* request,
productservice::ProductResponse* response) override {
// Add first product
auto* product1 = response->add_products();
product1->set_name("Laptop");
product1->set_price(1200.50);
// Add second product
auto* product2 = response->add_products();
product2->set_name("Headphones");
product2->set_price(200.99);
return grpc::Status::OK;
}
};
#endif // PRODUCT_SERVICE_IMPL_H
#include "product_service_impl.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
void RunServer() {
std::string server_address("0.0.0.0:50052");
ProductServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Product Service listening on " << server_address << std::endl;
server->Wait();
}
int main() {
RunServer();
return 0;
}
Implementation: Processes payment and returns success.
#ifndef PAYMENT_SERVICE_IMPL_H
#define PAYMENT_SERVICE_IMPL_H
#include "payment_service.grpc.pb.h"
#include <iostream>
class PaymentServiceImpl final : public paymentservice::PaymentService::Service {
public:
grpc::Status ProcessPayment(grpc::ServerContext* context, const paymentservice::PaymentRequest* request,
paymentservice::PaymentResponse* response) override {
double amount = request->amount();
std::cout << "Processing payment of $" << amount << std::endl;
// Assume payment is successful
response->set_success(true);
response->set_message("Payment processed successfully");
return grpc::Status::OK;
}
};
#endif // PAYMENT_SERVICE_IMPL_H
#include "payment_service_impl.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
void RunServer() {
std::string server_address("0.0.0.0:50053");
PaymentServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Payment Service listening on " << server_address << std::endl;
server->Wait();
}
int main() {
RunServer();
return 0;
}
Implementation: Acts as a client to other services.
#ifndef ORDER_SERVICE_IMPL_H
#define ORDER_SERVICE_IMPL_H
#include "order_service.grpc.pb.h"
#include "user_service.grpc.pb.h"
#include "product_service.grpc.pb.h"
#include "payment_service.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
class OrderServiceImpl final : public orderservice::OrderService::Service {
public:
OrderServiceImpl(std::shared_ptr<grpc::Channel> user_channel,
std::shared_ptr<grpc::Channel> product_channel,
std::shared_ptr<grpc::Channel> payment_channel)
: user_stub_(userservice::UserService::NewStub(user_channel)),
product_stub_(productservice::ProductService::NewStub(product_channel)),
payment_stub_(paymentservice::PaymentService::NewStub(payment_channel)) {}
grpc::Status CreateOrder(grpc::ServerContext* context, const orderservice::OrderRequest* request,
orderservice::OrderResponse* response) override {
// Get user info
userservice::UserRequest user_request;
user_request.set_user_id(request->user_id());
userservice::UserResponse user_response;
grpc::ClientContext user_context;
grpc::Status user_status = user_stub_->GetUserInfo(&user_context, user_request, &user_response);
if (!user_status.ok()) {
std::cerr << "User Service error: " << user_status.error_message() << std::endl;
response->set_success(false);
return user_status;
}
// Get product catalog
productservice::ProductRequest product_request;
productservice::ProductResponse product_response;
grpc::ClientContext product_context;
grpc::Status product_status = product_stub_->GetProductCatalog(&product_context, product_request, &product_response);
if (!product_status.ok()) {
std::cerr << "Product Service error: " << product_status.error_message() << std::endl;
response->set_success(false);
return product_status;
}
// Calculate total amount
double total_amount = 0;
for (const auto& product_name : request->product_names()) {
for (const auto& product : product_response.products()) {
if (product.name() == product_name) {
total_amount += product.price();
break;
}
}
}
// Process payment
paymentservice::PaymentRequest payment_request;
payment_request.set_amount(total_amount);
paymentservice::PaymentResponse payment_response;
grpc::ClientContext payment_context;
grpc::Status payment_status = payment_stub_->ProcessPayment(&payment_context, payment_request, &payment_response);
if (!payment_status.ok() || !payment_response.success()) {
std::cerr << "Payment Service error: " << payment_status.error_message() << std::endl;
response->set_success(false);
return payment_status;
}
// Order creation successful
response->set_order_id("order_12345"); // In real scenarios, generate unique IDs
response->set_success(true);
std::cout << "Order created successfully for user: " << user_response.name() << std::endl;
return grpc::Status::OK;
}
private:
std::unique_ptr<userservice::UserService::Stub> user_stub_;
std::unique_ptr<productservice::ProductService::Stub> product_stub_;
std::unique_ptr<paymentservice::PaymentService::Stub> payment_stub_;
};
#endif // ORDER_SERVICE_IMPL_H
#include "order_service_impl.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
void RunServer() {
std::string server_address("0.0.0.0:50054");
// Channels to other services
auto user_channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
auto product_channel = grpc::CreateChannel("localhost:50052", grpc::InsecureChannelCredentials());
auto payment_channel = grpc::CreateChannel("localhost:50053", grpc::InsecureChannelCredentials());
OrderServiceImpl service(user_channel, product_channel, payment_channel);
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Order Service listening on " << server_address << std::endl;
server->Wait();
}
int main() {
RunServer();
return 0;
}
Create a CMakeLists.txt
file to build all services.
cmake_minimum_required(VERSION 3.5)
project(MicroservicesWithGRPC)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(Protobuf REQUIRED)
find_package(gRPC CONFIG REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
# Function to generate protobuf files
function(generate_proto target_name proto_file)
get_filename_component(proto_abs ${proto_file} ABSOLUTE)
get_filename_component(proto_name ${proto_file} NAME_WE)
# Generate the sources
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${proto_abs})
grpc_generate_cpp(GRPC_SRCS GRPC_HDRS ${proto_abs})
# Create library
add_library(${target_name} ${PROTO_SRCS} ${PROTO_HDRS} ${GRPC_SRCS} ${GRPC_HDRS})
target_link_libraries(${target_name} PUBLIC protobuf::libprotobuf gRPC::grpc++)
set(${target_name}_SRCS ${PROTO_SRCS} PARENT_SCOPE)
set(${target_name}_HDRS ${PROTO_HDRS} PARENT_SCOPE)
endfunction()
# User Service
generate_proto(user_service_proto user_service.proto)
add_executable(user_service_server user_service_server.cc)
target_link_libraries(user_service_server user_service_proto)
# Product Service
generate_proto(product_service_proto product_service.proto)
add_executable(product_service_server product_service_server.cc)
target_link_libraries(product_service_server product_service_proto)
# Payment Service
generate_proto(payment_service_proto payment_service.proto)
add_executable(payment_service_server payment_service_server.cc)
target_link_libraries(payment_service_server payment_service_proto)
# Order Service
generate_proto(order_service_proto order_service.proto)
add_executable(order_service_server order_service_server.cc)
target_link_libraries(order_service_server
order_service_proto
user_service_proto
product_service_proto
payment_service_proto)
In the project directory:
mkdir build
cd build
cmake ..
make
Open separate terminals for each service.
-
Terminal 1: Run User Service
./user_service_server
-
Terminal 2: Run Product Service
./product_service_server
-
Terminal 3: Run Payment Service
./payment_service_server
-
Terminal 4: Run Order Service
./order_service_server
Create a simple client to test the Order Service.
#include "order_service.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
#include <string>
int main() {
auto channel = grpc::CreateChannel("localhost:50054", grpc::InsecureChannelCredentials());
auto stub = orderservice::OrderService::NewStub(channel);
orderservice::OrderRequest request;
request.set_user_id("user_123");
request.add_product_names("Laptop");
request.add_product_names("Headphones");
orderservice::OrderResponse response;
grpc::ClientContext context;
grpc::Status status = stub->CreateOrder(&context, request, &response);
if (status.ok()) {
if (response.success()) {
std::cout << "Order created successfully! Order ID: " << response.order_id() << std::endl;
} else {
std::cout << "Order creation failed." << std::endl;
}
} else {
std::cout << "RPC failed: " << status.error_message() << std::endl;
}
return 0;
}
Update CMakeLists.txt
to include the client:
add_executable(order_service_client order_service_client.cc)
target_link_libraries(order_service_client
order_service_proto
user_service_proto
product_service_proto
payment_service_proto)
Rebuild the project:
make
Run the client:
./order_service_client
- Microservices Implemented with gRPC: We reimplemented the services using gRPC in C++.
- Service Definitions: Defined services using Protocol Buffers.
- Inter-Service Communication: The Order Service communicates with other services via gRPC calls.
- Scalable Architecture: Each service can be independently deployed and scaled.
- Error Handling: The code includes basic error handling; in a production environment, you would enhance this.
- Security: Authentication and encryption (e.g., SSL/TLS) should be added for secure communication.
- Service Discovery: For scalable deployment, consider implementing service discovery mechanisms.
Follow the instructions from the gRPC C++ Quick Start Guide.
Ensure you have CMake 3.13 or higher for the FindProtobuf
and FindgRPC
modules.
Using gRPC for implementing microservices in C++ allows for efficient, strongly-typed, and scalable inter-service communication. This example demonstrates the foundational steps to build such a system. For a production environment, you would expand upon this with more robust error handling, security features, logging, and configuration management.
If you have any questions or need further clarification on any part of the implementation, feel free to ask!