Mosaic Game 的 RPC 实现
考虑到便利性的需求,mosaic_game使用基于json的RPC方案。为了避免前述的原始Json RPC方案里的各种需要手工维护的注册和序列化反序列化代码的重复劳动,mosaic_game也使用类似于UHT的工具来自动生成这些代码。mosaic_game使用的代码生成工具与UHT的原理有很大的不同。UHT是通过纯文本扫描项目中所有头文件里带UFunction, UProperty等标记的行来获取相关反射信息,而mosaic_game使用的是libclang对相关头文件进行完整的cpp语义解析,并以Json格式导出所有带标注的类型声明信息。有了这些类型声明信息之后,我们再使用基于mustache的模板引擎,生成所需的胶水代码来进行RPC的注册和序列化。这个基于libclang的cpp代码生成工具在mosaic_game中有很多应用场景,后面有单独的一章来描述此工具的相关细节。
Mosaic Game 中的RPC声明与注册
在mosaic_game中,我们支持了在Actor/ActorComponent, Space/SpaceComponent, Manager/ManagerComponent等类型上声明RPC,这三种RPC宿主实现基本一样,所以这里以Actor/ActorComponent来作为样例来说明。下面就是一个在PlayerChatComponent上的RPC声明:
Meta(rpc) void chat_add_msg_request(const utility::rpc_msg& msg, std::uint8_t chat_type, const std::string& to_player_id, const json::object_t& detail);
上面的函数声明中,Meta(rpc)的作用是提示libclang当前函数声明是一个RPC函数。在mosaic_game中我们规定所有的RPC的声明都要遵循如下结构:
- 左边必须以
Meta(rpc)开头,以通知libclang当前函数声明是一个RPC函数 - 函数的返回值必须为
void,即所有的RPC函数都是异步调用 - 函数的第一个参数必须为
const utility::rpc_msg&类型,这个类型用来容纳当前RPC的所有信息
struct rpc_msg
{
std::string cmd; // 当前rpc的名字
std::string err; // 如果是响应rpc 则此处填充响应错误信息 如果无错误则保持为空
std::vector<json> args; // 当前rpc的所有参数序列化为json之后的数组
std::string from; // 当前rpc的调用者信息 可选
std::uint32_t flag = 0; // 当前rpc的flag信息 例如是否允许客户端调用 是否允许服务器调用
// 下面两个接口提供到json的序列化与反序列化
friend void to_json(json& result, const utility::rpc_msg& msg);
bool from_json(const json& data);
}
- 函数的实际使用参数依次排在第一个参数之后
有了这个RPC函数声明之后,我们就可以使用libclang进行类型信息导出与胶水代码生成,针对每个带有RPC函数声明的xxx.h头文件,我们都会生成一个xxx.rpc.incpp文件,插入到xxx.cpp之中:
// player_chat_component.cpp
#include "component/player/player_chat_component.h"
#include "entity/player_entity.h"
#include "player_chat_component.rpc.incpp"
而这个xxx.rpc.incpp文件主要提供了一个内部类型xxx::rpc_helper的完整定义:
// player_chat_component.rpc.incpp
namespace spiritsaway::mosaic_game::entity
{
class player_chat_component::rpc_helper
{
static utility::rpc_msg::call_result chat_add_msg_request(player_chat_component* rpc_owner, const spiritsaway::utility::rpc_msg& cur_rpc)
{
std::remove_cv_t<std::remove_reference_t<unsigned char>> chat_type;
std::remove_cv_t<std::remove_reference_t<const std::string&>> to_player_id;
std::remove_cv_t<std::remove_reference_t<const json::object_t&>> detail;
if(!spiritsaway::serialize::decode_multi(cur_rpc.args, chat_type , to_player_id , detail ))
{
return utility::rpc_msg::call_result::invalid_format;
}
rpc_owner->chat_add_msg_request(cur_rpc, chat_type , to_player_id , detail );
return utility::rpc_msg::call_result::suc;
}
constexpr static std::uint32_t flag_for_chat_add_msg_request = 0;
static const std::unordered_map<std::string, spiritsaway::utility::rpc_cmd_info>& get_rpc_map()
{
static std::unordered_map<std::string, spiritsaway::utility::rpc_cmd_info> cur_rpc_map = {
{ "chat_add_msg_notify", spiritsaway::utility::rpc_cmd_info{ 0, "(const spiritsaway::utility::rpc_msg& msg, const std::string& chat_key, unsigned int msg_seq, const json::object_t& detail)", flag_for_chat_add_msg_notify } },
{ "chat_add_msg_reply", spiritsaway::utility::rpc_cmd_info{ 1, "(const spiritsaway::utility::rpc_msg& msg, const std::string& chat_key, unsigned int msg_seq, const json::object_t& detail)", flag_for_chat_add_msg_reply } },
{ "chat_add_msg_request", spiritsaway::utility::rpc_cmd_info{ 2, "(const spiritsaway::utility::rpc_msg& msg, unsigned char chat_type, const std::string& to_player_id, const json::object_t& detail)", flag_for_chat_add_msg_request } },
};
return cur_rpc_map;
}
static utility::rpc_msg::call_result rpc_call(player_chat_component* rpc_owner, std::uint32_t cur_rpc_idx, const spiritsaway::utility::rpc_msg& cur_rpc)
{
switch(cur_rpc_idx)
{
case 0:
return chat_add_msg_notify(rpc_owner, cur_rpc);
case 1:
return chat_add_msg_reply(rpc_owner, cur_rpc);
case 2:
return chat_add_msg_request(rpc_owner, cur_rpc);
default:
return utility::rpc_msg::call_result::rpc_not_found;
}
}
static utility::rpc_msg::call_result rpc_call(player_chat_component* rpc_owner, const spiritsaway::utility::rpc_msg& cur_rpc)
{
auto& cur_rpc_map = get_rpc_map();
auto cur_iter = cur_rpc_map.find(cur_rpc.cmd);
if(cur_iter == cur_rpc_map.end())
{
return utility::rpc_msg::call_result::rpc_not_found;
}
if(cur_rpc.flag && (cur_rpc.flag &cur_iter->second.cmd_flag) == 0)
{
return utility::rpc_msg::call_result::flag_not_meet;
}
return rpc_call(rpc_owner, std::uint32_t(cur_iter->second.cmd_idx), cur_rpc);
}
};
}
除了这个辅助类型的完整定义,这个rpc.incpp文件里还提供了如下两个函数的定义:
const std::unordered_map<std::string, spiritsaway::utility::rpc_cmd_info>& player_chat_component::get_rpc_indexes() const
{
return rpc_helper::get_rpc_map();
}
utility::rpc_msg::call_result player_chat_component::rpc_component_on_rpc(std::uint32_t cur_rpc_idx, const utility::rpc_msg& cur_msg)
{
return rpc_helper::rpc_call(this, cur_rpc_idx, cur_msg);
}
这两个函数的声明提供在base_component上,调用方则在component_owner上:
virtual utility::rpc_msg::call_result component_owner::rpc_owner_on_rpc(const utility::rpc_msg& cur_msg)
{
// 通过rpc名字查找该rpc的注册信息
auto cur_iter = m_component_register_info->component_rpcs.find(cur_msg.cmd);
if(cur_iter == m_component_register_info->component_rpcs.end())
{
return utility::rpc_msg::call_result::rpc_not_found;
}
// 找到之后判断flag是否满足 例如是否允许客户端调用
if(cur_msg.flag && (cur_msg.flag & cur_iter->second.cmd_flag) == 0)
{
return utility::rpc_msg::call_result::flag_not_meet;
}
// 这里的cmd_idx是一个uint64_t 由两个32位整数拼接而成
// 高32位代表所属component在component_owner的components数组的索引
// 低32位则是这个rpc在对应component里的索引
std::uint64_t rpc_idx = cur_iter->second.cmd_idx & 0xffffffff;
auto cur_component_idx = (cur_iter->second.cmd_idx >> 32) & 0xffffffff;
auto cur_comp = get_component(std::uint32_t(cur_component_idx));
if(!cur_comp)
{
return utility::rpc_msg::call_result::rpc_not_found;
}
// 找到对应component之后 调用base_component上提供的虚方法rpc_component_on_rpc
// 内部实现就是通过switch case来执行对应的rpc函数
return cur_comp->rpc_component_on_rpc(std::uint32_t(rpc_idx), cur_msg);
}
上面的RPC信息查找依赖于component_owner上维护好的m_component_register_info->component_rpcs信息,每个base_component在绑定到一个component_owner上之后,会将此base_component上的所有rpc都注册到component_rpcs这个 map中:
// 当一个component添加到某个owner之后会调用此函数
virtual void base_component::on_set_owner()
{
m_owner->add_component_rpcs(get_rpc_indexes(), m_component_type_id);
}
void component_owner::add_component_rpcs(const std::unordered_map<std::string, rpc_cmd_info>& rpc_names, const std::uint32_t component_type_id)
{
auto& component_rpc_registered = m_component_register_info->component_rpc_registered;
if(component_type_id >= component_rpc_registered.size())
{
component_rpc_registered.resize(component_type_id + 1);
}
// 避免重复注册
if(!component_rpc_registered[component_type_id])
{
for(const auto& one_rpc_name: rpc_names)
{
// 这里拼接两个uint32为一个uint64
m_component_register_info->component_rpcs[one_rpc_name.first] = rpc_cmd_info{(std::uint64_t(component_type_id)<<32) + one_rpc_name.second.cmd_idx, one_rpc_name.second.cmd_info};
}
}
}
通过上述base_component component_owner rpc_helper的配合,整体的一个RPC注册与调用流程就基本跑通了。
Mosaic Game 中的RPC序列化
上面一节的内容描述了一个Actor及相关ActorComponent上的所有RPC函数的注册与调用,为了让网络另外一端的特定Actor对象执行指定RPC,我们还需要解决RPC的序列化问题。此时我们来回顾一下之前我们构造的一个RPC封装结构:
struct rpc_msg
{
std::string cmd; // 当前rpc的名字
std::string err; // 如果是响应rpc 则此处填充响应错误信息 如果无错误则保持为空
std::vector<json> args; // 当前rpc的所有参数序列化为json之后的数组
std::string from; // 当前rpc的调用者信息 可选
std::uint32_t flag = 0; // 当前rpc的flag信息 例如是否允许客户端调用 是否允许服务器调用
// 下面两个接口提供到json的序列化与反序列化
friend void to_json(json& result, const utility::rpc_msg& msg);
bool from_json(const json& data);
}
从这个结构体的成员变量声明可以看出,我们需要将RPC的所有参数都转换为Json,然后依次填充到args字段上,同时将RPC的名字填充到cmd字段上。这里为了方便填充所有的RPC参数,提供了一个基于模板的set_args接口,这个接口会将传入的所有变参都调用开头提到的json encode接口自动转换为json类型,其实就等价于encode_multi:
template <typename... Args>
void set_args(Args&&... in_args)
{
args.reserve(sizeof...(Args));
(args.push_back(encode<Args>(std::forward<Args>(in_args))),...);
}
通过encode这个模板函数,我们不仅支持了那些本来就支持自动转换为json的基本类型,还支持了带json encode() const接口的任意类型作为RPC参数。
所有字段都填充好之后,再将这个结构体序列化为Json,这个序列化流程比较简单。Json可以进一步序列化为字符串,转变为字符串之后就可以直接进行网络数据传递,投送到网络另外一端的指定Actor,对应的Actor会将此字符串反序列化为Json,并调用rpc_msg::from_json来进行字段初始化,成功初始化之后,通过rpc_owner_on_rpc(const utility::rpc_msg& cur_msg)进行分发,调用xxx::rpc_heler上通过libclang生成的辅助函数,这个辅助函数则负责解析出当前RPC的所有参数,并调用最终执行的函数。这里的参数解析使用了前面提到的decode_multi,等价于encode_multi的逆过程。
class player_chat_component::rpc_helper
{
static utility::rpc_msg::call_result chat_add_msg_request(player_chat_component* rpc_owner, const spiritsaway::utility::rpc_msg& cur_rpc)
{
std::remove_cv_t<std::remove_reference_t<unsigned char>> chat_type;
std::remove_cv_t<std::remove_reference_t<const std::string&>> to_player_id;
std::remove_cv_t<std::remove_reference_t<const json::object_t&>> detail;
if(!spiritsaway::serialize::decode_multi(cur_rpc.args, chat_type , to_player_id , detail ))
{
return utility::rpc_msg::call_result::invalid_format;
}
rpc_owner->chat_add_msg_request(cur_rpc, chat_type , to_player_id , detail );
return utility::rpc_msg::call_result::suc;
}
};
实际使用中发现RPC的序列化数据中,cmd字段占据的长度相当可观。因为我们在给RPC赋予名字时,为了表达足够的信息,会不可避免的将名字弄得很长。为了降低RPC名字所需要的网络流量,我们参考Unreal Engine里RPC的实现, 将RPC的名字转换为一个uint32索引。为了实现这个名字索引机制,我们在mosaic_game中维护了一个常用术语表vector<string> cmd_vec,基于这个术语表构造出名字字符串与名字索引之间的映射std::unordered_map<std::string, std::uint32_t> cmd_map;:
void rpc_msg::init_cmd_vec(const std::vector<std::string>& in_cmd_vec)
{
assert(cmd_vec.empty());
cmd_vec.push_back({});
cmd_vec.insert(cmd_vec.end(), in_cmd_vec.begin(), in_cmd_vec.end());
cmd_vec = in_cmd_vec;
for(std::uint32_t i = 0; i< in_cmd_vec.size(); i++)
{
cmd_map[in_cmd_vec[i]] = i;
}
}
std::string rpc_msg::seq_to_cmd(std::uint32_t seq)
{
if(seq >= cmd_vec.size())
{
return {};
}
return cmd_vec[seq];
}
std::uint32_t rpc_msg::cmd_to_seq(const std::string& cmd)
{
auto temp_iter = cmd_map.find(cmd);
if(temp_iter == cmd_map.end())
{
return 0;
}
return temp_iter->second;
}
有了这个cmd_map cmd_vec之后,rpc_msg的序列化与反序列化需要做相应的修改:
void to_json(json& result, const utility::rpc_msg& msg)
{
auto temp_seq = utility::rpc_msg::cmd_to_seq(msg.cmd);
if(temp_seq == 0)
{
result["cmd"] = msg.cmd;
}
else
{
result["cmd"] = temp_seq;
}
// 其他字段的序列化
}
bool rpc_msg::from_json(const json& data)
{
auto temp_cmd_iter = data.find("cmd");
if(temp_cmd_iter == data.end())
{
return false;
}
if(temp_cmd_iter->is_string())
{
temp_cmd_iter->get_to(cmd);
}
else if(temp_cmd_iter->is_number_unsigned())
{
auto temp_seq = temp_cmd_iter->get<std::uint32_t>();
if(temp_seq)
{
cmd = seq_to_cmd(temp_seq);
}
else
{
return false;
}
}
else
{
return false;
}
// 其他字段的反序列化
}
整个常用名字术语表的维护则通过我们前述的libclang工具来完成的,大大的避免了人工处理时容易出现的各种纰漏。