消息处理框架
unity/c++ mmo大型多人在线游戏开发系列教程,本节代码在此。
本节我们来整理一下消息处理部分的代码,之前纯靠字符串解析,实在是有点简陋。
目标
消息分发应该是稳定的一个函数,不需要每加一个消息类型,就加一个if,理想的实现大致是:
void Service::handle_msg(TcpConnection* conn, const std::string& msg)
{
std::string msg_name = get_msg_name(msg);
std::string msg_params = get_msg_params(msg);
MsgHandlerFunc* msg_handler = find_msg_handler(msg_name);
if (msg_handler)
msg_handler(conn, msg_params);
}
为了实现这个函数,我们引入了好几个概念,下面我们来逐一解析。
消息名称
首先是msg_name
。这是消息的名称,用于索引到对应的消息处理函数,目前是一个字符串。
消息参数
其次是msg_params
。这是消息的参数,注意每个消息处理函数最终的需要的参数是不一样的,这个msg_params
需要经过进一步的解析才能被使用。
消息处理函数
最后是msg_handler
。这是消息处理函数,也就是上一节SpaceService::handle_msg
在if/else
里的那些函数。跟上一节不一样的是,现在消息参数是固定的msg_params
字符串,需要消息处理函数自行解析,例如login
会变成:
void SpaceService::login(TcpConnection* conn, const std::string& msg_params)
{
// 自行从msg_params中解析出需要的参数
const std::string username = get_string(msg_params);
// ...
}
MsgHandlerFunc
是消息处理器的类型,其实就是一个类成员函数指针类型,每个服务类都会定义自己的MsgHandlerFunc
:
using MsgHandlerFunc = void (SpaceService::*)(TcpConnection*, const std::string&);
消息处理函数在被使用前需要先注册,例如对SpaceService::login
来说,大致会有:
// name_2_handler作为一个静态成员变量存在,存放了消息名称到消息处理函数的映射
name_2_handler.insert(std::make_pair("login"), &SpaceSerivce::login);
每个消息处理函数都要加一行注册,可能是有点烦,这可以通过一些宏来减轻工作量,或者直接使用libclang
解析源码,然后自动生成。不过这些都不是本教程的重点,就先不搞了。
消息序列化
留意到前面新的login
实现中,我们写了一个get_string
,意图从字节流中解析一个字符串出来。可以想象,发送端必然也会有一个set_string
的操作,以写入字符串到字节流中。
同理,不单是字符串,所有消息的参数类型都要有对应的get/set
方法。
这就是消息的序列化。
我们当然可以自己写这部分的代码,例如字符串就是写入长度和字符串本身,整型就按c++版简易服务器
那一节末尾提到的数据压缩来写。但是,类型实在太多了,还有数组、自定义结构体之类。另外还有类似可选参数、版本兼容性之类的问题需要考虑。
很蓝的啦。
还好,网上已经有一大堆的开源的消息格式以及对应的开源库可以帮我们搞定这个问题。希望消息序列化之后可读性强的可以选json
或者xml
,希望序列化后数量量能尽量少的则可以选MsgPack
或者Protobuf
,还有其他各种。
甚至你自己可以再抽象一层,后面可以随时替换。
这里我就直接选用protobuf
了,成熟、开源跨平台、数据压缩比也不错。稍微痛点是每个消息都要定义一个protobuf
的消息类型,当然这也保证了消息的序列化很难写错。
所以login
的实现最终会是:
void SpaceService::login(TcpConnection* conn, const std::string& msg_bytes)
{
// 使用protobuf反序列化出username
space_service::LoginRequest login_req;
login_req.ParseFromString(msg_bytes);
const std::string& username = login_req.username();
// 同理,login_reply现在也使用protobuf去序列化参数
space_service::LoginReply login_reply;
int result = 0;
if (find_player(conn)) {
result = (int)LoginError::already_logined;
}
else if (_exists_names.contains(username)) {
result = (int)LoginError::name_exists;
}
else {
Player* player = new Player{ conn, username };
_conn_2_player.insert(std::make_pair(conn, player));
_exists_names.insert(username);
}
login_reply.set_result(result);
send_proto_msg(conn, "login_reply", login_reply);
}
包管理器
在引入protobuf
的依赖之后,我发现这些第三方的依赖还是使用包管理器去维护比较方便一点。对c++来说,可以使用conan
,这是一个跨平台的包管理器,安装和使用也很简单。
如果不想看conan
的文档的话,那记住下面这几条指令:
# 安装,记得先安装python3
pip install conan
# 生成全局默认配置
conan profile detect --force
# 写完conanfile.txt后,安装依赖,build_type根据需要可以改成Release
conan install . --build=missing --settings=build_type=Debug
# 一般上面的指令最后会输出一些提示,按着来写就好,我这边是提示:
cmake --preset conan-default
# 执行完之后,工程就生成好了
因此,我就把thirdparty
这个目录干掉了,直接写conanfile.txt
就好。
另外,Unity
也需要引入Protobuf
的支持,这就没什么包管理器了,直接建一个c#
项目,然后通过NutGet
安装Protobuf
,编译生成后,把bin
目录下的相关dll文件拖到Unity
项目中即可。
小结
到目前为止,消息处理框架就比较完善了,handle_msg
这个函数就稳定不变,消息序列化也不再使用简陋的字符串解析,而使用比较成熟的protobuf
。
下一节,我们来继续关注游戏方面的内容,移动同步。