#头文件
#include "co/rpc.h"
API 在 co 命名空间,同时提供别名:
namespace rpc {
using service = co::rpc_service;
using server = co::rpc_server;
using client = co::rpc_client;
using method_t = co::rpc_method_t;
}
使用前需在 main 开头调用 flag::parse(argc, argv);。
#概述
- 基于 TCP + JSON 的轻量 RPC。
- 协议:8 字节 header + JSON body。
- 请求通过
api字段指定方法,格式ServiceName.method_name,如HelloWorld.hello。
#协议
#Header
struct Header {
uint16 flags; // reserved, 0
uint16 magic; // 0x7777
uint32 len; // body len, network byte order
}; // 8 bytes
magic = 0x7777,用于校验。len是 body 长度,网络字节序。- body 是 JSON 文本。
#请求
{"api": "ServiceName.method_name", ...}
api必须是 string。- 其他字段是用户参数。
#响应
- 成功:用户方法填充的 JSON。
- 失败:
{"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()返回服务名。methods()返回方法名到rpc_method_t的映射。- 通过
rpc_server::add_service注册到 rpc_server 中。 rpc_method_t:第一个参数是请求 JSON,第二个是响应 JSON。
#gen 工具
根据 .proto 文件生成 rpc_service 子类。在 coost 根目录执行如下命令构建 gen:
xmake b gen
#使用方式
hello_world.proto:
package xx
service HelloWorld {
hello
world
}
执行:
gen hello_world.proto
生成 hello_world.h,包含 xx::HelloWorld 类:
// 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
用户继承并实现纯虚方法:
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 语法
#程序结构
package <package_name>
service <ServiceName> {
method1
method2
}
object <ObjectName> {
<field_type> <field_name> [= <default_value>]
}
package可选,对应生成代码的命名空间。- 一个
.proto文件最多一个service。 object定义结构体类型。
#service
service HelloWorld {
hello
world
}
- 大括号内是方法列表,每行一个方法名。
- 方法名可以是任意合法标识符。
- 方法之间可用
,或;分隔,也可省略。
生成的方法键格式为 ServiceName.method_name。
#object
object User {
int32 id
string name
int64 score = 0
}
object名对应生成的结构体类型名。- 字段格式:
<type> <name> [= <default>]。
#类型
基础类型:
bool
int
int32
int64
uint32
uint64
double
string
对象类型:
User user
或匿名对象:
user {
int32 id
string name
}
数组类型:
[string] tags
[int32] scores
[User] users
#字段值
int32 id = 0
string name = "unknown"
double score = 3.14
bool enabled = true
= <value>可选,表示默认值。- 支持 bool、int、double、string 字面量。
#注释
// 单行注释
/* 多行注释 */
#字面量
- 整数:
123、-32、+5 - 十六进制:
0xFF、-0x10 - 浮点数:
3.14、1e10、-2.5E-3 - 布尔:
true、false - 字符串:单引号或双引号,支持
\r、\n、\t、\"、\'、\\
#标识符
[a-zA-Z_]([a-zA-Z_0-9]|\.[a-zA-Z_0-9])*
- 字母或下划线开头。
- 可包含字母、数字、下划线、
.。 .后必须是字母或下划线开头。
#语法约束
- 一个
.proto最多一个service。 - 方法名不能重复。
- 对象名不能重复。
- 同一对象内字段名不能重复。
- 自定义对象类型必须在之前已定义。
#生成的代码
对每个 service 生成一个类:
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;
};
- 构造函数注册所有方法,键为
ServiceName.method_name。 - 每个方法都是纯虚函数,用户必须实现。
- 命名空间由
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();
};
- 基于
co::tcp_server实现,每连接一个协程。 add_service注册服务,返回*this,可链式调用。start()启动内部tcp_server。stop()停止内部tcp_server。
示例:
#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;
};
- 拷贝构造只复制 host / port,不复制连接。
call:发送请求,接收响应。ping:发送{"api":"ping"}。close:关闭底层连接。
rpc_client 不能多协程同时 call,可以将 rpc_client 放入 co::pool 复用。
示例:
#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 相关 flag
| flag | 默认值 | 含义 |
|---|---|---|
rpc_max_msg_size |
8 << 20(8M) |
最大消息长度 |
rpc_recv_timeout |
3000 |
接收超时(毫秒) |
rpc_send_timeout |
3000 |
发送超时(毫秒) |
rpc_conn_timeout |
3000 |
连接超时(毫秒) |
rpc_conn_idle_sec |
180 |
连接最大空闲时间(秒) |
rpc_max_idle_conn |
128 |
最大空闲连接数 |
rpc_log |
false |
打印 RPC 日志 |
- 可用命令行或配置文件调整:
./app -rpc_log=true -rpc_recv_timeout=5000
#注意事项
- RPC 协议 header 固定 8 字节,magic 为
0x7777。 - 请求必须包含
api字段,且为 string。 - 方法名格式
ServiceName.method_name。 - 内置方法
ping,响应{"res":"pong"}。 rpc_client::call未连接时会尝试连接,失败直接返回。rpc_client不能多线程同时call。rpc_client拷贝构造只复制 host / port,不复制连接。add_service时同名服务或方法会log::check失败。- 需要
flag::parse才会启动底层线程。 - 用户通常继承
gen生成的类,而非直接继承co::rpc_service。 - 一个
.proto文件最多一个service。