This article is suitable for friends with C++ foundation

HelloGitHub-Anthony

Rest_rpc, an open source RPC framework based on C++, is a framework that allows even small people to develop RPC services quickly (10 minutes).

Project address: github.com/qicosmos/re…

Rest_rpc is a high-performance, easy-to-use, cross-platform, header only C++11 RPC library that aims to make TCP communication so easy to use that even people who don’t know much about network communication can use it quickly. At the same time, users only need to focus on their own business logic.

In short, REST_RPC lets you quickly write your own network programs in just a few lines of code without any knowledge of network programming, and is a great way to get started with network programming and RPC frameworks!

First, prepare knowledge

1.1 What is RPC

RPC stands for Remote Procedure Call.

1.2 What is RPC used for

For example, if there are two servers, A and B, now the program on A wants to call the function/method provided by the application on B remotely, it needs to transfer the message needed for the call over the network.

But there are many things involved in the network transmission of messages, such as:

  • Establish, maintain, and disconnect the TCP connection between the client and server

  • Serialization and marshalling of messages

  • Network transmission of messages

  • Deserialization of messages

  • , etc.

The purpose of RPC is to block network operations so that programs that are not in the same memory space, or even in the same machine, can be called just like ordinary functions.

1.3 rest_rpc advantages

Rest_rpc has a number of advantages:

  • Using a simple
  • Support subscription model
  • allowfuturecallbackTwo kinds of asynchronous call interface, to meet the different interests of people

Start fast

Rest_rpc relies on Boost and should have Boost installed properly before using it.

2.1 installation

Use git clone to download the project locally:

git clone https://github.com/qicosmos/rest_rpc
Copy the code

2.2 Directory Structure

The files in the root directory of the REST_RPC project and their meanings are shown in the following table:

The file name role
doc Rest_rpc performance test report
examples The rest_RPC example contains the client and server parts
include Rest_rpc framework header file
third Msgpack supports libraries for serializing and deserializing messages

2.3 Running routines

Rest_rpc routines are Visual Studio projects, and client and server routines are stored in examples/client and Examples/Server, respectively. Use Visual Studio to open basic_client.vcxproj or basic_server.vcxproj and compile it directly.

Note: The project requires Boost/ ASIO support. If Boost is not installed, install it correctly and add it to the project.

Add to projectBoostThe method is as follows:

  1. Open the project and click on the menu barprojectattribute(shortcutsAlt+F7)
  2. Choose the one on the leftVc + + directoriesThe choices, the ones on the rightContains the directoryThe library catalogaddBoostThe root directoryDependent librariesAfter saving

I used Boost 1.75 installation directory D: devpack_boost_1_75_0 as shown in the following figure:

Three, detailed tutorial

3.1 in front

Include /rest_rpc. HPP is the only file required for both the server and client.

All of the sample code uses the following as a framework:

#include <iostream>
#include <rest_rpc.hpp>
#include <chrono>
using namespace rest_rpc;
using namespace rest_rpc::rpc_service;

int main(a){
    // do something
}
Copy the code

3.2 Writing the server

Generating a client that can provide a service goes through several processes:

  1. rpc_serverObject, set the listening port and other properties
  2. Registration of service functions that define which services are provided by the server
  3. Service startup

1) rpc_server

Rpc_server is a rest_RPC server object that registers services, publishes and subscribes, and manages thread pools. Rpc_server resides in the rest_RPC ::rpc_service namespace.

Instantiate a rpc_server object and provide a listening port, thread pool size, for example:

rpc_server server(9000.6); // Listen on port 9000, thread pool size 6
Copy the code

2) Register and start the server

Rpc_server provides the register_handler method to register the service and the run method to start the server. Examples are as follows:

/* The first argument to the service function must be rpc_conn, followed by the required arguments (variable arguments, variable number, or no */)
std::string hello(rpc_conn conn, std::string name){ 
	/* Can be a void return type, meaning that no message is returned to the remote client after the call */
    return ("Hello " + name); /* What is returned to the remote client */
}


int main(a){
    rpc_server server(9000.6);
    
    Func_greet is the service name. Func_greet is the service name used to call the function */
    /* Hello is the function that binds the current service to call */
    server.register_handler("func_greet", hello);
	
    server.run(a);// Start the server
    
    return EXIT_SUCCESS;
}
Copy the code

Function can be a functor or a lambda. Examples are as follows:

Using a functor:

/* function method */

struct test_func{
    std::string hello(rpc_conn conn){
        return "Hello Github!"; }};int main(a){
    test_func greeting;
    rpc_server server(9000.6);
    
    /* Greet is the name of the service
    /*test_func::hello is the function that binds the current service to call */
    /*greeting instantiates the functor object */
    server.register_handler("greet", &test_func::hello, &greeting);
    
    server.run(a);// Start the server
    
    return EXIT_SUCCESS;
}
Copy the code

Examples of using the Lambda method:

/* Use lambda method */

int main(a){
    rpc_server server(9000.6);
    
    /* Call_lambda is the service name, and remote calls are called by the service name */
    /*[&server](rpc_conn conn){... } is the lambda object */
    server.register_handler("call_lambda"./* All parameters except conn are variable */
                            [&server](rpc_conn conn /* Other parameters are optional */) {
                                std::cout << "Hello Github!" << std::endl;
                                // Return value is optional
                            });
    
    server.run(a);// Start the server
    
    return EXIT_SUCCESS;
}
Copy the code

3) Register asynchronous services

Sometimes for various reasons we cannot or do not want a remote call to return synchronously (such as waiting for a thread to return), we simply give the register_handler method an Async template argument (in the rest_RPC namespace) :

/* The asynchronous service return type is void*/
void async_greet(rpc_conn conn, const std::string& name) {
    auto req_id = conn.lock() - >request_id(a);// Asynchronous services need to save the request ID first

    // A new thread is created to handle some tasks asynchronously
    std::thread thd([conn, req_id, name] {
        
        std::string ret = "Hello " + name + ", Welcome to Hello Github!";
        
        /* where CONN is a weak_ptr*/
        auto conn_sp = conn.lock();// Use weak_ptr lock method to get a shared_ptr
        
        if (conn_sp) {
            /* Return; STD ::move(ret) is the return value */conn_sp->pack_and_response(req_id, std::move(ret)); }});
    
    thd.detach(a); }int main(a){
    rpc_server server(9000.6);
    
	server.register_handler<Async>("async_greet", async_greet);// Use Async as the template argument
    
    server.run(a);// Start the server
    
    return EXIT_SUCCESS;
}
Copy the code

Rest_rpc supports registering multiple services on the same port, for example:

server.register_handler("func_greet", hello);
server.register_handler("greet", &test_func::hello, &greeting);
server.register_handler("call_lambda"./* All parameters except conn are variable */
                        [&server](rpc_conn conn /* Other parameters are optional */) {
                            std::cout << "Hello Github!" << std::endl;
                            // Return value is optional
                        });
// Other services, etc
server.run(a);Copy the code

3.3 Writing a Client

Generating a client that can make a remote service call goes through the following process:

  1. rpc_clientObject instantiation, set server address and port
  2. Connecting to the Server
  3. Call the service

1) rpc_client

Rpc_client is a Rest_RPC client object. Rpc_client connects to, invoks, serializes, and deserializes server services. Rpc_client is located in the REST_RPC namespace.

To use rPC_client, you need to instantiate an rPC_client object, and then use the connect or asynC_connect methods provided by rPC_client to connect to the server synchronously/asynchronously, such as:

rpc_client client;

bool has_connected = client.connect("127.0.0.1".9000);// Synchronize the connection and return whether the connection is successful

client.async_connect("127.0.0.1".9000);Asynchronous connection, no return value
Copy the code

Of course, rPC_client also provides the enable_AUTO_reconnect and enable_AUTO_heartbeat functions for keeping the connection alive under different circumstances.

2) Invoke the remote service

Rpc_ client provides two methods of async_call and Call to call remote services asynchronously or synchronously. Async_call supports callback and Future methods to process returned messages. This section describes the synchronous call method call.

When calling the call method, we need to set the template parameters if our service returns a value. For example, if the remote service returns an integer, we need to specify the return type call

. If we do not specify the return type call

, there is no return value.

In writing the server part we said that each service is registered with a name that can be used to call a remote service. Now let’s write the first example calling the server part:

int main(a){
    /* REST_RPC throws an exception when it encounters an error (inconsistent parameters for calling service and remote service requirements, connection failure, etc.) */
    try{

        /* Establish a connection */
        rpc_client client("127.0.0.1".9000);// IP address, port number
        /* Set timeout 5s (default: 3s), connect timeout false, return true*/ on success
        bool has_connected = client.connect(5);
        /* If no connection is established, exit program */
        if(! has_connected) { std::cout <<"connect timeout" << std::endl;
            exit(- 1);
        }

        /* Call the remote service and return a welcome message */
        std::string result = client.call<std::string>("func_greet"."HG");Func_greet is the pre-registered service name and requires a name parameter (HG for Hello Github)
        std::cout << result << std::endl;

    }
    /* An exception will be thrown if a connection error occurs and the service is called with unequal parameters */
    catch (const std::exception & e) {
        std::cout << e.what() << std::endl;
    }
    
    return EXIT_SUCCESS;
}
Copy the code

Of course, some calls may not return any messages, so it’s time to use client.call(” XXX “,…) directly. The call method returns type void.

3) Invoke remote services asynchronously

Sometimes the remote service we call takes some time to return due to various reasons. In this case, we can use the asynchronous call method async_call provided by Rpc_client. By default, it is callback mode and the template parameter is timeout time. You need to specify this if you want to use the Future pattern.

Callback mode, in which the callback function takes the same parameters as in the routine, followed by client.run() :

/* The default is call back mode, the template parameter represents timeout 2000ms, async_call parameter sequence is the service name, callback function, call the service parameters (the number of types vary)*/
/*timeout If this parameter is not specified, the default value is 5s. If this parameter is set to 0, timeout */ is not checked
client.async_call<2000> ("async_greet"./* This callback function is automatically called when the remote service returns. Note that the parameter can only be */
                  [&client](const boost::system::error_code & ec, string_view data) {
                        
                        auto str = as<std::string>(data);
                        std::cout << str << std::endl;
                   }, 
                  "HG");// The echo service returns the parameters passed directly
client.run(a);// Start the service thread and wait for it to return

// The rest is used in the same way as call
Copy the code

The Future mode:

auto f = client.async_call<FUTURE>("async_greet"."HG");

if (f.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout) {
    std::cout << "timeout" << std::endl;
}
else {
    auto ret = f.get().as<std::string>();// Convert to string, no return value can be f.et ().as()
    std::cout << ret << std::endl;
}
Copy the code

3.4 the serialization

When using rest_RPC, you do not need to specify the serialization method separately if the parameters are library-related objects. When using custom objects, you need to define the serialization method using MSgPack, for example, to transport a structure like this:

struct person {
	int id;
	std::string name;
	int age;
};
Copy the code

MSGPACK_DEFINE() :

/ * note: MSGPACK_DEFINE() must be entered in the same order, This and the way msgpack is serialized, such as the inconsistent order of parameters in MSGPACK_DEFINE() on the client and server side, can cause unpacking errors */
struct person {
	int id;
	std::string name;
	int age;

	MSGPACK_DEFINE(id, name, age);// Define what needs to be serialized
};
Copy the code

The same is true for objects:

class person{
    private:
    	int id;
        std::string name;
        int age;
    public:
    	MSGPACK_DEFINE(id, name, age);// Need to be in public
}
Copy the code

You can then use Person as a parameter type.

Features: Publish/subscribe mode

One of the features of REST_RPC is the publish-subscribe mode, which is useful when messages need to be constantly transferred between the client and the server.

The server only needs to publish or publish_by_token method of rpc_server to publish a subscription message. If token is used, subscribers need to use the same token to access, for example:

int main(a) {
    rpc_server server(9000.6);

    std::thread broadcast([&server]() {
        while (true) {
            /* Publish a greet message to all clients that subscribe to greet */
            server.publish("greet"."Hello GitHub!");
            /* To get messages, you must subscribe to secret_greet and provide www.hellogithub.com as a token */
            server.publish_by_token("secret_greet"."www.hellogithub.com"."Hello Github! this is secret message");

            std::this_thread::sleep_for(std::chrono::seconds(1));// Wait a second}});

    server.run(a);// Start the server

    return EXIT_SUCCESS;
}
Copy the code

The client simply uses the SUBSCRIBE method of rpc_client:

void test_subscribe(a) {
    rpc_client client;

    client.enable_auto_reconnect(a);// Automatic reconnection
    client.enable_auto_heartbeat(a);// Automatic heartbeat packet
    bool r = client.connect("127.0.0.1".9000);
    if(! r) {return;
    }

    // Direct subscription, no token
    client.subscribe("greet", [](string_view data) {
        std::cout << data << std::endl;
        });
    // You need a token to get subscription messages normally
    client.subscribe("secret_greet"."www.hellogithub.com", [](string_view data) {
        std::cout << data << std::endl;
        });
    
    client.run(a);// Keep running
}

int main(a) {
    
    test_subscribe(a);return EXIT_SUCCESS;
}
Copy the code

1) Transfer custom objects when subscribing

If there is such an object to transfer:

struct person {
	int id;
	std::string name;
	int age;

	MSGPACK_DEFINE(id, name, age);
};
Copy the code

The server simply takes it as a parameter, for example:

person p{ 1."tom".20 };
server.publish("key", p);
Copy the code

The client needs to deserialize:

client.subscribe("key", 
                 [](string_view data) {
                     msgpack_codec codec;
                     
                     person p = codec.unpack<person>(data.data(), data.size());
                     std::cout << p.name << std::endl;
                 });
Copy the code

Five, the last

RPC has many mature industrial frameworks such as:

  • Google’s GRPC
  • Baidu BRPC and so on

However, it is more complex to configure and use than rest_RPC. Rest_rpc is a good starting point for newcomers to RPC.

Now that you know most of the features of REST_RPC, it’s time to get your hands on an RPC service!

Vi. Reference materials

  • What is the RPC
  • A popular explanation of RPC

Follow the HelloGitHub public account to receive the first updates.

There are more open source projects and treasure projects waiting to be discovered.