RPC
#include "co/rpc.h"

The API is in the co namespace, and aliases are also provided:

namespace rpc {
using service  = co::rpc_service;
using server   = co::rpc_server;
using client   = co::rpc_client;
using method_t = co::rpc_method_t;
}

Before use, you need to call flag::parse(argc, argv); at the beginning of main.

#Overview

  • A lightweight RPC based on TCP + JSON.
  • Protocol: 8-byte header + JSON body.
  • Requests specify the method through the api field, in the format ServiceName.method_name, such as HelloWorld.hello.

#Protocol

#Header

struct Header {
    uint16 flags; // reserved, 0
    uint16 magic; // 0x7777
    uint32 len;   // body len, network byte order
}; // 8 bytes
  • magic = 0x7777, used for validation.
  • len is the body length, in network byte order.
  • The body is JSON text.

#Request

{"api": "ServiceName.method_name", ...}
  • api must be a string.
  • Other fields are user parameters.

#Response

  • Success: JSON filled in by the user method.
  • Failure: {"error": "..."}.

#rpc_service

struct rpc_service {
    rpc_service() = default;
    virtual ~rpc_service() = default;

    virtual const char* name() const = 0;
    virtual const co::map<const char*, rpc_method_t>& methods() const = 0;
};

using rpc_method_t = std::function<void(json::any&, json::any&)>;
  • name() returns the service name.
  • methods() returns a mapping from method names to rpc_method_t.
  • Registered into rpc_server through rpc_server::add_service.
  • rpc_method_t: the first parameter is the request JSON, and the second is the response JSON.

#gen Tool

Generates a subclass of rpc_service from a .proto file. Execute the following command in the coost root directory to build gen:

xmake b gen

#Usage

hello_world.proto:

package xx

service HelloWorld {
    hello
    world
}

Execute:

gen hello_world.proto

Generates hello_world.h, containing the xx::HelloWorld class:

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

#include "co/rpc.h"

namespace xx {

struct HelloWorld : co::rpc_service {
    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*, co::rpc_method_t>& methods() const {
        return _methods;
    }

    virtual void hello(json::any& req, json::any& res) = 0;

    virtual void world(json::any& req, json::any& res) = 0;

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

} // xx

Users inherit from it and implement the pure virtual methods:

struct HelloWorldImpl : xx::HelloWorld {
    void hello(json::any& req, json::any& res) override {
        res.add_member("msg", "hello");
    }

    void world(json::any& req, json::any& res) override {
        res.add_member("msg", "world");
    }
};

#proto Syntax

#Program Structure

package <package_name>

service <ServiceName> {
    method1
    method2
}

object <ObjectName> {
    <field_type> <field_name> [= <default_value>]
}
  • package is optional and corresponds to the namespace of the generated code.
  • A .proto file can contain at most one service.
  • object defines a struct type.

#service

service HelloWorld {
    hello
    world
}
  • The braces contain a list of methods, one method name per line.
  • Method names can be any valid identifier.
  • Methods can be separated by , or ;, or the separators can be omitted.

The generated method key format is ServiceName.method_name.

#object

object User {
    int32 id
    string name
    int64 score = 0
}
  • The object name corresponds to the generated struct type name.
  • Field format: <type> <name> [= <default>].

#Types

Basic types:

bool
int
int32
int64
uint32
uint64
double
string

Object types:

User user

Or anonymous objects:

user {
    int32 id
    string name
}

Array types:

[string] tags
[int32] scores
[User] users

#Field Values

int32 id = 0
string name = "unknown"
double score = 3.14
bool enabled = true
  • = <value> is optional and represents the default value.
  • Supports bool, int, double, and string literals.

#Comments

// Single-line comment
/* Multi-line comment */

#Literals

  • Integers: 123, -32, +5
  • Hexadecimal: 0xFF, -0x10
  • Floating point: 3.14, 1e10, -2.5E-3
  • Boolean: true, false
  • Strings: single or double quotes; supports \r, \n, \t, \", \', \\

#Identifiers

[a-zA-Z_]([a-zA-Z_0-9]|\.[a-zA-Z_0-9])*
  • Starts with a letter or underscore.
  • Can contain letters, digits, underscores, and ..
  • After ., it must start with a letter or underscore.

#Syntax Constraints

  • A .proto file can contain at most one service.
  • Method names must not be duplicated.
  • Object names must not be duplicated.
  • Field names within the same object must not be duplicated.
  • Custom object types must have been defined earlier.

#Generated Code

For each service, a class is generated:

struct ServiceName : co::rpc_service {
    ServiceName();
    virtual ~ServiceName();

    virtual const char* name() const;
    virtual const co::map<const char*, rpc_method_t>& methods() const;

    virtual void method1(json::any& req, json::any& res) = 0;
    virtual void method2(json::any& req, json::any& res) = 0;

    co::map<const char*, rpc_method_t> _methods;
};
  • The constructor registers all methods, with keys ServiceName.method_name.
  • Each method is a pure virtual function that users must implement.
  • The namespace is determined by package.

#rpc_server

struct rpc_server {
    rpc_server(const char* ip, int port);
    ~rpc_server();

    rpc_server(const rpc_server&) = delete;
    rpc_server(rpc_server&&) = delete;
    void operator=(const rpc_server&) = delete;
    void operator=(rpc_server&&) = delete;

    rpc_server& add_service(co::unique<rpc_service>&& s);
    void start();
    void stop();
};
  • Implemented based on co::tcp_server, with one coroutine per connection.
  • add_service registers a service and returns *this, allowing chained calls.
  • start() starts the internal tcp_server.
  • stop() stops the internal tcp_server.

Example:

#include "co/rpc.h"
#include "co/co.h"
#include "hello_world.h"

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

    co::rpc_server s("0.0.0.0", 7788);
    s.add_service(co::make_unique<HelloWorldImpl>());
    s.start();

    co::sleep(60000);
    s.stop();
    return 0;
}

#rpc_client

struct rpc_client {
    rpc_client(const char* server_host, int server_port)
        : _tcp_cli(server_host, (uint16)server_port) {}

    rpc_client(const rpc_client& c)
        : _tcp_cli(c._tcp_cli) {}

    ~rpc_client() = default;

    rpc_client(rpc_client&&) = delete;
    void operator=(const rpc_client& c) = delete;
    void operator=(rpc_client&&) = delete;

    void call(const json::any& req, json::any& res);
    void ping();
    void close() { _tcp_cli.close(); }

    co::tcp_client _tcp_cli;
};
  • Copy construction only copies host / port, not the connection.
  • call: sends a request and receives a response.
  • ping: sends {"api":"ping"}.
  • close: closes the underlying connection.
rpc_client cannot be called by multiple coroutines at the same time; you can put rpc_client into co::pool for reuse.

Example:

#include "co/rpc.h"
#include "co/co.h"
#include "co/print.h"

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

    go([] {
        co::rpc_client c("127.0.0.1", 7788);

        json::any req = json::object();
        req.add_member("api", "HelloWorld.hello");

        json::any res;
        c.call(req, res);
        co::println("res = ", res.str());
    });

    co::sleep(5000);
    return 0;
}
flag Default Meaning
rpc_max_msg_size 8 << 20 (8M) Maximum message length
rpc_recv_timeout 3000 Receive timeout (milliseconds)
rpc_send_timeout 3000 Send timeout (milliseconds)
rpc_conn_timeout 3000 Connection timeout (milliseconds)
rpc_conn_idle_sec 180 Maximum connection idle time (seconds)
rpc_max_idle_conn 128 Maximum number of idle connections
rpc_log false Print RPC logs
  • Can be adjusted via command line or configuration file:
./app -rpc_log=true -rpc_recv_timeout=5000

#Notes

  • The RPC protocol header is fixed at 8 bytes, with magic 0x7777.
  • Requests must contain the api field, and it must be a string.
  • The method name format is ServiceName.method_name.
  • The built-in method ping responds with {"res":"pong"}.
  • rpc_client::call attempts to connect if not connected; returns directly on failure.
  • rpc_client cannot be called by multiple threads at the same time.
  • Copy construction of rpc_client only copies host / port, not the connection.
  • When adding a service, duplicate service or method names will cause log::check to fail.
  • flag::parse is required to start the underlying threads.
  • Users usually inherit from the class generated by gen, rather than directly inheriting from co::rpc_service.
  • A .proto file can contain at most one service.