#Header
#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
apifield, in the formatServiceName.method_name, such asHelloWorld.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.lenis the body length, in network byte order.- The body is JSON text.
#Request
{"api": "ServiceName.method_name", ...}
apimust 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 torpc_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>]
}
packageis optional and corresponds to the namespace of the generated code.- A
.protofile can contain at most oneservice. objectdefines 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
objectname 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
.protofile can contain at most oneservice. - 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_serviceregisters a service and returns*this, allowing chained calls.start()starts the internaltcp_server.stop()stops the internaltcp_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;
}
#RPC-Related Flags
| 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
apifield, and it must be a string. - The method name format is
ServiceName.method_name. - The built-in method
pingresponds with{"res":"pong"}. rpc_client::callattempts to connect if not connected; returns directly on failure.rpc_clientcannot be called by multiple threads at the same time.- Copy construction of
rpc_clientonly copies host / port, not the connection. - When adding a service, duplicate service or method names will cause
log::checkto fail. flag::parseis required to start the underlying threads.- Users usually inherit from the class generated by
gen, rather than directly inheriting fromco::rpc_service. - A
.protofile can contain at most oneservice.