棋牌游戏开发教程系列:游戏服务器框架搭建(二)

对象序列化

棋牌游戏开发教程系列:游戏服务器框架搭建(二)


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

  • 数据传输:我们可以把通过网络传输的数据,看成是一个对象。这样我们可以简单的构造一个对象,然后直接通过网络收、发它。
  • 数据缓存:一批在内存中的,可以用对象进行“抽象”。而key-value的模型也是最常见的数据容器,因此我们可以起码把key-value中的value作为对象处理。
  • 数据持久化:长久以来,我们使用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
  10. ssize_t
  11. SerializeTo
  12. (
  13. char
  14. *
  15. buffer
  16. ,
  17. int
  18. buffer_length
  19. )
  20. const
  21. =
  22. 0
  23. ;
  24. /**
  25.      * @brief 从一个 buffer 中读取 length 个字节,反序列化到本对象。
  26.      *@return  返回 0 表示成功,其他值表示出错。
  27.      */
  28. virtual
  29. int
  30. SerializeFrom
  31. (
  32. const
  33. char
  34. *
  35. buffer
  36. ,
  37. int
  38. length
  39. )
  40. =
  41. 0
  42. ;
  43. virtual
  44. ~
  45. Serializable
  46. (){}
  47. };

复制代码



网络传输

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


  1. // ObjectProcessor 定义
  2. class
  3. ObjectProcessor
  4. :
  5. public
  6. ProcessorHelper
  7. {
  8. public
  9. :
  10. ObjectProcessor
  11. ();
  12. virtual
  13. ~
  14. ObjectProcessor
  15. ();
  16. // 继承自 Processor 处理器函数
  17. virtual
  18. int
  19. Init
  20. (
  21. Server
  22. *
  23. server
  24. ,
  25. Config
  26. *
  27. config
  28. =
  29. NULL
  30. );
  31. // 继承自 Processor 的处理函数
  32. virtual
  33. int
  34. Process
  35. (
  36. const
  37. Request
  38. &
  39. request
  40. ,
  41. const
  42. Peer
  43. &
  44. peer
  45. );
  46. virtual
  47. int
  48. Process
  49. (
  50. const
  51. Request
  52. &
  53. request
  54. ,
  55. const
  56. Peer
  57. &
  58. peer
  59. ,
  60. Server
  61. *
  62. server
  63. );
  64. ///@brief 设置默认处理器,所有没有注册具体服务名字的消息都会用这个消息处理
  65. inline
  66. void
  67. set_default_handler
  68. (
  69. ObjectHandler
  70. *
  71. default_handler
  72. )
  73. {
  74.         default_handler_
  75. =
  76. default_handler
  77. ;
  78. }
  79. /**
  80.      * @brief 针对 service_name,注册对应处理的 handler ,注意 handler 本身是带对象类型信息的。
  81.      * @param service_name 服务名字,通过 Request.service 传输
  82.      * @param handler 请求的处理对象
  83.      */
  84. void
  85. Register
  86. (
  87. const
  88. std
  89. ::
  90. string
  91. &
  92. service_name
  93. ,
  94. ObjectHandler
  95. *
  96. handler
  97. );
  98. /**
  99.      * @brief 使用 handler 自己的 GetName() 返回值,注册服务。
  100.      * 如果 handler->GetName() 返回 “” 字符串,则会替换默认处理器对象
  101.      * @param handler 服务处理对象。
  102.      */
  103. void
  104. Register
  105. (
  106. ObjectHandler
  107. *
  108. handler
  109. );
  110. ///@brief 关闭此服务
  111. virtual
  112. int
  113. Close
  114. ();
  115. private
  116. :
  117.     std
  118. ::
  119. map
  120. <
  121. std
  122. ::
  123. string
  124. ,
  125. ObjectHandler
  126. *>
  127. handler_table_
  128. ;
  129. Config
  130. *
  131. config_
  132. ;
  133. ObjectHandler
  134. *
  135. default_handler_
  136. ;
  137. int
  138. ProcessBy
  139. (
  140. const
  141. Request
  142. &
  143. request
  144. ,
  145. const
  146. Peer
  147. &
  148. peer
  149. ,
  150. ObjectHandler
  151. *
  152. handler
  153. ,
  154. Server
  155. *
  156. server
  157. =
  158. NULL
  159. );
  160. int
  161. DefaultProcess
  162. (
  163. const
  164. Request
  165. &
  166. request
  167. ,
  168. const
  169. Peer
  170. &
  171. peer
  172. ,
  173. Server
  174. *
  175. server
  176. =
  177. NULL
  178. );
  179. bool
  180. InitHandler
  181. (
  182. ObjectHandler
  183. *
  184. handler
  185. );
  186. };
  187. // ObjectHandler 定义
  188. class
  189. ObjectHandler
  190. :
  191. public
  192. Serializable
  193. ,
  194. public
  195. Updateable
  196. {
  197. public
  198. :
  199. ObjectHandler
  200. ();
  201. virtual
  202. ~
  203. ObjectHandler
  204. ()
  205. ;
  206. virtual
  207. void
  208. ProcessRequest
  209. (
  210. const
  211. Peer
  212. &
  213. peer
  214. );
  215. virtual
  216. void
  217. ProcessRequest
  218. (
  219. const
  220. Peer
  221. &
  222. peer
  223. ,
  224. Server
  225. *
  226. server
  227. );
  228. /**
  229.      * DenOS 用此方法确定进行服务注册,你应该覆盖此方法。
  230.      * 默认名字为空,会注册为“默认服务”,就是所有找不到对应名字服务的请求,都会转发给此对象处理。
  231.      * @return 注册此服务的名字。在 Request.service 字段中传输。
  232.      */
  233. virtual
  234. std
  235. ::
  236. string
  237. GetName
  238. ()
  239. ;
  240. int
  241. Reply
  242. (
  243. const
  244. char
  245. *
  246. buffer
  247. ,
  248. int
  249. length
  250. ,
  251. const
  252. Peer
  253. &
  254. peer
  255. ,
  256. const
  257. std
  258. ::
  259. string
  260. &
  261. service_name
  262. =
  263. “”
  264. ,
  265. Server
  266. *
  267. server
  268. =
  269. NULL
  270. )
  271. ;
  272. int
  273. Inform
  274. (
  275. char
  276. *
  277. buffer
  278. ,
  279. int
  280. length
  281. ,
  282. const
  283. std
  284. ::
  285. string
  286. &
  287. session_id
  288. ,
  289. const
  290. std
  291. ::
  292. string
  293. &
  294. service_name
  295. ,
  296. Server
  297. *
  298. server
  299. =
  300. NULL
  301. );
  302. int
  303. Inform
  304. (
  305. char
  306. *
  307. buffer
  308. ,
  309. int
  310. length
  311. ,
  312. const
  313. Peer
  314. &
  315. peer
  316. ,
  317. const
  318. std
  319. ::
  320. string
  321. &
  322. service_name
  323. =
  324. “”
  325. ,
  326. Server
  327. *
  328. server
  329. =
  330. NULL
  331. );
  332. virtual
  333. int
  334. Init
  335. (
  336. Server
  337. *
  338. server
  339. ,
  340. Config
  341. *
  342. config
  343. );
  344. /**
  345.      * 如果需要在主循环中进行操作,可以实现此方法。
  346.      * 返回值小于 0 的话,此任务会被移除循环
  347.      */
  348. virtual
  349. int
  350. Update
  351. ()
  352. {
  353. return
  354. 0
  355. ;
  356. }
  357. protected
  358. :
  359. Server
  360. *
  361. server_
  362. ;
  363.     std
  364. ::
  365. map
  366. <
  367. int
  368. ,
  369. MessageHeader
  370. >
  371. header_map_
  372. ;
  373. Response
  374. response_
  375. ;
  376. Notice
  377. notice_
  378. ;
  379. };

复制代码



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

  1. // ObjectHandlerCast 模板定义
  2. /**
  3. * 用户继承这个模板的实例化类型,可以节省关于对象序列化的编写代码。
  4. * 直接编写开发业务逻辑的函数。
  5. */
  6. template
  7. <
  8. typename
  9. REQ
  10. ,
  11. typename
  12. RES
  13. =
  14. REQ
  15. >
  16. class
  17. ObjectHandlerCast
  18. :
  19. public
  20. ObjectHandler
  21. {
  22. public
  23. :
  24. ObjectHandlerCast
  25. ()
  26. :
  27. req_obj_
  28. (
  29. NULL
  30. ),
  31.               res_obj_
  32. (
  33. NULL
  34. ),
  35.               buffer_
  36. (
  37. NULL
  38. )
  39. {
  40.         buffer_
  41. =
  42. new
  43. char
  44. [
  45. Message
  46. ::
  47. MAX_MAESSAGE_LENGTH
  48. ];
  49.         bzero
  50. (
  51. buffer_
  52. ,
  53. Message
  54. ::
  55. MAX_MAESSAGE_LENGTH
  56. );
  57. }
  58. virtual
  59. ~
  60. ObjectHandlerCast
  61. ()
  62. {
  63. delete
  64. []
  65. buffer_
  66. ;
  67. }
  68. /**
  69.      * 对于不想使用 obj_ 成员来实现 Serializable 接口的,可以实现此接口
  70.      */
  71. virtual
  72. int
  73. Pack
  74. (
  75. char
  76. *
  77. buffer
  78. ,
  79. int
  80. length
  81. ,
  82. const
  83. RES
  84. &
  85. object
  86. )
  87. const
  88. {
  89. return
  90. 1
  91. ;
  92. }
  93. virtual
  94. int
  95. Unpack
  96. (
  97. const
  98. char
  99. *
  100. buffer
  101. ,
  102. int
  103. length
  104. ,
  105. REQ
  106. *
  107. object
  108. )
  109. {
  110. return
  111. 1
  112. ;
  113. }
  114. int
  115. ReplyObject
  116. (
  117. const
  118. RES
  119. &
  120. object
  121. ,
  122. const
  123. Peer
  124. &
  125. peer
  126. ,
  127. const
  128. std
  129. ::
  130. string
  131. &
  132. service_name
  133. =
  134. “”
  135. )
  136. {
  137.         res_obj_
  138. =
  139. &
  140. object
  141. ;
  142. int
  143. len
  144. =
  145. SerializeTo
  146. (
  147. buffer_
  148. ,
  149. Message
  150. ::
  151. MAX_MAESSAGE_LENGTH
  152. );
  153. if
  154. (
  155. len
  156. <
  157. 0
  158. )
  159. return
  160. 1
  161. ;
  162. return
  163. Reply
  164. (
  165. buffer_
  166. ,
  167. len
  168. ,
  169. peer
  170. ,
  171. service_name
  172. );
  173. }
  174. int
  175. InformObject
  176. (
  177. const
  178. std
  179. ::
  180. string
  181. &
  182. session_id
  183. ,
  184. const
  185. RES
  186. &
  187. object
  188. ,
  189. const
  190. std
  191. ::
  192. string
  193. &
  194. service_name
  195. )
  196. {
  197.         res_obj_
  198. =
  199. &
  200. object
  201. ;
  202. int
  203. len
  204. =
  205. SerializeTo
  206. (
  207. buffer_
  208. ,
  209. Message
  210. ::
  211. MAX_MAESSAGE_LENGTH
  212. );
  213. if
  214. (
  215. len
  216. <
  217. 0
  218. )
  219. return
  220. 1
  221. ;
  222. return
  223. Inform
  224. (
  225. buffer_
  226. ,
  227. len
  228. ,
  229. session_id
  230. ,
  231. service_name
  232. );
  233. }
  234. virtual
  235. void
  236. ProcessRequest
  237. (
  238. const
  239. Peer
  240. &
  241. peer
  242. ,
  243. Server
  244. *
  245. server
  246. )
  247. {
  248.         REQ
  249. *
  250. obj
  251. =
  252. req_obj_
  253. ;
  254. ProcessObject
  255. (*
  256. obj
  257. ,
  258. peer
  259. ,
  260. server
  261. );
  262. delete
  263. obj
  264. ;
  265.         req_obj_
  266. =
  267. NULL
  268. ;
  269. }
  270. virtual
  271. void
  272. ProcessObject
  273. (
  274. const
  275. REQ
  276. &
  277. object
  278. ,
  279. const
  280. Peer
  281. &
  282. peer
  283. )
  284. {
  285.         ERROR_LOG
  286. (
  287. “This object have no process handler.”
  288. );
  289. }
  290. virtual
  291. void
  292. ProcessObject
  293. (
  294. const
  295. REQ
  296. &
  297. object
  298. ,
  299. const
  300. Peer
  301. &
  302. peer
  303. ,
  304. Server
  305. *
  306. server
  307. )
  308. {
  309. ProcessObject
  310. (
  311. object
  312. ,
  313. peer
  314. );
  315. }
  316. protected
  317. :
  318.     REQ
  319. *
  320. req_obj_
  321. ;
  322. const
  323. RES
  324. *
  325. res_obj_
  326. ;
  327. private
  328. :
  329. char
  330. *
  331. buffer_
  332. ;
  333. virtual
  334. ssize_t
  335. SerializeTo
  336. (
  337. char
  338. *
  339. buffer
  340. ,
  341. int
  342. buffer_length
  343. )
  344. const
  345. {
  346. ssize_t
  347. ret
  348. =
  349. 0
  350. ;
  351.         ret
  352. =
  353. Pack
  354. (
  355. buffer
  356. ,
  357. buffer_length
  358. ,
  359. *
  360. res_obj_
  361. );
  362. return
  363. ret
  364. ;
  365. }
  366. virtual
  367. int
  368. SerializeFrom
  369. (
  370. const
  371. char
  372. *
  373. buffer
  374. ,
  375. int
  376. length
  377. )
  378. {
  379.         req_obj_
  380. =
  381. new
  382. REQ
  383. ();
  384. // 新建一个对象,为了协程中不被别的协程篡改
  385. int
  386. ret
  387. =
  388. Unpack
  389. (
  390. buffer
  391. ,
  392. length
  393. ,
  394. req_obj_
  395. );
  396. if
  397. (
  398. ret
  399. )
  400. {
  401. delete
  402. req_obj_
  403. ;
  404.             req_obj_
  405. =
  406. NULL
  407. ;
  408. }
  409. return
  410. ret
  411. ;
  412. }
  413. };

复制代码



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

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

缓冲和持久化

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

  • 固定长度的类型,比如int。序列化其实就是一个memcpy()而已。
  • std::string字符串。这个需要使用c_str()变成一个字节数组。
  • JSON格式串。使用了某个开源的json解析器。推荐GITHUB上的Tencent/RapidJson。



数据缓冲

棋牌游戏开发教程系列:游戏服务器框架搭建(二)



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

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

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

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

基本模型

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

  1. class
  2. DataMap
  3. :
  4. public
  5. Updateable
  6. {
  7. public
  8. :
  9. DataMap
  10. ();
  11. virtual
  12. ~
  13. DataMap
  14. ();
  15. /**
  16.      * @brief 对于可能阻塞的异步操作,需要调用这个接口来驱动回调。
  17.      */
  18. virtual
  19. int
  20. Update
  21. (){
  22. return
  23. 0
  24. ;
  25. }
  26. /**
  27.      * @brief 获取 key 对应的数据。
  28.      * @param key 数据的 Key
  29.      * @param value_buf 是输出缓冲区指针
  30.      * @param value_buf_len 是缓冲区最大长度
  31.      * @return 返回 -1 表示找不到这个 key,返回 -2 表示 value_buf_len 太小,不足以读出数据。其他负数表示错误,返回 >= 0 的值表示 value 的长度
  32.      */
  33. virtual
  34. int
  35. Get
  36. (
  37. const
  38. std
  39. ::
  40. string
  41. &
  42. key
  43. ,
  44. char
  45. *
  46. value_buf
  47. ,
  48. int
  49. value_buf_len
  50. )
  51. =
  52. 0
  53. ;
  54. virtual
  55. int
  56. Get
  57. (
  58. const
  59. std
  60. ::
  61. string
  62. &
  63. key
  64. ,
  65. Serializable
  66. *
  67. value
  68. );
  69. /**
  70.      * @brief 异步 Get 的接口
  71.      * @param key 获取数据的 Key
  72.      * @param callback 获取数据的回调对象,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL
  73.      * @return 返回 0 表示发起查询成功,其他值表示错误。
  74.      */
  75. virtual
  76. int
  77. Get
  78. (
  79. const
  80. std
  81. ::
  82. string
  83. &
  84. key
  85. ,
  86. DataMapCallback
  87. *
  88. callback
  89. )
  90. =
  91. 0
  92. ;
  93. /**
  94.      * @brief 覆盖、写入key对应的缓冲数据。
  95.      * @return 成功返回0。 返回 -1 表示value数据太大,其他负数表示其他错误
  96.      */
  97. virtual
  98. int
  99. Put
  100. (
  101. const
  102. std
  103. ::
  104. string
  105. &
  106. key
  107. ,
  108. const
  109. char
  110. *
  111. value_buf
  112. ,
  113. int
  114. value_buf_len
  115. )
  116. =
  117. 0
  118. ;
  119. virtual
  120. int
  121. Put
  122. (
  123. const
  124. std
  125. ::
  126. string
  127. &
  128. key
  129. ,
  130. const
  131. Serializable
  132. &
  133. value
  134. );
  135. /**
  136.      * 写入数据的异步接口,使用 callback 来通知写入结果
  137.      * @param key 数据 key
  138.      * @param value 数据 Value
  139.      * @param callback 写入结果会调用此对象的 PutResult() 方法
  140.      * @return 返回 0 表示准备操作成功
  141.      */
  142. virtual
  143. int
  144. Put
  145. (
  146. const
  147. std
  148. ::
  149. string
  150. &
  151. key
  152. ,
  153. const
  154. Serializable
  155. &
  156. value
  157. ,
  158. DataMapCallback
  159. *
  160. callback
  161. );
  162. virtual
  163. int
  164. Put
  165. (
  166. const
  167. std
  168. ::
  169. string
  170. &
  171. key
  172. ,
  173. const
  174. char
  175. *
  176. value_buf
  177. ,
  178. int
  179. value_buf_len
  180. ,
  181. DataMapCallback
  182. *
  183. callback
  184. );
  185. /**
  186.      * 删除 key 对应的数据
  187.      * @return 返回 0 表示成功删除,返回 -1 表示这个 key 本身就不存在,其他负数返回值表示其他错误。
  188.      */
  189. virtual
  190. int
  191. Remove
  192. (
  193. const
  194. std
  195. ::
  196. string
  197. &
  198. key
  199. )
  200. =
  201. 0
  202. ;
  203. virtual
  204. int
  205. Remove
  206. (
  207. const
  208. std
  209. ::
  210. string
  211. &
  212. key
  213. ,
  214. DataMapCallback
  215. *
  216. callback
  217. );
  218. /**
  219.      * 是否有 Key 对应的数据
  220.      *@return  返回 key 值表示找到,0 表示找不到
  221.      */
  222. virtual
  223. int
  224. ContainsKey
  225. (
  226. const
  227. std
  228. ::
  229. string
  230. &
  231. key
  232. )
  233. =
  234. 0
  235. ;
  236. /**
  237.      * 异步 ContainsKey 接口,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL。
  238.      * 如果 key 存在,value_buf 则不为NULL,但也不保证指向任何可用数据。可能是目标数值,
  239.      * 也可能内部的某个空缓冲区。如果是在本地的数据,就会是目标数据,如果是远程的数据,
  240.      * 为了减少性能就不会传入具体的 value 数值。
  241.      */
  242. virtual
  243. int
  244. ContainsKey
  245. (
  246. const
  247. std
  248. ::
  249. string
  250. &
  251. key
  252. ,
  253. DataMapCallback
  254. *
  255. callback
  256. )
  257. =
  258. 0
  259. ;
  260. /**
  261.      * 遍历整个缓存。
  262.      * @param callback 每条记录都会调用 callback 对象的 FetchData() 方法
  263.      * @return 返回 0 表示成功,其他表示错误。
  264.      */
  265. virtual
  266. int
  267. GetAll
  268. (
  269. DataMapCallback
  270. *
  271. callback
  272. )
  273. =
  274. 0
  275. ;
  276. /**
  277.      * 获取整个缓存的数据
  278.      * @param result 结果会放在这个 map 里面,记得每条记录中的 Bytes 的 buffer_ptr 需要 delete[]
  279.      * @return 返回 0 表示成功,其他表示错误
  280.      */
  281. virtual
  282. int
  283. GetAll
  284. (
  285. std
  286. ::
  287. map
  288. <
  289. std
  290. ::
  291. string
  292. ,
  293. Bytes
  294. >*
  295. result
  296. );
  297. private
  298. :
  299. char
  300. *
  301. tmp_buffer_
  302. ;
  303. };

复制代码



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

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

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

Redis

Memcached

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

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

分布式设计

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

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

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

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

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

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

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

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

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

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

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

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

  1. /**
  2. * 定义几种网络缓冲模型
  3. */
  4. enum
  5. CacheType
  6. {
  7. TypeLocal
  8. ,
  9. ///< 本地
  10. TypeName
  11. ,
  12. ///< 按 Cache 名字分布
  13. TypeKey
  14. ,
  15. ///< 先按 Cache 名字分布,再按数据的 key 分布
  16. TypeCopy
  17. ///< 复制型分布
  18. };
  19. /**
  20. * @brief 定义了分布式缓存对象的基本结构
  21. */
  22. class
  23. Cache
  24. :
  25. public
  26. DataMap
  27. {
  28. public
  29. :
  30. /**
  31.      * @brief 连接集群。
  32.      * @return 返回是否连接成功。
  33.      */
  34. static
  35. bool
  36. EnsureCluster
  37. (
  38. Config
  39. *
  40. config
  41. =
  42. NULL
  43. );
  44. /**
  45.      * 获得一个 Cache 对象。
  46.      * @attention 如果第一次调用此函数时,输入的 type, local_data_map 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。
  47.      * @param name 为名字
  48.      * @param type 此缓存希望是哪种类型
  49.      * @param local_data_map 为本地存放的数据容器。
  50.      * @return 如果是 NULL 表示已经达到 Cache 数量的上限。
  51.      */
  52. static
  53. Cache
  54. *
  55. GetCache
  56. (
  57. const
  58. std
  59. ::
  60. string
  61. &
  62. name
  63. ,
  64. CacheType
  65. type
  66. ,
  67. DataMap
  68. *
  69. local_data_map
  70. );
  71. /**
  72.      * 获得一个 Cache 对象。
  73.      * @param  name 为名字
  74.      * @param local_data_map 为本地存放的数据容器。
  75.      * @note 如果第一次调用此函数时,输入的 type, class M 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。
  76.      * @return 如果是 NULL 表示已经达到 Cache 数量的上限。
  77.      */
  78. template
  79. <
  80. class
  81. M
  82. >
  83. static
  84. Cache
  85. *
  86. GetCache
  87. (
  88. const
  89. std
  90. ::
  91. string
  92. &
  93. name
  94. ,
  95. CacheType
  96. type
  97. ,
  98. int
  99. *
  100. data_map_arg1
  101. =
  102. NULL
  103. )
  104. {
  105. DataMap
  106. *
  107. local_data_map
  108. =
  109. NULL
  110. ;
  111.         std
  112. ::
  113. map
  114. <
  115. std
  116. ::
  117. string
  118. ,
  119. DataMap
  120. *>::
  121. iterator
  122. it
  123. =
  124. cache_store_
  125. .
  126. find
  127. (
  128. name
  129. );
  130. if
  131. (
  132. it
  133. !=
  134. cache_store_
  135. .
  136. end
  137. ())
  138. {
  139.             local_data_map
  140. =
  141. it
  142. ->
  143. second
  144. ;
  145. }
  146. else
  147. {
  148. if
  149. (
  150. data_map_arg1
  151. !=
  152. NULL
  153. )
  154. {
  155.                 local_data_map
  156. =
  157. new
  158. M
  159. (*
  160. data_map_arg1
  161. );
  162. }
  163. else
  164. {
  165.                 local_data_map
  166. =
  167. new
  168. M
  169. ();
  170. }
  171.             cache_store_
  172. [
  173. name
  174. ]
  175. =
  176. local_data_map
  177. ;
  178. }
  179. return
  180. GetCache
  181. (
  182. name
  183. ,
  184. type
  185. ,
  186. local_data_map
  187. );
  188. }
  189. /**
  190.      * 删除掉本进程内存放的 Cache
  191.      */
  192. static
  193. void
  194. RemoveCache
  195. (
  196. const
  197. std
  198. ::
  199. string
  200. &
  201. name
  202. );
  203. explicit
  204. Cache
  205. (
  206. const
  207. std
  208. ::
  209. string
  210. &
  211. name
  212. );
  213. virtual
  214. ~
  215. Cache
  216. ();
  217. virtual
  218. std
  219. ::
  220. string
  221. GetName
  222. ()
  223. const
  224. ;
  225. protected
  226. :
  227. static
  228. std
  229. ::
  230. map
  231. <
  232. std
  233. ::
  234. string
  235. ,
  236. Cache
  237. *>
  238. cache_map_
  239. ;
  240.     std
  241. ::
  242. string name_
  243. ;
  244. private
  245. :
  246. static
  247. int
  248. MAX_CACHE_NUM
  249. ;
  250. static
  251. std
  252. ::
  253. map
  254. <
  255. std
  256. ::
  257. string
  258. ,
  259. DataMap
  260. *>
  261. cache_store_
  262. ;
  263. };

复制代码



持久化

棋牌游戏开发教程系列:游戏服务器框架搭建(二)



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

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

玩家的存档数据

游戏中的各种设定数据

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

  1. /**
  2. * @brief 由于持久化操作一般都是耗时等待的操作,所以需要回调接口来通知各种操作的结果。
  3. */
  4. class
  5. DataStoreCallback
  6. {
  7. public
  8. :
  9. static
  10. int
  11. MAX_HANG_UP_CALLBACK_NUM
  12. ;
  13. // 最大回调挂起数
  14.     std
  15. ::
  16. string key
  17. ;
  18. Serializable
  19. *
  20. value
  21. ;
  22. DataStoreCallback
  23. ();
  24. virtual
  25. ~
  26. DataStoreCallback
  27. ();
  28. /**
  29.      * 当 Init() 初始化结束时会被调用。
  30.      * @param  result 是初始化的结果。0 表示初始化成功。
  31.      * @param  msg 是初始化可能存在的错误信息。可能是空字符串 “”。
  32.      */
  33. virtual
  34. void
  35. OnInit
  36. (
  37. int
  38. result
  39. ,
  40. const
  41. std
  42. ::
  43. string
  44. &
  45. msg
  46. );
  47. /**
  48.      * 当调用 Get() 获得结果会被调用
  49.      * @param key
  50.      * @param value
  51.      * @param result 是 0 表示能获得对象,否则 value 中的数据可能是没有被修改的。
  52.      */
  53. virtual
  54. void
  55. OnGot
  56. (
  57. const
  58. std
  59. ::
  60. string
  61. &
  62. key
  63. ,
  64. Serializable
  65. *
  66. value
  67. ,
  68. int
  69. result
  70. );
  71. /**
  72.      * 当调用 Put() 获得结果会被调用。
  73.      * @param key
  74.      * @param result 如果 result 是 0 表示写入成功,其他值表示失败。
  75.      */
  76. virtual
  77. void
  78. OnPut
  79. (
  80. const
  81. std
  82. ::
  83. string
  84. &
  85. key
  86. ,
  87. int
  88. result
  89. );
  90. /**
  91.      * 当调用 Remove() 获得结果会被调用
  92.      * @param key
  93.      * @param result 返回 0 表示删除成功,其他值表示失败,如这个 key 代表的对象并不存在。
  94.      */
  95. virtual
  96. void
  97. OnRemove
  98. (
  99. const
  100. std
  101. ::
  102. string
  103. &
  104. key
  105. ,
  106. int
  107. result
  108. );
  109. /**
  110.      * 准备在发起用户设置的回调,如果使用者没有单独为一个回调事务设置单独的回调对象。
  111.      * 在 PrepareRegisterCallback() 中会生成一个临时对象,在此处会被串点参数并清理。
  112.      * @param privdata 由底层回调机制所携带的回调标识参数,如果为 NULL 则返回 NULL。
  113.      * @param init_cb 初始化时传入的共用回调对象
  114.      * @return 可以发起回调的对象,如果为 NULL 表示参数 privdata 是 NULL
  115.      */
  116. static
  117. DataStoreCallback
  118. *
  119. PrepareUseCallback
  120. (
  121. void
  122. *
  123. privdata
  124. ,
  125. DataStoreCallback
  126. *
  127. init_cb
  128. );
  129. /**
  130.      * 检查和登记回调对象,以防内存泄漏。
  131.      * @param callback 可以是NULL,会新建一个仅仅用于存放key数据的临时callback对象。
  132.      *  @return 返回一个callback对象,如果是 NULL 表示有太多的回调过程未被释放。
  133.      */
  134. static
  135. DataStoreCallback
  136. *
  137. PrepareRegisterCallback
  138. (
  139. DataStoreCallback
  140. *
  141. callback
  142. );
  143. protected
  144. :
  145. static
  146. int
  147. kHandupCallbacks
  148. ;
  149. /// 有多少个回调指针被挂起,达到上限后会停止工作
  150. };
  151. /**
  152. *@brief 用来定义可以持久化对象的数据存储工具接口
  153. */
  154. class
  155. DataStore
  156. :
  157. public
  158. Component
  159. {
  160. public
  161. :
  162. DataStore
  163. (
  164. DataStoreCallback
  165. *
  166. callback
  167. =
  168. NULL
  169. )
  170. :
  171. callback_
  172. (
  173. callback
  174. )
  175. {
  176. // Do nothing
  177. }
  178. virtual
  179. ~
  180. DataStore
  181. ()
  182. {
  183. // Do nothing
  184. }
  185. /**
  186.      * 初始化数据存取设备的方法,譬如去连接数据库、打开文件之类。
  187.      * @param config 配置对象
  188.      * @param callback 参数 callback 为基本回调对象,初始化的结果会回调其 OnInit() 函数通知用户。
  189.      * @return 基本的配置是否正确,返回 0 表示正常。
  190.      */
  191. virtual
  192. int
  193. Init
  194. (
  195. Config
  196. *
  197. config
  198. ,
  199. DataStoreCallback
  200. *
  201. callback
  202. );
  203. virtual
  204. int
  205. Init
  206. (
  207. Application
  208. *
  209. app
  210. ,
  211. Config
  212. *
  213. cfg
  214. ){
  215.     app_
  216. =
  217. app
  218. ;
  219. return
  220. Init
  221. (
  222. cfg
  223. ,
  224. callback_
  225. );
  226. }
  227. virtual
  228. std
  229. ::
  230. string
  231. GetName
  232. ()
  233. {
  234. return
  235. “den::DataStore”
  236. ;
  237. }
  238. virtual
  239. int
  240. Stop
  241. ()
  242. {
  243. Close
  244. ();
  245. return
  246. 0
  247. ;
  248. }
  249. /**
  250.      * 驱动存储接口程序运行,触发回调函数。
  251.      * @return 返回 0 表示此次没有进行任何操作,通知上层本次调用后可以 sleep 一下。
  252.      */
  253. virtual
  254. int
  255. Update
  256. ()
  257. =
  258. 0
  259. ;
  260. /**
  261.      * 关闭程序,关闭动作虽然是异步的,但不再返回结果,直接关闭就好。
  262.      */
  263. virtual
  264. void
  265. Close
  266. ()
  267. =
  268. 0
  269. ;
  270. /**
  271.      * 读取一个数据对象,通过 key ,把数据放入到 value,结果会回调通知 callback。
  272.      * 发起调用前,必须把 callback 的 value 字段设置为输出参数。
  273.      * @param key
  274.      * @param callback 如果NULL,则会回调从 Init() 中传入的 callback 对象。
  275.      * @return 0 表示发起请求成功,其他值为失败
  276.      */
  277. virtual
  278. int
  279. Get
  280. (
  281. const
  282. std
  283. ::
  284. string
  285. &
  286. key
  287. ,
  288. DataStoreCallback
  289. *
  290. callback
  291. =
  292. NULL
  293. )
  294. =
  295. 0
  296. ;
  297. /**
  298.      * 写入一个数据对象,写入 key ,value,写入结果会回调通知 callback。
  299.      * @param key
  300.      * @param value
  301.      * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。
  302.      * @return 表示发起请求成功,其他值为失败
  303.      */
  304. virtual
  305. int
  306. Put
  307. (
  308. const
  309. std
  310. ::
  311. string
  312. &
  313. key
  314. ,
  315. const
  316. Serializable
  317. &
  318. value
  319. ,
  320. DataStoreCallback
  321. *
  322. callback
  323. =
  324. NULL
  325. )
  326. =
  327. 0
  328. ;
  329. /**
  330.      * 删除一个数据对象,通过 key ,结果会回调通知 callback。
  331.      * @param key
  332.      * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。
  333.      * @return 表示发起请求成功,其他值为失败
  334.      */
  335. virtual
  336. int
  337. Remove
  338. (
  339. const
  340. std
  341. ::
  342. string
  343. &
  344. key
  345. ,
  346. DataStoreCallback
  347. *
  348. callback
  349. =
  350. NULL
  351. )
  352. =
  353. 0
  354. ;
  355. protected
  356. :
  357. /// 存放初始化传入的回调指针
  358. DataStoreCallback
  359. *
  360. callback_
  361. ;
  362. };

复制代码



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

文件存储

Redis

其他的各种数据库

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

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

原创文章,作者:棋牌游戏,如若转载,请注明出处:https://www.qp49.com/2019/04/11/1991.html

(0)
上一篇 2019年4月10日 上午10:37
下一篇 2019年4月19日 下午2:24

相关推荐

发表评论

邮箱地址不会被公开。 必填项已用*标注