gRPC Server in C++

link). If you don't want to read the code and comments in English, welcome under the cut.

Theory, or rather, the lack of it

Let's assume that you know the basic theory of gRPC and have some understanding of what proto files are and how to generate C++ source code from them using the protoc utility.

Test example

Now let's move on to an example, on the basis of which we will analyze step by step the sequence of actions for creating a gRPC server. Let's say we have a need to create an application that in some sense resembles a gastronomic social network.

Here the user can:

  • register

  • follow another person

  • rate your visit to a restaurant or cafe on a 5-point scale, indicating the dishes ordered and the date of the visit

  • go to the page of the person you subscribed to, view their visits

Architecture

Before we start writing, let's sketch out a rough project architecture. We'll start from the description of the functional capabilities given above.

“The ability to subscribe/rate a visit/leave a comment” – all this information needs to be stored somewhere. Let's add the component “storage“.

“Register” – the presence of this functionality implies the implementation of three important components at once: identification, authentication And authorization. But for the sake of simplicity of the example we will omit them.

While we can't interact with the application in any way, we need API-shka. This is where our gRPC component will be based (among other things, for local interaction with the server in C++ we could deploy a unix domain socket, for remote interaction – interact according to REST principles, so that gRPC alone API-the number is not limited).

The only thing left is the entry point to the software, the function mainwhich we will place in the component “app“.

Approximate project architecture

Approximate project architecture

Please note: the boundaries of component interactions pass through interfaces, while the implementation of the component can be absolutely anything, as long as it implements the interface.

To gRPC for the Pros

First of all, we are interested in gRPC-API. Let's delve into this part of the architecture. Here we should think about proto– contracts, on the basis of which we will further generate C++ code. We will start from the same descriptive part.

The user can register, so we'll do it rpcRegistrate(…) returns (…). The first ellipsis in brackets is what the rpc method (called on the client) uses as an argument, the second ellipsis is what the method returns, i.e. the response from the server. Let's call them ClientRegistrationReq and ClientRegistrationResp respectively. Next, you should think about what to fill the content of these two messages with. What does a person usually indicate when registering? Email, first/last name, phone number (optional). What can the server send in response to such a message? Registration status (success or failure) and an optional descriptive part (for example, the reason why registration failed). Then we have something like this:

service GrpcTransport {
    // Registrate new user
    rpc Registrate(ClientRegistrationReq) returns (ClientRegistrationResp) {}
}

// Message for registrate new user
message ClientRegistrationReq {
    string   electronic_mail       = 1;
    string   name                  = 2;
    string   sername               = 3;
    optional string phone_number   = 4;
}

// Response on new user registration
message ClientRegistrationResp {
    bool            ok     = 1;
    optional string reason = 2;
}

Just a little bit about what's in proto-file happens. First, we declared the service GrpcTransportwithin which certain things exist rpc-methods called by the client (syntax: rpc $MehtodName($Params) returns ($ReturnedVals) {}). Next we describe each of the messages, that is, $Params And $ReturnedVals respectively. From the point of view of the programming language, they can be perceived as structures with listed fields of a certain type (with all types proto you can see it Here). The keyword used in the example optional indicates that the parameter is optional.

Let's move on. “Subscribe to another person.” In social networks, people search for others by name or nickname, and we did not provide for it. But with 100% probability, e-mail is a unique identification key. Of course, this is private information, but for a test example, searching for another user by mail is quite suitable. In response from the server, we receive the status whether it was possible to subscribe and, if not, why. So, our rpc-method Subscribe and the corresponding messages:

// Subscribe
rpc Subscribe(SubscriptionReq) returns (SubscriptionResp) {}

// Message for subscribe to another user
message SubscriptionReq {
    string electronic_mail = 1;
}

// Server response about subscription
message SubscriptionResp {
    bool            ok     = 1;
    optional string reason = 2;
}

Establishment rating. Here we will give a person the opportunity to enter the name and address of the establishment, indicating a list of rated dishes (map from the name of the dish, i.e. the string, into a numeric integer rating with a maximum of 5). In response, we will expect the status – whether the visit was added or not with the same optional indication of the reason.

// Estimate dishes
rpc EstimateEstablishment(EstimationReq) returns (EstimatonResp) {}

// Message for estimate dishes
message EstimationReq {
    string             name    = 1;
    string             address = 2;
    map<string, int32> dishes  = 3;
}

// Server response about dishes estimation
message EstimatonResp {
    bool            ok     = 1;
    optional string reason = 2;
}

The final thing remains. Go to the page of the person you subscribed to, look at the list of his visits. Again, we identify the person we are interested in by e-mail. In response, we receive a list of visits with ratings and an optional reason-explanation if the server failed to transmit the information.

// Subscription estimations
rpc GetSubscriptionEstimations(SubscriptionEstimationsReq) returns (SubscriptionEstimationsResp) {}

// Message for get subscription dishes estimations
message SubscriptionEstimationsReq {
    string electronic_mail = 1;
}

// Server response about getting subscription estimations
message SubscriptionEstimationsResp {
    bool                   ok          = 1;
    optional string        reason      = 2;
    repeated EstimationReq estimations = 3;
}

The last field type is array (keyword repeated) messag-ey, invented by us.

Automated code generation

So, proto-we have the file, all our “contracts” are thought out. What's next? The next step is to use a special utility protocwhich is based on proto-files will generate files with C++ code for us. Detailed description of how to install protocyou can find Here. For code generation we will need to use two commands:

protoc -I <path to folder with proto-files>\
--cpp_out=<path where need to place generated cpp messages files>\
<path to proto-file>

protoc -I --grpc_out=<path where need to place cpp services files>\
--plugin=protoc-gen-grpc=`which grpc_cpp_plugin`\
<path to proto-file>

Let's take a closer look at the listed parameters.

<path to folder with proto files> – path to the folder where they are located proto– contracts (in our case there is only one file, but there may be more)

<path where need to place generated cpp messages files> – the path where we want to place the generated header and cpp files describing the messags

<path where need to place cpp services files> – the path where the generated header and cpp files describing the services and their rpc-methods

<path to proto file> – the way to our proto-file

Example of using protoc

Example of use protoc

Entering these commands manually into the console every time is annoying and time-consuming. official example automation is done at the project assembly stage using CMake. We will do exactly that. But first we should define the hierarchy of files and folders of our project. Below is the hierarchy:

Project file structure

Project file structure

The key is CMakeLists.txt of the ./lib/api/src/ folder (relative to the project root). The following lines are required in the CMake list:

# Generate cpp-files due to proto
execute_process(
    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generate.sh
)

Team execute_process is needed to run the generate.sh bash script with automated code generation. In the script itself, we specify the paths to the sources proto and the instructions that need to be done with them:

#!/bin/bash

SCRIPT_DIR_PATH=$(cd "$(dirname "$0")" && pwd)
echo $SCRIPT_DIR_PATH

SRC_DIR=$SCRIPT_DIR_PATH/
PROTO_DIR=/$SCRIPT_DIR_PATH/../resource

protoc -I $PROTO_DIR --cpp_out=$SRC_DIR $PROTO_DIR/main.proto
protoc -I $PROTO_DIR --grpc_out=$SRC_DIR --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` $PROTO_DIR/main.proto

Using the generated code

As a result of code generation, new files are saved in the ./lib/api/src/ folder (relative to the root). We will use them to implement two simple public functions (starting and stopping the server):

namespace api_grpc
{

//! Public function for start gRPC-server
void runServer(const std::string& address, std::shared_ptr<storage::IStorageManager> pStoreManager);

//! Public function for stop gRPC-server
void stopServer();

} // namespace api_grpc

So, they will be declared in the file ./lib/api/include/api/GrpcAPI.h, and defined in ./lib/api/src/GrpcAPI.cpp:

using api_grpc::ServerGRPC;

using grpc::Server;
using grpc::ServerBuilder;

ServerGRPC*             pService = nullptr;
std::unique_ptr<Server> pServer  = nullptr;

void api_grpc::runServer(const std::string&                        address,
                         std::shared_ptr<storage::IStorageManager> pStoreManager) {
    // создаем свой сервис
    pService = new ServerGRPC(pStoreManager);

    // создаем gRPC-шный server builder
    ServerBuilder serverBuilder;

    // добавляем порт и специфицируем вид подключения (не защищенное)
    serverBuilder.AddListeningPort(address, grpc::InsecureServerCredentials());

    // регистрируем наш собственный сервис и запускаем
    serverBuilder.RegisterService(pService);
    pServer = serverBuilder.BuildAndStart();
    std::cout << "Server listening on " << address << std::endl;

    // этот метод является блокирующим
    pServer->Wait();
}

//! Public function for stop gRPC-server
void api_grpc::stopServer() {
    // этот метод завершит блокоирующий Wait()
    pServer->Shutdown();

    delete pService;
    delete(pServer.release());
}

The implementation is tied to the class we wrote. ServerGRPCwho is the heir GrpcTransport::Service (exactly the service that we described in proto file and generated using protoc):

namespace api_grpc
{

//! gRPC-server implementation
class ServerGRPC final : public GrpcTransport::Service {
public:
    //! Ctor by default
    ServerGRPC() = delete;

    //! Constructor
    ServerGRPC(std::shared_ptr<storage::IStorageManager> pStoreManager);

    //! Destructor
    ~ServerGRPC();

    //! Registrate new user
    grpc::Status Registrate(
        grpc::ServerContext* context,
        const ClientRegistrationReq* request,
        ClientRegistrationResp* response
    ) override;

    //! Subscribe to user
    grpc::Status Subscribe(
        grpc::ServerContext* context,
        const SubscriptionReq* request,
        SubscriptionResp* response
    ) override;
    
    //! Estimate dishes
    grpc::Status EstimateEstablishment(
        grpc::ServerContext* context,
        const EstimationReq* request,
        EstimatonResp* response
    ) override;
    
    //! Subscription estimations
    grpc::Status GetSubscriptionEstimations(
        grpc::ServerContext* context,
        const SubscriptionEstimationsReq* request,
        SubscriptionEstimationsResp* response
    ) override;

private:
    std::shared_ptr<storage::IStorageManager> pStorageManager_;
};

}

As you can see, all those rpc-methods that are present in proto-file, there are also here, and each of them is marked overridebecause they are exactly the same virtual methods are in the parent class GrpcTransport::Service.

The meaning you give to these methods depends on your imagination.

Conclusion

You can see what business logic I filled the methods with on github, where I attached the entire code of this project (the article has already turned out to be quite thick), and also wrote unit tests (I used GTest), so that you have the opportunity to play around, in order to better understand the whole point. For complete server tests, you also need a gRPC client, which is also there. I hope this material will be useful. Link: https://github.com/Daniel-Ager-Okker/gRPC-example

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *