|
| 1 | +## gRPC |
| 2 | +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. |
| 3 | + |
| 4 | +### Installation |
| 5 | +Installation has been done with `vcpk`. Please check the corresponding files. |
| 6 | + run this |
| 7 | + |
| 8 | +The content of `vcpkg.json`: |
| 9 | + |
| 10 | +``` |
| 11 | + { |
| 12 | + "name": "microservices", |
| 13 | + "version-string": "1.1.0", |
| 14 | + "dependencies": [ |
| 15 | + { "name": "grpc" } |
| 16 | + ] |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +and `CMakeLists.txt`: |
| 21 | + |
| 22 | + |
| 23 | +``` |
| 24 | + cmake_minimum_required(VERSION 3.14) |
| 25 | +project(microservices CXX) |
| 26 | +
|
| 27 | +if (NOT DEFINED CMAKE_TOOLCHAIN_FILE) |
| 28 | + set(CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE PATH "toolchain file") |
| 29 | +endif() |
| 30 | +message("toolchain file: ${CMAKE_TOOLCHAIN_FILE}") |
| 31 | +
|
| 32 | +# Find Protobuf and gRPC packages |
| 33 | +find_package(Protobuf CONFIG REQUIRED) |
| 34 | +find_package(gRPC CONFIG REQUIRED) |
| 35 | +
|
| 36 | +message("gRPC_FOUND: "${gRPC_FOUND}) |
| 37 | +message("gRPC_VERSION: "${gRPC_VERSION}) |
| 38 | +
|
| 39 | +
|
| 40 | +message("Protobuf_FOUND: "${Protobuf_FOUND}) |
| 41 | +message("Protobuf_VERSION: "${Protobuf_VERSION}) |
| 42 | +``` |
| 43 | + |
| 44 | +Now run: |
| 45 | + |
| 46 | +``` |
| 47 | +cmake -S . -B build -G "Ninja Multi-Config" -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake |
| 48 | +``` |
| 49 | + |
| 50 | +### Project Structure: |
| 51 | +``` |
| 52 | +. |
| 53 | +├── CMakeLists.txt |
| 54 | +├── proto |
| 55 | +│ └── calculator.proto |
| 56 | +├── src |
| 57 | +│ ├── client.cpp |
| 58 | +│ └── server.cpp |
| 59 | +└── vcpkg.json |
| 60 | +
|
| 61 | +``` |
| 62 | + |
| 63 | +### Step 1: Define the `calculator.proto` file |
| 64 | +This `.proto` file defines the service and message types that gRPC will use to generate code for both the server and client. |
| 65 | + |
| 66 | +**calculator.proto**: |
| 67 | +```proto |
| 68 | +syntax = "proto3"; |
| 69 | +
|
| 70 | +package calculator; |
| 71 | +
|
| 72 | +// The request message containing two numbers. |
| 73 | +message CalcRequest { |
| 74 | + double number1 = 1; |
| 75 | + double number2 = 2; |
| 76 | +} |
| 77 | +
|
| 78 | +// The response message containing the result. |
| 79 | +message CalcResponse { |
| 80 | + double result = 1; |
| 81 | +} |
| 82 | +
|
| 83 | +// The Calculator service definition. |
| 84 | +service CalculatorService { |
| 85 | + // Performs addition of two numbers. |
| 86 | + rpc Add(CalcRequest) returns (CalcResponse); |
| 87 | +
|
| 88 | + // Performs subtraction of two numbers. |
| 89 | + rpc Subtract(CalcRequest) returns (CalcResponse); |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +- **message CalcRequest**: This message defines two numbers (`number1` and `number2`) for the input. |
| 94 | +- **message CalcResponse**: This message will hold the result of the calculation. |
| 95 | +- **CalculatorService**: Defines two RPC methods, `Add` and `Subtract`, which accept a `CalcRequest` and return a `CalcResponse`. |
| 96 | + |
| 97 | +### Step 2: Generate gRPC Code |
| 98 | +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: |
| 99 | + |
| 100 | + |
| 101 | +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: |
| 102 | + |
| 103 | +``` |
| 104 | +export PATH=$PWD/build/vcpkg_installed/x64-linux/tools/protobuf:$PWD/build/vcpkg_installed/x64-linux/tools/grpc/:$PATH |
| 105 | +``` |
| 106 | + |
| 107 | +Then go to `proto` directory |
| 108 | + |
| 109 | +```bash |
| 110 | +cd proto |
| 111 | +protoc -I=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` calculator.proto |
| 112 | +protoc -I=. --cpp_out=. calculator.proto |
| 113 | +``` |
| 114 | + |
| 115 | +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. |
| 116 | + |
| 117 | +copy these file into your `generated` directory: |
| 118 | + |
| 119 | +``` |
| 120 | +. |
| 121 | +├── CMakeLists.txt |
| 122 | +├── generated |
| 123 | +│ ├── calculator.grpc.pb.cc |
| 124 | +│ ├── calculator.grpc.pb.h |
| 125 | +│ ├── calculator.pb.cc |
| 126 | +│ └── calculator.pb.h |
| 127 | +├── proto |
| 128 | +│ └── calculator.proto |
| 129 | +├── src |
| 130 | +│ ├── client.cpp |
| 131 | +│ └── server.cpp |
| 132 | +└── vcpkg.json |
| 133 | +
|
| 134 | +``` |
| 135 | + |
| 136 | + |
| 137 | +### Step 3: Write the Server |
| 138 | + |
| 139 | +**server.cpp**: |
| 140 | +```cpp |
| 141 | +// Implementation of the Calculator Service |
| 142 | +class CalculatorServiceImpl final : public CalculatorService::Service { |
| 143 | + Status Add(ServerContext* context, const CalcRequest* request, CalcResponse* response) override { |
| 144 | + double sum = request->number1() + request->number2(); |
| 145 | + response->set_result(sum); |
| 146 | + return Status::OK; |
| 147 | + } |
| 148 | + |
| 149 | + Status Subtract(ServerContext* context, const CalcRequest* request, CalcResponse* response) override { |
| 150 | + double diff = request->number1() - request->number2(); |
| 151 | + response->set_result(diff); |
| 152 | + return Status::OK; |
| 153 | + } |
| 154 | +}; |
| 155 | + |
| 156 | +void RunServer() { |
| 157 | + std::string server_address("0.0.0.0:50051"); |
| 158 | + CalculatorServiceImpl service; |
| 159 | + |
| 160 | + ServerBuilder builder; |
| 161 | + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); |
| 162 | + builder.RegisterService(&service); |
| 163 | + |
| 164 | + std::unique_ptr<Server> server(builder.BuildAndStart()); |
| 165 | + std::cout << "Server listening on " << server_address << std::endl; |
| 166 | + server->Wait(); |
| 167 | +} |
| 168 | + |
| 169 | +int main() { |
| 170 | + RunServer(); |
| 171 | + return 0; |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +- **CalculatorServiceImpl**: Implements the service defined in the `proto` file. It overrides the `Add` and `Subtract` methods to provide logic for the operations. |
| 176 | +- **RunServer**: Sets up and starts the gRPC server on port `50051`. |
| 177 | + |
| 178 | +### Step 4: Write the Client |
| 179 | + |
| 180 | +**client.cpp**: |
| 181 | +```cpp |
| 182 | +class CalculatorClient { |
| 183 | +public: |
| 184 | + CalculatorClient(std::shared_ptr<Channel> channel) |
| 185 | + : stub_(CalculatorService::NewStub(channel)) {} |
| 186 | + |
| 187 | + double Add(double num1, double num2) { |
| 188 | + CalcRequest request; |
| 189 | + request.set_number1(num1); |
| 190 | + request.set_number2(num2); |
| 191 | + |
| 192 | + CalcResponse response; |
| 193 | + ClientContext context; |
| 194 | + |
| 195 | + Status status = stub_->Add(&context, request, &response); |
| 196 | + |
| 197 | + if (status.ok()) { |
| 198 | + return response.result(); |
| 199 | + } else { |
| 200 | + std::cout << "RPC failed" << std::endl; |
| 201 | + return 0.0; |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + double Subtract(double num1, double num2) { |
| 206 | + CalcRequest request; |
| 207 | + request.set_number1(num1); |
| 208 | + request.set_number2(num2); |
| 209 | + |
| 210 | + CalcResponse response; |
| 211 | + ClientContext context; |
| 212 | + |
| 213 | + Status status = stub_->Subtract(&context, request, &response); |
| 214 | + |
| 215 | + if (status.ok()) { |
| 216 | + return response.result(); |
| 217 | + } else { |
| 218 | + std::cout << "RPC failed" << std::endl; |
| 219 | + return 0.0; |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | +private: |
| 224 | + std::unique_ptr<CalculatorService::Stub> stub_; |
| 225 | +}; |
| 226 | + |
| 227 | +int main() { |
| 228 | + CalculatorClient client(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials())); |
| 229 | + |
| 230 | + double num1 = 10.0; |
| 231 | + double num2 = 5.0; |
| 232 | + |
| 233 | + std::cout << "Add: " << client.Add(num1, num2) << std::endl; |
| 234 | + std::cout << "Subtract: " << client.Subtract(num1, num2) << std::endl; |
| 235 | + |
| 236 | + return 0; |
| 237 | +} |
| 238 | +``` |
| 239 | +
|
| 240 | +- **CalculatorClient**: Defines a client that connects to the server and calls the `Add` and `Subtract` methods. It sets up an RPC request, sends it to the server, and retrieves the result. |
| 241 | +
|
| 242 | +### Step 5: CMakeLists.txt |
| 243 | +
|
| 244 | +Here's how you can set up the CMake build file. |
| 245 | +
|
| 246 | +```cmake |
| 247 | +set(GENERATED_DIR "${CMAKE_SOURCE_DIR}/generated" ) |
| 248 | +set(PROTO_SRCS "${GENERATED_DIR}/calculator.pb.cc" ) |
| 249 | +set(GRPC_SRCS "${GENERATED_DIR}/calculator.grpc.pb.cc") |
| 250 | +
|
| 251 | +include_directories(${GENERATED_DIR}) |
| 252 | +
|
| 253 | +
|
| 254 | +add_executable(server src/server.cpp ${PROTO_SRCS} ${GRPC_SRCS}) |
| 255 | +add_executable(client src/client.cpp ${PROTO_SRCS} ${GRPC_SRCS}) |
| 256 | +
|
| 257 | +target_link_libraries(server PRIVATE gRPC::grpc++ protobuf::libprotobuf) |
| 258 | +target_link_libraries(client PRIVATE gRPC::grpc++ protobuf::libprotobuf) |
| 259 | +``` |
| 260 | + |
| 261 | + |
| 262 | +### Step 6: Build and Run |
| 263 | +In the root of the project, run: |
| 264 | + |
| 265 | +``` |
| 266 | +cmake -S . -B build -G "Ninja Multi-Config" -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake |
| 267 | +
|
| 268 | +``` |
| 269 | + |
| 270 | +Start the server in one terminal: |
| 271 | +```bash |
| 272 | +./server |
| 273 | +``` |
| 274 | + |
| 275 | +Run the client in another terminal: |
| 276 | +```bash |
| 277 | +./client |
| 278 | +``` |
0 commit comments