RPC

#头文件

#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。

#协议

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。