RPC

include: co/rpc.h.

Coost implements a coroutine-based RPC framework, which internally uses JSON as the data exchange format. Compared with RPC frameworks using binary protocols such as protobuf, it is more flexible and easier to use.

Since v3.0, the RPC framework also supports HTTP protocol, and we are able to send a RPC request with the HTTP POST method.

#rpc::Service

class Service {
   public:
     Service() = default;
     virtual ~Service() = default;

     typedef std::function<void(Json&, Json&)> Fun;

     virtual const char* name() const = 0;
     virtual const co::map<const char*, Fun>& methods() const = 0;
};
  • This class is a pure interface, which represents a RPC service. A RPC server may have multiple services.
  • name() returns the service name, methods() returns all the RPC methods.

#rpc::Server

#Server::Server

Server();
  • The default constructor, users do not need to care.

#Server::add_service

1. Server& add_service(rpc::Service* s);
2. Server& add_service(const std::shared_ptr<rpc::Service>& s);
  • Add a service, the parameter s in the first one must be dynamically created with operator new.
  • Users can call this method multiple times to add multiple services, and different services must have different names.

#Server::start

void start(
    const char* ip, int port,
    const char* url="/",
    const char* key=0, const char* ca=0
);
  • Start the RPC server, this method will not block the current thread.
  • The parameter ip is the server ip, which can be an IPv4 or IPv6 address, and the parameter port is the server port.
  • The parameter url is the url of the HTTP server, and must start with /.
  • The parameter key is path of a PEMfile which stores the SSL private key, and the parameter ca is path of a PEM file which stores the SSL certificate. They are NULL by default, and SSL is disabled.
  • Starting from v3.0, the server no longer depends on the rpc::Server object after startup.

#Server::exit

void exit();
  • Added since v2.0.2.
  • Exit the RPC server, close the listening socket, and no longer receive new connections.
  • Since v3.0, after the RPC server exits, previously established connections will be reset in the future.

#RPC server example

#Define a proto file

Here is a simple proto file hello_world.proto:

package xx

service HelloWorld {
    hello
    world
}

package defines the package name.

  • package defines the package name, which corresponds to namespace in C++.

  • service defines a RPC service, it has 2 methods, hello and world.

  • Since the RPC request and response are both JSON, there is no need to define the structure in the proto file.

  • At most one service can be defined in a proto file.

Coost v3.0.1 rewrote gen with flex and byacc, and we can alse define object (struct) in the proto file. For specific usage, please refer to j2s.

#Generate code for RPC service

gen is the RPC code generator provided by coost, which can be used to generate code for RPC service.

xmake -b gen             # build gen
cp gen /usr/local/bin    # put gen in the /usr/local/bin directory
gen hello_world.proto    # Generate code

The generated file hello_world.proto is as follow:

// Autogenerated.
// DO NOT EDIT. All changes will be undone.
#pragma once

#include "co/rpc.h"

namespace xx {

class HelloWorld : public rpc::Service {
  public:
    typedef std::function<void(Json&, Json&)> Fun;

    HelloWorld() {
        using std::placeholders::_1;
        using std::placeholders::_2;
        _methods["HelloWorld.hello"] = std::bind(&HelloWorld::hello, this, _1, _2);
        _methods["HelloWorld.world"] = std::bind(&HelloWorld::world, this, _1, _2);
    }

    virtual ~HelloWorld() {}

    virtual const char* name() const {
        return "HelloWorld";
    }

    virtual const co::map<const char*, Fun>& methods() const {
        return _methods;
    }

    virtual void hello(Json& req, Json& res) = 0;

    virtual void world(Json& req, Json& res) = 0;

  private:
    co::map<const char*, Fun> _methods;
};

} // xx
  • As you can see, the HelloWorld class inherits from rpc::Service, and it has already implemented name() and methods() in rpc::Service.
  • Users only need to inherit the HelloWorld class and implement the methods hello and world.

#Implement the RPC service

#include "hello_world.h"

namespace xx {

class HelloWorldImpl : public HelloWorld {
  public:
    HelloWorldImpl() = default;
    virtual ~HelloWorldImpl() = default;

    virtual void hello(Json& req, Json& res) {
        res = {
            { "result", {
                { "hello", 23 }
            }}
        };
    }

    virtual void world(Json& req, Json& res) {
        res = {
            { "error", "not supported"}
        };
    }
};

} // xx
  • The above is just a very simple example. In actual applications, it is generally necessary to perform corresponding business processing according to the parameters in req, and then fill in res.

#Start RPC server

int main(int argc, char** argv) {
    flag::parse(argc, argv);

    rpc::Server()
        .add_service(new xx::HelloWorldImpl)
        .start("127.0.0.1", 7788, "/xx");

    for (;;) sleep::sec(80000);
    return 0;
}
}
The start() method will not block the current thread, so we need a for loop to prevent the main function from exiting.

#Call RPC service with curl

In v3.0, the RPC framework supports HTTP protocol, so we can call the RPC service with the curl command:

curl http://127.0.0.1:7788/xx --request POST --data '{"api":"ping"}'
curl http://127.0.0.1:7788/xx --request POST --data '{"api":"HelloWorld.hello"}'
  • The above use curl to send a POST request to the RPC server, the parameter is a JSON string, and a "api" field must be provided to indicate the RPC method to be called.

  • "ping" is a built-in method of the RPC framework, generally used for testing or sending heartbeats.

  • /xx in the url should be consistent with the url specified when the RPC server is started.

#rpc::Client

#Client::Client

1. Client(const char* ip, int port, bool use_ssl=false);
2. Client(const Client& c);
  • 1, the parameter ip is ip of the server, which can be a domain name, IPv4 or IPv6 address; the parameter port is port of the server; the parameter use_ssl indicates whether to enable SSL transmission, the default is false, and SSL is disabled.
When rpc::Client was constructed, the connection is not established immediately.

#Client::~Client

Client::~Client();
  • Destructor, close the internal connection.

#Client::call

void call(const Json& req, Json& res);
  • Perform a RPC request, it must be called in coroutine.
  • The parameter req must contain the "api" field, its value is generally in the form of "service.method".
  • The parameter res is the response of the RPC request.
  • If the RPC request is not sent, or no response from the server is received, res will not be filled.
  • This method checks the connection status before sending the RPC request, and establishes the connection first if it is not connected.

#Client::close

void close();
  • Close the connection, it is safe to call this function multiple times.

#Client::ping

void ping();
  • Send a ping request to the server, generally used for testing or sending heartbeats.

#RPC client example

#Use rpc::Client directly

DEF_bool(use_ssl, false, "use ssl if true");
DEF_int32(n, 3, "request num");

void client_fun() {
    rpc::Client c("127.0.0.1", 7788, FLG_use_ssl);

    for (int i = 0; i < FLG_n; ++i) {
        co::Json req = {
            {"api", "HelloWorld.hello"}
        };
        co::Json res;
        c.call(req, res);
        co::sleep(1000);
    }

    c.close();
}

go(client_fun);
  • In the above example, the client sends an RPC request to the server every 1 second.

#Use connection pool

When a client needs to establish a large number of connections, co::pool can be used to manage these connections.

std::unique_ptr<rpc::Client> proto;

co::pool pool(
    []() { return (void*) new rpc::Client(*proto); },
    [](void* p) { delete (rpc::Client*) p; }
);

void client_fun() {
    co::pool_guard<rpc::Client> c(pool);

    while (true) {
        c->ping();
        co::sleep(3000);
    }
}

proto.reset(new rpc::Client("127.0.0.1", 7788));

for (int i = 0; i < 8; ++i) {
    go(client_fun);
}
  • In the above example, co::pool is used to store the clients, and multiple coroutines can share these clients.
  • The ccb of co::pool uses copy construction to copy a client from proto.

#Config items

Coost uses co.flag to define config items for RPC.

#rpc_conn_idle_sec

DEF_int32(rpc_conn_idle_sec, 180, "#2 connection may be closed if no data...");
  • Timeout in seconds for idle connections in rpc::Server. If a connection does not receive any data within this time, the server may close the connection.

#rpc_conn_timeout

DEF_int32(rpc_conn_timeout, 3000, "#2 connect timeout in ms");
  • Connect timeout in milliseconds for rpc::Client.

#rpc_log

DEF_bool(rpc_log, true, "#2 enable rpc log if true");
  • Whether to print RPC logs, the default is true, rpc::Server and rpc::Client will print RPC requests and responses.

#rpc_max_idle_conn

DEF_int32(rpc_max_idle_conn, 128, "#2 max idle connections");
  • Maximum number of idle connections for rpc::Server. The default is 128. When this number is exceeded, the server will close some idle connections.

#rpc_max_msg_size

DEF_int32(rpc_max_msg_size, 8 << 20, "#2 max size of rpc message, default: 8M");
  • The maximum length of RPC messages, the default is 8M.

#rpc_recv_timeout

DEF_int32(rpc_recv_timeout, 3000, "#2 recv timeout in ms");
  • RPC recv timeout in milliseconds.

#rpc_send_timeout

DEF_int32(rpc_send_timeout, 3000, "#2 send timeout in ms");
  • RPC send timeout in milliseconds.