游戏服务端架构实现-设计一个高性能HandleMap
设计原因
在使用C++ 开发游戏服务端中。 大多数我们的玩家对象 会使用一个 类 来表示。 比如 class Human 、class Player 、class Actor 等。在与客户端通信的时候 我们往往会生成一个唯一的 ID 与一个 实例化的 玩家对象 相绑定。 方便客户端发出如下的指令 (或者也叫协议) 如何高效的查找插入ID 对实时战斗类的游戏的性能影响是非常大的。而且 我吃饱了撑着想造轮子。
一个典型的协议
攻击 {
目标ID:12355,
使用技能ID:5
}
常用的实现
我见过大多数的服务端使用的是 std:map <unsigned int ,Player*> 类似的方式 来产生id 对 *Player 的映射。这样的好处就是简单 对于新生的ID 只需要 +1就可以了。 当数值达到 2^32 - 1时 表示 handle 用尽。
光查找来说这个性能在I5 4核心 处理器 100W次 大约300 毫秒左右。
《热血传奇》的实现
而我见过最牛逼的还是 热血传奇的服务端代码( delphi ) 直接将 Object 的地址转化为 int 传递给客户端 优点是两者的互相转化 0 消耗。而缺点也很显而易见,客户端可以伪造一个错误的ID 导致服务器崩溃。
所以为了避免 这种情况 使用了windows 平台 特有的 SEH 也就是
Try
转换 以及运行逻辑
Except
输出错误信息。
end;
调侃一下
而这种做法 一是典型的依赖 windows 而是 因为SEH 并不是 0 消耗的 实际上 这样降低了 程序的性能。 而且对于linux 的设计哲学来说, 应当暴露问题 而不是 隐藏起来 比如 也就是程序一旦遇到预期外的运行结果 应当,立马宕机 保存事故现场。所以这要求 程序的设计者对自己的程序流程 需要有一个严格的把控。而windows 提供的SEH 让很多 数据访问 或者写入 异常。不至于导致程序的崩溃。 让程序看起来 “稳定” 了很多。这两种设计哲学 好处坏处都很明显。门槛高低 和 头发的多少。所以业界调侃。服务端开发 分为 “服务端开发” 和 “windows 服务端开发” 。
性能对比
那么我考虑从新设计一种比这个快速的 映射列表。最快的查找当然是下标直接访问。先上一个结果对比图 再上具体实现。debug 情况下
自己实现的handle_map 插入100W 次 和 查询100W次的耗时分别为 1796 毫秒 和 532 毫秒。
使用std::unorder_map 实现的 插入100W次 和 查询100W次 耗时分别为 9031 毫秒 和 3281 毫秒。
实现原理
简单来说就是利用 取模 以及 整除 划分一个整数为 模数 和 整除 连个部分。
整除数部分 用于对 std::vector 的直接寻址。
vector 里头 保存两个数据 重用次数 和 用户数据。
那么便可以做到直接寻址。
使用模数部分 和 vector 里的 重用次数 进行相比较 即可 确定 这个分配出去的Handle 是否过期。
每次释放Handle 会加入 待重用列表 一个Handle 最大重用次数 为模数部分的最大值。
超过这个最大值这个下标将被标记为不可使用。
优缺点总结
优点: O(1) 的访问查询速度快。
缺点: 最大能同时容纳的Handle数量为 整除数部分的数据有效位。 Handle 会被耗尽 取决于使用的 Handle 对应的类型。耗尽以后 的选择 有 1.程序GG 2.重用过期的句柄(有可能造成冲突)
总结:
如果我使用 unsigned int 20个bit 来保存 整除数部分 那么就会如图上所诉 可以同时容纳100W个Handle (1 << 20 - 1) 。
对于游戏服务器来 说 如果用于玩家 或者 怪物 单个服务器承载100W 来说 绝对是够用了。(当然可以修改bit数量进行增减)
而 2^32 -2 个handle 用于生成 也是足够了 毕竟 一秒生成一个 也要好几百年。 当然 如果对于非常频繁的 那只能使用 unsigned int64来做为handle 了
github
已经将实现代码 和测试代码 放到github 上了 需要的请自取
https://github.com/SunYoung91/HandleMap