查看原文
其他

硬核干货丨教你写游戏服务器框架(二)

韩大wade 腾讯GWB游戏无界 2022-08-30

关于作者:韩伟,腾讯互娱高级工程师,目前在Next产品中心研发创新类型游戏。


本文为系列文章的第 2 篇,点击查看上一篇文章


对象序列化



现代编程技术中,面向对象是一个最常见的思想。因此不管是 C++ Java C#,还是 Python JS,都有对象的概念。虽然说面向对象并不是软件开发的“银弹”,但也不失为一种解决复杂逻辑的优秀工具。回到游戏服务器端程序来说,自然我会希望能有一定面向对象方面的支持。所以,从游戏服务器端的整个处理过程来看,我认为,有以下几个地方,是可以用对象来抽象业务数据的:


01

数据传输


我们可以把通过网络传输的数据,看成是一个对象。这样我们可以简单的构造一个对象,然后直接通过网络收、发它。


02

数据缓存


一批在内存中的,可以用对象进行“抽象”。而 key-value 的模型也是最常见的数据容器,因此我们可以起码把 key-value 中的 value 作为对象处理。


03

数据持久化


长久以来,我们使用 SQL 的二维表结构来持久化数据。但是 ORM (对象关系映射)的库一直非常流行,就是想在二维表和对象之间搭起桥梁。现在 NoSQL 的使用越来越常见,其实也是一种 key-value 模型。所以也是可以视为一个存放“对象”的工具。


对象序列化的标准模式也很常见,因此我定义成:


  1. class Serializable {

  2. public:


  3.    /**

  4.     * 序列化到一个数组中

  5.     * @param buffer 目地缓冲区数组

  6.     * @param buffer_length 缓冲区长度

  7.     * @return 返回写入了 buffer 的数据长度。如果返回 -1 表示出错,比如 buffer_length 不够。

  8.     */

  9.    virtual ssize_t SerializeTo(char* buffer, int buffer_length) const = 0;


  10.    /**

  11.     * @brief 从一个 buffer 中读取 length 个字节,反序列化到本对象。

  12.     *@return  返回 0 表示成功,其他值表示出错。

  13.     */

  14.    virtual int SerializeFrom(const char* buffer, int length) = 0;


  15.    virtual ~Serializable(){}


  16. };


(*本文代码可左右滑动查看)


网络传输


有了对象序列化的定义,就可以从网络传输处使用了。因此专门在 Processor 层设计一个收发对象的处理器 ObjectProcessor,它可以接纳一种 ObjectHandler 的对象注册。这个对象根据注册的 Service 名字,负责把收发的接口从 Request, Response 里面的字节数组转换成对象,然后处理。


  1. // ObjectProcessor 定义

  2. class ObjectProcessor : public ProcessorHelper {

  3. public:

  4.    ObjectProcessor();

  5.    virtual ~ObjectProcessor();


  6.    // 继承自 Processor 处理器函数

  7.    virtual int Init(Server* server, Config* config = NULL);


  8.    // 继承自 Processor 的处理函数

  9.    virtual int Process(const Request& request, const Peer& peer);


  10.    virtual int Process(const Request& request, const Peer& peer, Server* server);


  11.    ///@brief 设置默认处理器,所有没有注册具体服务名字的消息都会用这个消息处理

  12.    inline void set_default_handler(ObjectHandler* default_handler) {

  13.        default_handler_ = default_handler;

  14.    }


  15.    /**

  16.     * @brief 针对 service_name,注册对应处理的 handler ,注意 handler 本身是带对象类型信息的。

  17.     * @param service_name 服务名字,通过 Request.service 传输

  18.     * @param handler 请求的处理对象

  19.     */

  20.    void Register(const std::string& service_name, ObjectHandler* handler);


  21.    /**

  22.     * @brief 使用 handler 自己的 GetName() 返回值,注册服务。

  23.     * 如果 handler->GetName() 返回 "" 字符串,则会替换默认处理器对象

  24.     * @param handler 服务处理对象。

  25.     */

  26.    void Register(ObjectHandler* handler);


  27.    ///@brief 关闭此服务

  28.    virtual int Close();


  29. private:

  30.    std::map<std::string, ObjectHandler*> handler_table_;

  31.    Config* config_;

  32.    ObjectHandler* default_handler_;


  33.    int ProcessBy(const Request& request, const Peer& peer,

  34.                  ObjectHandler* handler, Server* server = NULL);


  35.    int DefaultProcess(const Request& request, const Peer& peer, Server* server = NULL);


  36.    bool InitHandler(ObjectHandler* handler);


  37. };


  38. // ObjectHandler 定义

  39. class ObjectHandler : public Serializable, public Updateable {

  40. public:

  41.    ObjectHandler();


  42.    virtual ~ObjectHandler() ;


  43.    virtual void ProcessRequest(const Peer& peer);

  44.    virtual void ProcessRequest(const Peer& peer, Server* server);



  45.    /**

  46.     * DenOS 用此方法确定进行服务注册,你应该覆盖此方法。

  47.     * 默认名字为空,会注册为“默认服务”,就是所有找不到对应名字服务的请求,都会转发给此对象处理。

  48.     * @return 注册此服务的名字。在 Request.service 字段中传输。

  49.     */

  50.    virtual std::string GetName() ;


  51.    int Reply(const char* buffer, int length, const Peer& peer,

  52.              const std::string& service_name = "", Server* server = NULL) ;


  53.    int Inform(char* buffer, int length, const std::string& session_id,

  54.               const std::string& service_name, Server* server = NULL);


  55.    int Inform(char* buffer, int length, const Peer& peer,

  56.               const std::string& service_name = "", Server* server = NULL);


  57.    virtual int Init(Server* server, Config* config);


  58.    /**

  59.     * 如果需要在主循环中进行操作,可以实现此方法。

  60.     * 返回值小于 0 的话,此任务会被移除循环

  61.     */

  62.    virtual int Update() {

  63.        return 0;

  64.    }


  65. protected:

  66.    Server* server_;

  67.    std::map<int, MessageHeader> header_map_;

  68.    Response response_;

  69.    Notice notice_;

  70. };



由于我们对于可序列化的对象,要求一定要实现 Serializable 这个接口,所以所有需要收发的数据,都要定义一个类来实现这个接口。但是,这种强迫用户一定要实现某个接口的方式,可能会不够友好,因为针对业务逻辑设计的类,加上一个这种接口,会比较繁琐。为了解决这种问题,我利用 C++ 的模板功能,对于那些不想去实现 Serializable 的类型,使用一个额外的 Pack()/Upack() 模板方法,来插入具体的序列化和反序列化方法(定义 ObjectHandlerCast 模板)。这样除了可以减少实现类型的代码,还可以让接受消息处理的接口方法 ProcessObject() 直接获得对应的类型指针,而不是通过 Serializable 来强行转换。在这里,其实也有另外一个思路,就是把 Serializable 设计成一个模板类,也是可以减少强制类型转换。但是我考虑到,序列化和反序列化,以及处理业务对象,都是使用同样一个(或两个,一个输入一个输出)模板类型参数,不如直接统一到一个类型里面好了。


  1. // ObjectHandlerCast 模板定义

  2. /**

  3. * 用户继承这个模板的实例化类型,可以节省关于对象序列化的编写代码。

  4. * 直接编写开发业务逻辑的函数。

  5. */

  6. template<typename REQ, typename RES = REQ>

  7. class ObjectHandlerCast : public ObjectHandler {

  8. public:

  9.    ObjectHandlerCast()

  10.            : req_obj_(NULL),

  11.              res_obj_(NULL),

  12.              buffer_(NULL) {

  13.        buffer_ = new char[Message::MAX_MAESSAGE_LENGTH];

  14.        bzero(buffer_, Message::MAX_MAESSAGE_LENGTH);

  15.    }


  16.    virtual ~ObjectHandlerCast() {

  17.        delete[] buffer_;

  18.    }


  19.    /**

  20.     * 对于不想使用 obj_ 成员来实现 Serializable 接口的,可以实现此接口

  21.     */

  22.    virtual int Pack(char* buffer, int length, const RES& object) const {

  23.        return -1;

  24.    }


  25.    virtual int Unpack(const char* buffer, int length, REQ* object) {

  26.        return -1;

  27.    }


  28.    int ReplyObject(const RES& object, const Peer& peer,

  29.                    const std::string& service_name = "") {


  30.        res_obj_ = &object;

  31.        int len = SerializeTo(buffer_, Message::MAX_MAESSAGE_LENGTH);

  32.        if (len < 0)

  33.            return -1;

  34.        return Reply(buffer_, len, peer, service_name);

  35.    }


  36.    int InformObject(const std::string& session_id, const RES& object,

  37.                     const std::string& service_name) {


  38.        res_obj_ = &object;

  39.        int len = SerializeTo(buffer_, Message::MAX_MAESSAGE_LENGTH);

  40.        if (len < 0)

  41.            return -1;

  42.        return Inform(buffer_, len, session_id, service_name);

  43.    }


  44.    virtual void ProcessRequest(const Peer& peer, Server* server) {

  45.        REQ* obj = req_obj_;

  46.        ProcessObject(*obj, peer, server);

  47.        delete obj;

  48.        req_obj_ = NULL;

  49.    }


  50.    virtual void ProcessObject(const REQ& object, const Peer& peer) {

  51.        ERROR_LOG("This object have no process handler.");

  52.    }


  53.    virtual void ProcessObject(const REQ& object, const Peer& peer,

  54.                               Server* server) {

  55.        ProcessObject(object, peer);

  56.    }


  57. protected:

  58.    REQ* req_obj_;

  59.    const RES* res_obj_;


  60. private:

  61.    char* buffer_;


  62.    virtual ssize_t SerializeTo(char* buffer, int buffer_length) const {

  63.        ssize_t ret = 0;

  64.        ret = Pack(buffer, buffer_length, *res_obj_);

  65.        return ret;

  66.    }


  67.    virtual int SerializeFrom(const char* buffer, int length) {

  68.        req_obj_ = new REQ();  // 新建一个对象,为了协程中不被别的协程篡改

  69.        int ret = Unpack(buffer, length, req_obj_);

  70.        if (ret) {

  71.            delete req_obj_;

  72.            req_obj_ = NULL;

  73.        }

  74.        return ret;

  75.    }

  76. };



任何类型的对象,如果想要在这个框架中以网络收发,只要为他写一个模板,完成 Pack() 和 UnPack() 这两个方法,就完成了。看起来确实方便。(如果想节省注册的时候编写其“类名”,还需要完成一个简单的 GetName() 方法)


当我完成上面的设计,不禁赞叹 C++ 对于模板支持的好处。由于模板可以在编译时绑定,只要是具备“预设”的方法的任何类型,都可以自动生成一个符合既有继承结构的类。这对于框架设计来说,是一个巨大的便利。而且编译时绑定也把可能出现的类型错误,暴露在编译期。对比那些可以通过反射实现同样功能的技术,其实是更容易修正的问题。



缓冲和持久化


数据传输的对象序列化问题解决后,下来就是缓存和持久化。由于缓存和持久化,我的设计都是基于 Map 接口的,也就是一种 Key-Value 的方式,所以就没有设计模板,而是希望用户自己去实现 Serializable 接口。但是我也实现了最常用的几种可序列化对象的实现代码:


  • 固定长度的类型,比如 int 。序列化其实就是一个 memcpy() 而已。

  • std::string 字符串。这个需要使用 c_str() 变成一个字节数组。

  • JSON 格式串。使用了某个开源的 json 解析器。推荐 GITHUB 上的 Tencent/RapidJson。


数据缓冲



数据缓冲这个需求,虽然在互联网领域非常常见,但是游戏的缓冲和其他一些领域的缓冲,实际需求是有非常大的差别。这里的差别主要有:


1


游戏的缓冲要求延迟极低,而且需要对服务器性能占用极少,因为游戏运行过程中,会有非常非常频繁的缓冲读写操作。举个例子来说,一个“群体伤害”的技能,可能会涉及对几十上百个数据对象的修改。而且这个操作可能会以每秒几百上千次的频率请求服务器。如果我们以传统的 memcache 方式来建立缓冲,这么高频率的网络 IO 往往不能满足延迟的要求,而且非常容易导致服务器过载。


2


游戏的缓冲数据之间的关联性非常强。和一张张互不关联的订单,或者一条条浏览结果不一样。游戏缓冲中往往存放着一个完整的虚拟世界的描述。比如一个地区中有几个房间,每个房间里面有不通的角色,角色身上又有各种状态和道具。而角色会在不同的房间里切换,道具也经常在不同角色身上转移。这种复杂的关系会导致一个游戏操作,带来的是多个数据的同时修改。如果我们把数据分割放在多个不同的进程上,这种关联性的修改可能会让进程间通信发生严重的过载。


3


游戏的缓冲数据的安全性具有一个明显的特点:更新时间越短,变换频率越大的数据,安全性要求越低。这对于简化数据缓冲安全性涉及,非常具有价值。我们不需要过于追求缓冲的“一致性”和“时效”,对于一些异常情况下的“脏”数据丢失,游戏领域的忍耐程度往往比较高。只要我们能保证最终一致性,甚至丢失一定程度以内的数据,都是可以接受的。这给了我们不挑战 CAP 定律的情况下,设计分布式缓冲系统的机会。


基本模型


基于上面的分析,我首先希望是建立一个足够简单的缓冲使用模型,那就是 Map 模型。


  1. class DataMap : public Updateable {

  2. public:

  3.    DataMap();

  4.    virtual ~DataMap();


  5.    /**

  6.     * @brief 对于可能阻塞的异步操作,需要调用这个接口来驱动回调。

  7.     */

  8.    virtual int Update(){ return 0; }


  9.    /**

  10.     * @brief 获取 key 对应的数据。

  11.     * @param key 数据的 Key

  12.     * @param value_buf 是输出缓冲区指针

  13.     * @param value_buf_len 是缓冲区最大长度

  14.     * @return 返回 -1 表示找不到这个 key,返回 -2 表示 value_buf_len 太小,不足以读出数据。其他负数表示错误,返回 >= 0 的值表示 value 的长度

  15.     */

  16.    virtual int Get(const std::string& key, char* value_buf, int value_buf_len) = 0;

  17.    virtual int Get(const std::string& key, Serializable* value);


  18.    /**

  19.     * @brief 异步 Get 的接口

  20.     * @param key 获取数据的 Key

  21.     * @param callback 获取数据的回调对象,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL

  22.     * @return 返回 0 表示发起查询成功,其他值表示错误。

  23.     */

  24.    virtual int Get(const std::string& key, DataMapCallback* callback) = 0;


  25.    /**

  26.     * @brief 覆盖、写入key对应的缓冲数据。

  27.     * @return 成功返回0。 返回 -1 表示value数据太大,其他负数表示其他错误

  28.     */

  29.    virtual int Put(const std::string& key, const char* value_buf, int value_buf_len) = 0;

  30.    virtual int Put(const std::string& key, const Serializable& value);


  31.    /**

  32.     * 写入数据的异步接口,使用 callback 来通知写入结果

  33.     * @param key 数据 key

  34.     * @param value 数据 Value

  35.     * @param callback 写入结果会调用此对象的 PutResult() 方法

  36.     * @return 返回 0 表示准备操作成功

  37.     */

  38.    virtual int Put(const std::string& key, const Serializable& value, DataMapCallback* callback);

  39.    virtual int Put(const std::string& key, const char* value_buf, int value_buf_len, DataMapCallback* callback);


  40.    /**

  41.     * 删除 key 对应的数据

  42.     * @return 返回 0 表示成功删除,返回 -1 表示这个 key 本身就不存在,其他负数返回值表示其他错误。

  43.     */

  44.    virtual int Remove(const std::string& key) = 0;

  45.    virtual int Remove(const std::string&key, DataMapCallback* callback);


  46.    /**

  47.     * 是否有 Key 对应的数据

  48.     *@return  返回 key 值表示找到,0 表示找不到

  49.     */

  50.    virtual int ContainsKey(const std::string& key) = 0;


  51.    /**

  52.     * 异步 ContainsKey 接口,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL。

  53.     * 如果 key 存在,value_buf 则不为NULL,但也不保证指向任何可用数据。可能是目标数值,

  54.     * 也可能内部的某个空缓冲区。如果是在本地的数据,就会是目标数据,如果是远程的数据,

  55.     * 为了减少性能就不会传入具体的 value 数值。

  56.     */

  57.    virtual int ContainsKey(const std::string& key, DataMapCallback* callback) = 0;


  58.    /**

  59.     * 遍历整个缓存。

  60.     * @param callback 每条记录都会调用 callback 对象的 FetchData() 方法

  61.     * @return 返回 0 表示成功,其他表示错误。

  62.     */

  63.    virtual int GetAll(DataMapCallback* callback) = 0;


  64.    /**

  65.     * 获取整个缓存的数据

  66.     * @param result 结果会放在这个 map 里面,记得每条记录中的 Bytes 的 buffer_ptr 需要 delete[]

  67.     * @return 返回 0 表示成功,其他表示错误

  68.     */

  69.    virtual int GetAll(std::map<std::string, Bytes>* result);

  70. private:

  71.    char* tmp_buffer_;



  72. };



这个接口其实只是一个 std::map 的简单模仿,把 key 固定成 string ,而把 value 固定成一个 buffer 或者是一个可序列化对象。另外为了实现分布式的缓冲,所有的接口都增加了回调接口。


可以用来充当数据缓存的业界方案其实非常多,他们包括:

  • 堆内存,这个是最简单的缓存容器

  • Redis

  • Memcached

  • ZooKeeper 这个自带了和进程关联的数据管理


由于我希望这个框架,可以让程序自由的选用不同的缓冲存储“设备”,比如在测试的时候,可以不按照任何其他软件,直接用自己的内存做缓冲,而在运营或者其他情况下,可以使用 Redis 或其他的设备。所以我们可以编写代码来实现上面的 DataMap 接口,以实现不同的缓冲存储方案。当然必须要把最简单的,使用堆内存的实现完成: RamMap


分布式设计


如果作为一个仅仅在“本地”服务器使用的缓冲,上面的 DataMap 已经足够了,但是我希望缓存是可以分布式的。不过,并不是任何的数据,都需要分布式存储,因为这会带来更多延迟和服务器负载。因此我希望设计一个接口,可以在使用时指定是否使用分布式存储,并且指定分布式存储的模式。


根据经验,在游戏领域中,分布式存储一般有以下几种模式:


1

本地模式


如果是分区分服的游戏,数据缓存全部放在一个地方即可。或者我们可以用一个 Redis 作为缓存存储点,然后多个游戏服务进程共同访问它。总之对于数据全部都缓存在一个地方的,都可以叫做本地模式。这也是最简单的缓冲模式。


2

按数据类型分布


这种模式和“本地模式”的差别,仅仅在于数据内容不同,就放在不同的地方。比如我们可以所有的场景数据放在一个 Redis 里面,然后把角色数据放在另外一个 Redis 里面。这种分布节点的选择是固定,仅仅根据数据类型来决定。这是为了减缓某一个缓冲节点的压力而设计。或者你对不同数据有缓冲隔离的需求:比如我不希望对用户的缓冲请求负载,影响对支付服务的缓冲请求负载。


3

按数据的 Key 分布


这是最复杂也最有价值的一种分布式缓存。因为缓冲模式是按照 Key-Vaule 的方式来存放的,所以我们可以把不同的 Key 的数据分布到不同节点上。如果刚好对应的 Key 数据,是分布在“本地”的,那么我们将获得本地操作的性能! 这种缓冲


4

按复制分布


就是多个节点间的数据一摸一样,在修改数据的时候,会广播到所有节点,这种是典型的读多写少性能好的模型。


在游戏开发中,我们往往习惯于把进程,按游戏所需要的数据来分布。比如我们会按照用户 ID ,把用户的状态数据,分布到不同的机器上,在登录的时候,就按照用户 ID 去引导客户端,直接连接到对应的服务器进程处。或者我们会把每个战斗副本或者游戏房间,放在不同的服务器上,所有的战斗操作请求,都会转发到对应的存放其副本、房间数据的服务器上。在这种开发中,我们会需要把大量的数据包路由、转发代码耦合到业务代码中。


如果我们按照上面的第 3 种模型,就可以把按“用户ID”或者“房间ID”分布的事情,交给底层缓冲模块去处理。当然如果仅仅这样做,也许会有大量的跨进程通信,导致性能下降。但是我们还可以增加一个“本地二级缓存”的设计,来提高性能。具体的流程大概为:


1.取 key 在本地二级缓存(一般是 RamMap)中读写数据。如果没有则从远端读取数据建立缓存,并在远端增加一条“二级缓存记录”。此记录包含了二级所在的服务器地址。


2.按 key 计算写入远端数据。根据“二级缓存记录”广播“清理二级缓存”的消息。此广播会忽略掉刚写入远端数据的那个服务节点。(此行为是异步的)


只要不是频繁的在不同的节点上写入同一个 Key 的记录,那么二级缓存的生存周期会足够长,从而提供足够好的性能。当然这种模式在同时多个写入记录时,有可能出现脏数据丢失或者覆盖,但可以再添加上乐观锁设计来防止。不过对于一般游戏业务,我们在 Key 的设计上,就应该尽量避免这种情况:如果涉及非常重要的,多个用户都可能修改的数据,应该避免使用“二级缓存”功能的模型 3 缓存,而是尽量利用服务器间通信,把请求集中转发到数据所在节点,以模式 1 (本地模式)使用缓冲。


以下为分布式设计的缓冲接口:


  1. /**

  2. * 定义几种网络缓冲模型

  3. */

  4. enum CacheType {

  5.    TypeLocal, ///< 本地

  6.    TypeName,  ///< 按 Cache 名字分布

  7.    TypeKey,   ///< 先按 Cache 名字分布,再按数据的 key 分布

  8.    TypeCopy   ///< 复制型分布

  9. };


  10. /**

  11. * @brief 定义了分布式缓存对象的基本结构

  12. */

  13. class Cache : public DataMap {

  14. public:


  15.    /**

  16.     * @brief 连接集群。

  17.     * @return 返回是否连接成功。

  18.     */

  19.    static bool EnsureCluster(Config* config = NULL);


  20.    /**

  21.     * 获得一个 Cache 对象。

  22.     * @attention 如果第一次调用此函数时,输入的 type, local_data_map 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。

  23.     * @param name 为名字

  24.     * @param type 此缓存希望是哪种类型

  25.     * @param local_data_map 为本地存放的数据容器。

  26.     * @return 如果是 NULL 表示已经达到 Cache 数量的上限。

  27.     */

  28.    static Cache* GetCache(const std::string& name, CacheType type,

  29.                           DataMap* local_data_map);

  30.    /**

  31.     * 获得一个 Cache 对象。

  32.     * @param  name 为名字

  33.     * @param local_data_map 为本地存放的数据容器。

  34.     * @note 如果第一次调用此函数时,输入的 type, class M 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。

  35.     * @return 如果是 NULL 表示已经达到 Cache 数量的上限。

  36.     */

  37.    template<class M>

  38.    static Cache* GetCache(const std::string& name, CacheType type,

  39.                           int* data_map_arg1 = NULL) {

  40.        DataMap* local_data_map = NULL;

  41.        std::map<std::string, DataMap*>::iterator it = cache_store_.find(name);

  42.        if (it != cache_store_.end()) {

  43.            local_data_map = it->second;

  44.        } else {

  45.            if (data_map_arg1 != NULL) {

  46.                local_data_map = new M(*data_map_arg1);

  47.            } else {

  48.                local_data_map = new M();

  49.            }

  50.            cache_store_[name] = local_data_map;

  51.        }

  52.        return GetCache(name, type, local_data_map);

  53.    }


  54.    /**

  55.     * 删除掉本进程内存放的 Cache

  56.     */

  57.    static void RemoveCache(const std::string& name);


  58.    explicit Cache(const std::string& name);

  59.    virtual ~Cache();

  60.    virtual std::string GetName() const;


  61. protected:

  62.    static std::map<std::string, Cache*> cache_map_;

  63.    std::string name_;


  64. private:

  65.    static int MAX_CACHE_NUM;

  66.    static std::map<std::string, DataMap*> cache_store_;


  67. };



持久化


长久以来,互联网的应用会使用类似 MySQL 这一类 SQL 数据库来存储数据。当然也有很多游戏是使用 SQL 数据库的,后来业界也出现“数据连接层”(DAL)的设计,其目的就是当需要更换不同的数据库时,可以不需要修改大量的代码。但是这种设计,依然是基于 SQL 这种抽象。然而不久之后,互联网业务都转向 NoSQL 的存储模型。实际上,游戏中对于玩家存档的数据,是完全可以不需要 SQL 这种关系型数据库的了。早期的游戏都是把玩家存档存放到文件里,就连游戏机如 PlayStation ,都是用存储卡就可以了。


一般来说,游戏中需要存储的数据会有两类:

  1. 玩家的存档数据

  2. 游戏中的各种设定数据


对于第一种数据,用 Key-Value 的方式基本上能满足。而第二种数据的模型可能会有很多种类,所以不需要特别的去规定什么模型。因此我设计了一个 key-value 模型的持久化结构。


  1. /**

  2. * @brief 由于持久化操作一般都是耗时等待的操作,所以需要回调接口来通知各种操作的结果。

  3. */

  4. class DataStoreCallback {

  5. public:

  6.    static int MAX_HANG_UP_CALLBACK_NUM;  // 最大回调挂起数

  7.    std::string key;

  8.    Serializable* value;


  9.    DataStoreCallback();

  10.    virtual ~DataStoreCallback();


  11.    /**

  12.     * 当 Init() 初始化结束时会被调用。

  13.     * @param  result 是初始化的结果。0 表示初始化成功。

  14.     * @param  msg 是初始化可能存在的错误信息。可能是空字符串 ""。

  15.     */

  16.    virtual void OnInit(int result, const std::string& msg);


  17.    /**

  18.     * 当调用 Get() 获得结果会被调用

  19.     * @param key

  20.     * @param value

  21.     * @param result 是 0 表示能获得对象,否则 value 中的数据可能是没有被修改的。

  22.     */

  23.    virtual void OnGot(const std::string& key, Serializable* value, int result);


  24.    /**

  25.     * 当调用 Put() 获得结果会被调用。

  26.     * @param key

  27.     * @param result 如果 result 是 0 表示写入成功,其他值表示失败。

  28.     */

  29.    virtual void OnPut(const std::string&key, int result);


  30.    /**

  31.     * 当调用 Remove() 获得结果会被调用

  32.     * @param key

  33.     * @param result 返回 0 表示删除成功,其他值表示失败,如这个 key 代表的对象并不存在。

  34.     */

  35.    virtual void OnRemove(const std::string&key, int result);


  36.    /**

  37.     * 准备在发起用户设置的回调,如果使用者没有单独为一个回调事务设置单独的回调对象。

  38.     * 在 PrepareRegisterCallback() 中会生成一个临时对象,在此处会被串点参数并清理。

  39.     * @param privdata 由底层回调机制所携带的回调标识参数,如果为 NULL 则返回 NULL。

  40.     * @param init_cb 初始化时传入的共用回调对象

  41.     * @return 可以发起回调的对象,如果为 NULL 表示参数 privdata 是 NULL

  42.     */

  43.    static DataStoreCallback* PrepareUseCallback(void* privdata,

  44.                                                 DataStoreCallback* init_cb);


  45.    /**

  46.     * 检查和登记回调对象,以防内存泄漏。

  47.     * @param callback 可以是NULL,会新建一个仅仅用于存放key数据的临时callback对象。

  48.     *  @return 返回一个callback对象,如果是 NULL 表示有太多的回调过程未被释放。

  49.     */

  50.    static DataStoreCallback* PrepareRegisterCallback(

  51.            DataStoreCallback* callback);


  52. protected:

  53.    static int kHandupCallbacks;  /// 有多少个回调指针被挂起,达到上限后会停止工作


  54. };


  55. /**

  56. *@brief 用来定义可以持久化对象的数据存储工具接口

  57. */

  58. class DataStore : public Component {

  59. public:

  60.    DataStore(DataStoreCallback* callback = NULL)

  61.            : callback_(callback) {

  62.        // Do nothing

  63.    }

  64.    virtual ~DataStore() {

  65.        // Do nothing

  66.    }


  67.    /**

  68.     * 初始化数据存取设备的方法,譬如去连接数据库、打开文件之类。

  69.     * @param config 配置对象

  70.     * @param callback 参数 callback 为基本回调对象,初始化的结果会回调其 OnInit() 函数通知用户。

  71.     * @return 基本的配置是否正确,返回 0 表示正常。

  72.     */

  73.    virtual int Init(Config* config, DataStoreCallback* callback);


  74.    virtual int Init(Application* app, Config* cfg){

  75.    app_ = app;

  76.    return Init(cfg, callback_);

  77.    }


  78.    virtual std::string GetName() {

  79.    return "den::DataStore";

  80.    }


  81.    virtual int Stop() {

  82.    Close();

  83.    return 0;  

  84.    }


  85.    /**

  86.     * 驱动存储接口程序运行,触发回调函数。

  87.     * @return 返回 0 表示此次没有进行任何操作,通知上层本次调用后可以 sleep 一下。

  88.     */

  89.    virtual int Update() = 0;


  90.    /**

  91.     * 关闭程序,关闭动作虽然是异步的,但不再返回结果,直接关闭就好。

  92.     */

  93.    virtual void Close() = 0;


  94.    /**

  95.     * 读取一个数据对象,通过 key ,把数据放入到 value,结果会回调通知 callback。

  96.     * 发起调用前,必须把 callback 的 value 字段设置为输出参数。

  97.     * @param key

  98.     * @param callback 如果NULL,则会回调从 Init() 中传入的 callback 对象。

  99.     * @return 0 表示发起请求成功,其他值为失败

  100.     */

  101.    virtual int Get(const std::string& key, DataStoreCallback* callback = NULL) = 0;


  102.    /**

  103.     * 写入一个数据对象,写入 key ,value,写入结果会回调通知 callback。

  104.     * @param key

  105.     * @param value

  106.     * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。

  107.     * @return 表示发起请求成功,其他值为失败

  108.     */

  109.    virtual int Put(const std::string&key, const Serializable& value,

  110.                    DataStoreCallback* callback = NULL) = 0;


  111.    /**

  112.     * 删除一个数据对象,通过 key ,结果会回调通知 callback。

  113.     * @param key

  114.     * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。

  115.     * @return 表示发起请求成功,其他值为失败

  116.     */

  117.    virtual int Remove(const std::string& key,

  118.                       DataStoreCallback* callback = NULL) = 0;


  119. protected:


  120.    /// 存放初始化传入的回调指针

  121.    DataStoreCallback* callback_;


  122. };



针对上面的 DataStore 模型,可以实现出多个具体的实现:

  1. 文件存储

  2. Redis

  3. 其他的各种数据库


基本上文件存储,是每个操作系统都会具备,所以在测试和一般场景下,是最方便的用法,所以这个是一定需要的。


在游戏的持久化数据里面,还有两类功能是比较常用的,一种是排行榜的使用;另外一种是拍卖行。这两个功能是基本的 Key-Value 无法完成的。使用 SQL 或者 Redis 一类 NOSQL 都有排序功能,所以实现排行榜问题不大。而拍卖行功能,则需要多个索引,所以只有一个索引的 Key-Value NoSQL 是无法满足的。不过 NOSQL 也可以“手工”的去建立多个 Key 的记录。不过这类需求,还真的很难统一到某一个框架里面,所以设计也是有限度,包含太多的东西可能还会有反效果。因此我并不打算在持久化这里包含太多的模型。


·END·


投稿请联系:微信 GAD013 / QQ 2074412784


今日推荐  

·“游戏无界” 腾讯游戏创意大赛正式启动

·“博弈论”对游戏设计有什么用?从“囚徒困境”谈起

·谁说女生不爱做游戏?聊聊你身边的女性游戏开发者

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存