教你从头写游戏服务器框架(2)

作者:韩大wade 韩大 2019-02-22


本文为系列文章的第2篇。

前一篇:教你从头写游戏服务器框架(1)

对象序列化


现代编程技术中,面向对象是一个最常见的思想。因此不管是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.    
  10. virtual

  11. ssize_t

  12. SerializeTo
  13. (
  14. char
  15. *
  16. buffer
  17. ,

  18. int
  19. buffer_length
  20. )

  21. const

  22. =

  23. 0
  24. ;

  25.    
  26. /**
  27.      * @brief 从一个 buffer 中读取 length 个字节,反序列化到本对象。
  28.      *@return  返回 0 表示成功,其他值表示出错。
  29.      */
  30.    
  31. virtual

  32. int

  33. SerializeFrom
  34. (
  35. const

  36. char
  37. *
  38. buffer
  39. ,

  40. int
  41. length
  42. )

  43. =

  44. 0
  45. ;

  46.    
  47. virtual

  48. ~
  49. Serializable
  50. (){}

  51. };
复制代码


网络传输

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


  1. // ObjectProcessor 定义
  2. class

  3. ObjectProcessor

  4. :

  5. public

  6. ProcessorHelper

  7. {
  8. public
  9. :
  10.    
  11. ObjectProcessor
  12. ();
  13.    
  14. virtual

  15. ~
  16. ObjectProcessor
  17. ();

  18.    
  19. // 继承自 Processor 处理器函数
  20.    
  21. virtual

  22. int

  23. Init
  24. (
  25. Server
  26. *
  27. server
  28. ,

  29. Config
  30. *
  31. config
  32. =
  33. NULL
  34. );

  35.    
  36. // 继承自 Processor 的处理函数
  37.    
  38. virtual

  39. int

  40. Process
  41. (
  42. const

  43. Request
  44. &
  45. request
  46. ,

  47. const

  48. Peer
  49. &
  50. peer
  51. );

  52.    
  53. virtual

  54. int

  55. Process
  56. (
  57. const

  58. Request
  59. &
  60. request
  61. ,

  62. const

  63. Peer
  64. &
  65. peer
  66. ,

  67. Server
  68. *
  69. server
  70. );

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

  75. void
  76. set_default_handler
  77. (
  78. ObjectHandler
  79. *
  80. default_handler
  81. )

  82. {
  83.         default_handler_
  84. =
  85. default_handler
  86. ;
  87.    
  88. }

  89.    
  90. /**
  91.      * @brief 针对 service_name,注册对应处理的 handler ,注意 handler 本身是带对象类型信息的。
  92.      * @param service_name 服务名字,通过 Request.service 传输
  93.      * @param handler 请求的处理对象
  94.      */
  95.    
  96. void

  97. Register
  98. (
  99. const
  100. std
  101. ::
  102. string
  103. &
  104. service_name
  105. ,

  106. ObjectHandler
  107. *
  108. handler
  109. );

  110.    
  111. /**
  112.      * @brief 使用 handler 自己的 GetName() 返回值,注册服务。
  113.      * 如果 handler->GetName() 返回 "" 字符串,则会替换默认处理器对象
  114.      * @param handler 服务处理对象。
  115.      */
  116.    
  117. void

  118. Register
  119. (
  120. ObjectHandler
  121. *
  122. handler
  123. );

  124.    
  125. ///@brief 关闭此服务
  126.    
  127. virtual

  128. int

  129. Close
  130. ();

  131. private
  132. :
  133.     std
  134. ::
  135. map
  136. <
  137. std
  138. ::
  139. string
  140. ,

  141. ObjectHandler
  142. *>
  143. handler_table_
  144. ;
  145.    
  146. Config
  147. *
  148. config_
  149. ;
  150.    
  151. ObjectHandler
  152. *
  153. default_handler_
  154. ;

  155.    
  156. int

  157. ProcessBy
  158. (
  159. const

  160. Request
  161. &
  162. request
  163. ,

  164. const

  165. Peer
  166. &
  167. peer
  168. ,
  169.                   
  170. ObjectHandler
  171. *
  172. handler
  173. ,

  174. Server
  175. *
  176. server
  177. =
  178. NULL
  179. );

  180.    
  181. int

  182. DefaultProcess
  183. (
  184. const

  185. Request
  186. &
  187. request
  188. ,

  189. const

  190. Peer
  191. &
  192. peer
  193. ,

  194. Server
  195. *
  196. server
  197. =
  198. NULL
  199. );

  200.    
  201. bool

  202. InitHandler
  203. (
  204. ObjectHandler
  205. *
  206. handler
  207. );

  208. };

  209. // ObjectHandler 定义
  210. class

  211. ObjectHandler

  212. :

  213. public

  214. Serializable
  215. ,

  216. public

  217. Updateable

  218. {
  219. public
  220. :
  221.    
  222. ObjectHandler
  223. ();

  224.    
  225. virtual

  226. ~
  227. ObjectHandler
  228. ()

  229. ;

  230.    
  231. virtual

  232. void

  233. ProcessRequest
  234. (
  235. const

  236. Peer
  237. &
  238. peer
  239. );
  240.    
  241. virtual

  242. void

  243. ProcessRequest
  244. (
  245. const

  246. Peer
  247. &
  248. peer
  249. ,

  250. Server
  251. *
  252. server
  253. );


  254.    
  255. /**
  256.      * DenOS 用此方法确定进行服务注册,你应该覆盖此方法。
  257.      * 默认名字为空,会注册为“默认服务”,就是所有找不到对应名字服务的请求,都会转发给此对象处理。
  258.      * @return 注册此服务的名字。在 Request.service 字段中传输。
  259.      */
  260.    
  261. virtual
  262. std
  263. ::
  264. string
  265. GetName
  266. ()

  267. ;

  268.    
  269. int

  270. Reply
  271. (
  272. const

  273. char
  274. *
  275. buffer
  276. ,

  277. int
  278. length
  279. ,

  280. const

  281. Peer
  282. &
  283. peer
  284. ,
  285.               
  286. const
  287. std
  288. ::
  289. string
  290. &
  291. service_name
  292. =

  293. ""
  294. ,

  295. Server
  296. *
  297. server
  298. =
  299. NULL
  300. )

  301. ;

  302.    
  303. int

  304. Inform
  305. (
  306. char
  307. *
  308. buffer
  309. ,

  310. int
  311. length
  312. ,

  313. const
  314. std
  315. ::
  316. string
  317. &
  318. session_id
  319. ,
  320.                
  321. const
  322. std
  323. ::
  324. string
  325. &
  326. service_name
  327. ,

  328. Server
  329. *
  330. server
  331. =
  332. NULL
  333. );

  334.    
  335. int

  336. Inform
  337. (
  338. char
  339. *
  340. buffer
  341. ,

  342. int
  343. length
  344. ,

  345. const

  346. Peer
  347. &
  348. peer
  349. ,
  350.                
  351. const
  352. std
  353. ::
  354. string
  355. &
  356. service_name
  357. =

  358. ""
  359. ,

  360. Server
  361. *
  362. server
  363. =
  364. NULL
  365. );

  366.    
  367. virtual

  368. int

  369. Init
  370. (
  371. Server
  372. *
  373. server
  374. ,

  375. Config
  376. *
  377. config
  378. );

  379.    
  380. /**
  381.      * 如果需要在主循环中进行操作,可以实现此方法。
  382.      * 返回值小于 0 的话,此任务会被移除循环
  383.      */
  384.    
  385. virtual

  386. int

  387. Update
  388. ()

  389. {
  390.         
  391. return

  392. 0
  393. ;
  394.    
  395. }

  396. protected
  397. :
  398.    
  399. Server
  400. *
  401. server_
  402. ;
  403.     std
  404. ::
  405. map
  406. <
  407. int
  408. ,

  409. MessageHeader
  410. >
  411. header_map_
  412. ;
  413.    
  414. Response
  415. response_
  416. ;
  417.    
  418. Notice
  419. notice_
  420. ;
  421. };
复制代码


由于我们对于可序列化的对象,要求一定要实现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.    
  25. ObjectHandlerCast
  26. ()
  27.             
  28. :
  29. req_obj_
  30. (
  31. NULL
  32. ),
  33.               res_obj_
  34. (
  35. NULL
  36. ),
  37.               buffer_
  38. (
  39. NULL
  40. )

  41. {
  42.         buffer_
  43. =

  44. new

  45. char
  46. [
  47. Message
  48. ::
  49. MAX_MAESSAGE_LENGTH
  50. ];
  51.         bzero
  52. (
  53. buffer_
  54. ,

  55. Message
  56. ::
  57. MAX_MAESSAGE_LENGTH
  58. );
  59.    
  60. }

  61.    
  62. virtual

  63. ~
  64. ObjectHandlerCast
  65. ()

  66. {
  67.         
  68. delete
  69. []
  70. buffer_
  71. ;
  72.    
  73. }

  74.    
  75. /**
  76.      * 对于不想使用 obj_ 成员来实现 Serializable 接口的,可以实现此接口
  77.      */
  78.    
  79. virtual

  80. int

  81. Pack
  82. (
  83. char
  84. *
  85. buffer
  86. ,

  87. int
  88. length
  89. ,

  90. const
  91. RES
  92. &
  93. object
  94. )

  95. const

  96. {
  97.         
  98. return

  99. -
  100. 1
  101. ;
  102.    
  103. }

  104.    
  105. virtual

  106. int

  107. Unpack
  108. (
  109. const

  110. char
  111. *
  112. buffer
  113. ,

  114. int
  115. length
  116. ,
  117. REQ
  118. *
  119. object
  120. )

  121. {
  122.         
  123. return

  124. -
  125. 1
  126. ;
  127.    
  128. }

  129.    
  130. int

  131. ReplyObject
  132. (
  133. const
  134. RES
  135. &
  136. object
  137. ,

  138. const

  139. Peer
  140. &
  141. peer
  142. ,
  143.                     
  144. const
  145. std
  146. ::
  147. string
  148. &
  149. service_name
  150. =

  151. ""
  152. )

  153. {

  154.         res_obj_
  155. =

  156. &
  157. object
  158. ;
  159.         
  160. int
  161. len
  162. =

  163. SerializeTo
  164. (
  165. buffer_
  166. ,

  167. Message
  168. ::
  169. MAX_MAESSAGE_LENGTH
  170. );
  171.         
  172. if

  173. (
  174. len
  175. <

  176. 0
  177. )
  178.             
  179. return

  180. -
  181. 1
  182. ;
  183.         
  184. return

  185. Reply
  186. (
  187. buffer_
  188. ,
  189. len
  190. ,
  191. peer
  192. ,
  193. service_name
  194. );
  195.    
  196. }

  197.    
  198. int

  199. InformObject
  200. (
  201. const
  202. std
  203. ::
  204. string
  205. &
  206. session_id
  207. ,

  208. const
  209. RES
  210. &
  211. object
  212. ,
  213.                      
  214. const
  215. std
  216. ::
  217. string
  218. &
  219. service_name
  220. )

  221. {

  222.         res_obj_
  223. =

  224. &
  225. object
  226. ;
  227.         
  228. int
  229. len
  230. =

  231. SerializeTo
  232. (
  233. buffer_
  234. ,

  235. Message
  236. ::
  237. MAX_MAESSAGE_LENGTH
  238. );
  239.         
  240. if

  241. (
  242. len
  243. <

  244. 0
  245. )
  246.             
  247. return

  248. -
  249. 1
  250. ;
  251.         
  252. return

  253. Inform
  254. (
  255. buffer_
  256. ,
  257. len
  258. ,
  259. session_id
  260. ,
  261. service_name
  262. );
  263.    
  264. }

  265.    
  266. virtual

  267. void

  268. ProcessRequest
  269. (
  270. const

  271. Peer
  272. &
  273. peer
  274. ,

  275. Server
  276. *
  277. server
  278. )

  279. {
  280.         REQ
  281. *
  282. obj
  283. =
  284. req_obj_
  285. ;
  286.         
  287. ProcessObject
  288. (*
  289. obj
  290. ,
  291. peer
  292. ,
  293. server
  294. );
  295.         
  296. delete
  297. obj
  298. ;
  299.         req_obj_
  300. =
  301. NULL
  302. ;
  303.    
  304. }

  305.    
  306. virtual

  307. void

  308. ProcessObject
  309. (
  310. const
  311. REQ
  312. &
  313. object
  314. ,

  315. const

  316. Peer
  317. &
  318. peer
  319. )

  320. {
  321.         ERROR_LOG
  322. (
  323. "This object have no process handler."
  324. );
  325.    
  326. }

  327.    
  328. virtual

  329. void

  330. ProcessObject
  331. (
  332. const
  333. REQ
  334. &
  335. object
  336. ,

  337. const

  338. Peer
  339. &
  340. peer
  341. ,
  342.                               
  343. Server
  344. *
  345. server
  346. )

  347. {
  348.         
  349. ProcessObject
  350. (
  351. object
  352. ,
  353. peer
  354. );
  355.    
  356. }

  357. protected
  358. :
  359.     REQ
  360. *
  361. req_obj_
  362. ;
  363.    
  364. const
  365. RES
  366. *
  367. res_obj_
  368. ;

  369. private
  370. :
  371.    
  372. char
  373. *
  374. buffer_
  375. ;

  376.    
  377. virtual

  378. ssize_t

  379. SerializeTo
  380. (
  381. char
  382. *
  383. buffer
  384. ,

  385. int
  386. buffer_length
  387. )

  388. const

  389. {
  390.         
  391. ssize_t
  392. ret
  393. =

  394. 0
  395. ;
  396.         ret
  397. =

  398. Pack
  399. (
  400. buffer
  401. ,
  402. buffer_length
  403. ,

  404. *
  405. res_obj_
  406. );
  407.         
  408. return
  409. ret
  410. ;
  411.    
  412. }

  413.    
  414. virtual

  415. int

  416. SerializeFrom
  417. (
  418. const

  419. char
  420. *
  421. buffer
  422. ,

  423. int
  424. length
  425. )

  426. {
  427.         req_obj_
  428. =

  429. new
  430. REQ
  431. ();
  432.   
  433. // 新建一个对象,为了协程中不被别的协程篡改
  434.         
  435. int
  436. ret
  437. =

  438. Unpack
  439. (
  440. buffer
  441. ,
  442. length
  443. ,
  444. req_obj_
  445. );
  446.         
  447. if

  448. (
  449. ret
  450. )

  451. {
  452.             
  453. delete
  454. req_obj_
  455. ;
  456.             req_obj_
  457. =
  458. NULL
  459. ;
  460.         
  461. }
  462.         
  463. return
  464. ret
  465. ;
  466.    
  467. }
  468. };
复制代码


任何类型的对象,如果想要在这个框架中以网络收发,只要为他写一个模板,完成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.    
  10. DataMap
  11. ();
  12.    
  13. virtual

  14. ~
  15. DataMap
  16. ();

  17.    
  18. /**
  19.      * @brief 对于可能阻塞的异步操作,需要调用这个接口来驱动回调。
  20.      */
  21.    
  22. virtual

  23. int

  24. Update
  25. (){

  26. return

  27. 0
  28. ;

  29. }

  30.    
  31. /**
  32.      * @brief 获取 key 对应的数据。
  33.      * @param key 数据的 Key
  34.      * @param value_buf 是输出缓冲区指针
  35.      * @param value_buf_len 是缓冲区最大长度
  36.      * @return 返回 -1 表示找不到这个 key,返回 -2 表示 value_buf_len 太小,不足以读出数据。其他负数表示错误,返回 >= 0 的值表示 value 的长度
  37.      */
  38.    
  39. virtual

  40. int

  41. Get
  42. (
  43. const
  44. std
  45. ::
  46. string
  47. &
  48. key
  49. ,

  50. char
  51. *
  52. value_buf
  53. ,

  54. int
  55. value_buf_len
  56. )

  57. =

  58. 0
  59. ;
  60.    
  61. virtual

  62. int

  63. Get
  64. (
  65. const
  66. std
  67. ::
  68. string
  69. &
  70. key
  71. ,

  72. Serializable
  73. *
  74. value
  75. );

  76.    
  77. /**
  78.      * @brief 异步 Get 的接口
  79.      * @param key 获取数据的 Key
  80.      * @param callback 获取数据的回调对象,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL
  81.      * @return 返回 0 表示发起查询成功,其他值表示错误。
  82.      */
  83.    
  84. virtual

  85. int

  86. Get
  87. (
  88. const
  89. std
  90. ::
  91. string
  92. &
  93. key
  94. ,

  95. DataMapCallback
  96. *
  97. callback
  98. )

  99. =

  100. 0
  101. ;

  102.    
  103. /**
  104.      * @brief 覆盖、写入key对应的缓冲数据。
  105.      * @return 成功返回0。 返回 -1 表示value数据太大,其他负数表示其他错误
  106.      */
  107.    
  108. virtual

  109. int

  110. Put
  111. (
  112. const
  113. std
  114. ::
  115. string
  116. &
  117. key
  118. ,

  119. const

  120. char
  121. *
  122. value_buf
  123. ,

  124. int
  125. value_buf_len
  126. )

  127. =

  128. 0
  129. ;
  130.    
  131. virtual

  132. int

  133. Put
  134. (
  135. const
  136. std
  137. ::
  138. string
  139. &
  140. key
  141. ,

  142. const

  143. Serializable
  144. &
  145. value
  146. );

  147.    
  148. /**
  149.      * 写入数据的异步接口,使用 callback 来通知写入结果
  150.      * @param key 数据 key
  151.      * @param value 数据 Value
  152.      * @param callback 写入结果会调用此对象的 PutResult() 方法
  153.      * @return 返回 0 表示准备操作成功
  154.      */
  155.    
  156. virtual

  157. int

  158. Put
  159. (
  160. const
  161. std
  162. ::
  163. string
  164. &
  165. key
  166. ,

  167. const

  168. Serializable
  169. &
  170. value
  171. ,

  172. DataMapCallback
  173. *
  174. callback
  175. );
  176.    
  177. virtual

  178. int

  179. Put
  180. (
  181. const
  182. std
  183. ::
  184. string
  185. &
  186. key
  187. ,

  188. const

  189. char
  190. *
  191. value_buf
  192. ,

  193. int
  194. value_buf_len
  195. ,

  196. DataMapCallback
  197. *
  198. callback
  199. );

  200.    
  201. /**
  202.      * 删除 key 对应的数据
  203.      * @return 返回 0 表示成功删除,返回 -1 表示这个 key 本身就不存在,其他负数返回值表示其他错误。
  204.      */
  205.    
  206. virtual

  207. int

  208. Remove
  209. (
  210. const
  211. std
  212. ::
  213. string
  214. &
  215. key
  216. )

  217. =

  218. 0
  219. ;
  220.    
  221. virtual

  222. int

  223. Remove
  224. (
  225. const
  226. std
  227. ::
  228. string
  229. &
  230. key
  231. ,

  232. DataMapCallback
  233. *
  234. callback
  235. );

  236.    
  237. /**
  238.      * 是否有 Key 对应的数据
  239.      *@return  返回 key 值表示找到,0 表示找不到
  240.      */
  241.    
  242. virtual

  243. int

  244. ContainsKey
  245. (
  246. const
  247. std
  248. ::
  249. string
  250. &
  251. key
  252. )

  253. =

  254. 0
  255. ;

  256.    
  257. /**
  258.      * 异步 ContainsKey 接口,如果 key 不存在, FetchData() 参数 value_buf 会为 NULL。
  259.      * 如果 key 存在,value_buf 则不为NULL,但也不保证指向任何可用数据。可能是目标数值,
  260.      * 也可能内部的某个空缓冲区。如果是在本地的数据,就会是目标数据,如果是远程的数据,
  261.      * 为了减少性能就不会传入具体的 value 数值。
  262.      */
  263.    
  264. virtual

  265. int

  266. ContainsKey
  267. (
  268. const
  269. std
  270. ::
  271. string
  272. &
  273. key
  274. ,

  275. DataMapCallback
  276. *
  277. callback
  278. )

  279. =

  280. 0
  281. ;

  282.    
  283. /**
  284.      * 遍历整个缓存。
  285.      * @param callback 每条记录都会调用 callback 对象的 FetchData() 方法
  286.      * @return 返回 0 表示成功,其他表示错误。
  287.      */
  288.    
  289. virtual

  290. int

  291. GetAll
  292. (
  293. DataMapCallback
  294. *
  295. callback
  296. )

  297. =

  298. 0
  299. ;

  300.    
  301. /**
  302.      * 获取整个缓存的数据
  303.      * @param result 结果会放在这个 map 里面,记得每条记录中的 Bytes 的 buffer_ptr 需要 delete[]
  304.      * @return 返回 0 表示成功,其他表示错误
  305.      */
  306.    
  307. virtual

  308. int

  309. GetAll
  310. (
  311. std
  312. ::
  313. map
  314. <
  315. std
  316. ::
  317. string
  318. ,

  319. Bytes
  320. >*
  321. result
  322. );
  323. private
  324. :
  325.    
  326. char
  327. *
  328. tmp_buffer_
  329. ;


  330. };
复制代码


这个接口其实只是一个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.    
  8. TypeLocal
  9. ,

  10. ///< 本地
  11.    
  12. TypeName
  13. ,
  14.   
  15. ///< 按 Cache 名字分布
  16.    
  17. TypeKey
  18. ,
  19.    
  20. ///< 先按 Cache 名字分布,再按数据的 key 分布
  21.    
  22. TypeCopy
  23.    
  24. ///< 复制型分布
  25. };

  26. /**
  27. * @brief 定义了分布式缓存对象的基本结构
  28. */
  29. class

  30. Cache

  31. :

  32. public

  33. DataMap

  34. {
  35. public
  36. :

  37.    
  38. /**
  39.      * @brief 连接集群。
  40.      * @return 返回是否连接成功。
  41.      */
  42.    
  43. static

  44. bool

  45. EnsureCluster
  46. (
  47. Config
  48. *
  49. config
  50. =
  51. NULL
  52. );

  53.    
  54. /**
  55.      * 获得一个 Cache 对象。
  56.      * @attention 如果第一次调用此函数时,输入的 type, local_data_map 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。
  57.      * @param name 为名字
  58.      * @param type 此缓存希望是哪种类型
  59.      * @param local_data_map 为本地存放的数据容器。
  60.      * @return 如果是 NULL 表示已经达到 Cache 数量的上限。
  61.      */
  62.    
  63. static

  64. Cache
  65. *

  66. GetCache
  67. (
  68. const
  69. std
  70. ::
  71. string
  72. &
  73. name
  74. ,

  75. CacheType
  76. type
  77. ,
  78.                            
  79. DataMap
  80. *
  81. local_data_map
  82. );
  83.    
  84. /**
  85.      * 获得一个 Cache 对象。
  86.      * @param  name 为名字
  87.      * @param local_data_map 为本地存放的数据容器。
  88.      * @note 如果第一次调用此函数时,输入的 type, class M 会成为这个 Cache 的固定属性,以后只要是 name 对的上,都会是同一个 Cache 对象。
  89.      * @return 如果是 NULL 表示已经达到 Cache 数量的上限。
  90.      */
  91.    
  92. template
  93. <
  94. class
  95. M
  96. >
  97.    
  98. static

  99. Cache
  100. *

  101. GetCache
  102. (
  103. const
  104. std
  105. ::
  106. string
  107. &
  108. name
  109. ,

  110. CacheType
  111. type
  112. ,
  113.                            
  114. int
  115. *
  116. data_map_arg1
  117. =
  118. NULL
  119. )

  120. {
  121.         
  122. DataMap
  123. *
  124. local_data_map
  125. =
  126. NULL
  127. ;
  128.         std
  129. ::
  130. map
  131. <
  132. std
  133. ::
  134. string
  135. ,

  136. DataMap
  137. *>::
  138. iterator
  139. it
  140. =
  141. cache_store_
  142. .
  143. find
  144. (
  145. name
  146. );
  147.         
  148. if

  149. (
  150. it
  151. !=
  152. cache_store_
  153. .
  154. end
  155. ())

  156. {
  157.             local_data_map
  158. =
  159. it
  160. ->
  161. second
  162. ;
  163.         
  164. }

  165. else

  166. {
  167.             
  168. if

  169. (
  170. data_map_arg1
  171. !=
  172. NULL
  173. )

  174. {
  175.                 local_data_map
  176. =

  177. new
  178. M
  179. (*
  180. data_map_arg1
  181. );
  182.             
  183. }

  184. else

  185. {
  186.                 local_data_map
  187. =

  188. new
  189. M
  190. ();
  191.             
  192. }
  193.             cache_store_
  194. [
  195. name
  196. ]

  197. =
  198. local_data_map
  199. ;
  200.         
  201. }
  202.         
  203. return

  204. GetCache
  205. (
  206. name
  207. ,
  208. type
  209. ,
  210. local_data_map
  211. );
  212.    
  213. }

  214.    
  215. /**
  216.      * 删除掉本进程内存放的 Cache
  217.      */
  218.    
  219. static

  220. void

  221. RemoveCache
  222. (
  223. const
  224. std
  225. ::
  226. string
  227. &
  228. name
  229. );

  230.    
  231. explicit

  232. Cache
  233. (
  234. const
  235. std
  236. ::
  237. string
  238. &
  239. name
  240. );
  241.    
  242. virtual

  243. ~
  244. Cache
  245. ();
  246.    
  247. virtual
  248. std
  249. ::
  250. string
  251. GetName
  252. ()

  253. const
  254. ;

  255. protected
  256. :
  257.    
  258. static
  259. std
  260. ::
  261. map
  262. <
  263. std
  264. ::
  265. string
  266. ,

  267. Cache
  268. *>
  269. cache_map_
  270. ;
  271.     std
  272. ::
  273. string name_
  274. ;

  275. private
  276. :
  277.    
  278. static

  279. int
  280. MAX_CACHE_NUM
  281. ;
  282.    
  283. static
  284. std
  285. ::
  286. map
  287. <
  288. std
  289. ::
  290. string
  291. ,

  292. DataMap
  293. *>
  294. cache_store_
  295. ;

  296. };
复制代码


持久化



长久以来,互联网的应用会使用类似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.    
  10. static

  11. int
  12. MAX_HANG_UP_CALLBACK_NUM
  13. ;
  14.   
  15. // 最大回调挂起数
  16.     std
  17. ::
  18. string key
  19. ;
  20.    
  21. Serializable
  22. *
  23. value
  24. ;

  25.    
  26. DataStoreCallback
  27. ();
  28.    
  29. virtual

  30. ~
  31. DataStoreCallback
  32. ();

  33.    
  34. /**
  35.      * 当 Init() 初始化结束时会被调用。
  36.      * @param  result 是初始化的结果。0 表示初始化成功。
  37.      * @param  msg 是初始化可能存在的错误信息。可能是空字符串 ""。
  38.      */
  39.    
  40. virtual

  41. void

  42. OnInit
  43. (
  44. int
  45. result
  46. ,

  47. const
  48. std
  49. ::
  50. string
  51. &
  52. msg
  53. );

  54.    
  55. /**
  56.      * 当调用 Get() 获得结果会被调用
  57.      * @param key
  58.      * @param value
  59.      * @param result 是 0 表示能获得对象,否则 value 中的数据可能是没有被修改的。
  60.      */
  61.    
  62. virtual

  63. void

  64. OnGot
  65. (
  66. const
  67. std
  68. ::
  69. string
  70. &
  71. key
  72. ,

  73. Serializable
  74. *
  75. value
  76. ,

  77. int
  78. result
  79. );

  80.    
  81. /**
  82.      * 当调用 Put() 获得结果会被调用。
  83.      * @param key
  84.      * @param result 如果 result 是 0 表示写入成功,其他值表示失败。
  85.      */
  86.    
  87. virtual

  88. void

  89. OnPut
  90. (
  91. const
  92. std
  93. ::
  94. string
  95. &
  96. key
  97. ,

  98. int
  99. result
  100. );

  101.    
  102. /**
  103.      * 当调用 Remove() 获得结果会被调用
  104.      * @param key
  105.      * @param result 返回 0 表示删除成功,其他值表示失败,如这个 key 代表的对象并不存在。
  106.      */
  107.    
  108. virtual

  109. void

  110. OnRemove
  111. (
  112. const
  113. std
  114. ::
  115. string
  116. &
  117. key
  118. ,

  119. int
  120. result
  121. );

  122.    
  123. /**
  124.      * 准备在发起用户设置的回调,如果使用者没有单独为一个回调事务设置单独的回调对象。
  125.      * 在 PrepareRegisterCallback() 中会生成一个临时对象,在此处会被串点参数并清理。
  126.      * @param privdata 由底层回调机制所携带的回调标识参数,如果为 NULL 则返回 NULL。
  127.      * @param init_cb 初始化时传入的共用回调对象
  128.      * @return 可以发起回调的对象,如果为 NULL 表示参数 privdata 是 NULL
  129.      */
  130.    
  131. static

  132. DataStoreCallback
  133. *

  134. PrepareUseCallback
  135. (
  136. void
  137. *
  138. privdata
  139. ,
  140.                                                 
  141. DataStoreCallback
  142. *
  143. init_cb
  144. );

  145.    
  146. /**
  147.      * 检查和登记回调对象,以防内存泄漏。
  148.      * @param callback 可以是NULL,会新建一个仅仅用于存放key数据的临时callback对象。
  149.      *  @return 返回一个callback对象,如果是 NULL 表示有太多的回调过程未被释放。
  150.      */
  151.    
  152. static

  153. DataStoreCallback
  154. *

  155. PrepareRegisterCallback
  156. (
  157.             
  158. DataStoreCallback
  159. *
  160. callback
  161. );

  162. protected
  163. :
  164.    
  165. static

  166. int
  167. kHandupCallbacks
  168. ;
  169.   
  170. /// 有多少个回调指针被挂起,达到上限后会停止工作

  171. };

  172. /**
  173. *@brief 用来定义可以持久化对象的数据存储工具接口
  174. */
  175. class

  176. DataStore

  177. :

  178. public

  179. Component

  180. {
  181. public
  182. :
  183.    
  184. DataStore
  185. (
  186. DataStoreCallback
  187. *
  188. callback
  189. =
  190. NULL
  191. )
  192.             
  193. :
  194. callback_
  195. (
  196. callback
  197. )

  198. {
  199.         
  200. // Do nothing
  201.    
  202. }
  203.    
  204. virtual

  205. ~
  206. DataStore
  207. ()

  208. {
  209.         
  210. // Do nothing
  211.    
  212. }

  213.    
  214. /**
  215.      * 初始化数据存取设备的方法,譬如去连接数据库、打开文件之类。
  216.      * @param config 配置对象
  217.      * @param callback 参数 callback 为基本回调对象,初始化的结果会回调其 OnInit() 函数通知用户。
  218.      * @return 基本的配置是否正确,返回 0 表示正常。
  219.      */
  220.    
  221. virtual

  222. int

  223. Init
  224. (
  225. Config
  226. *
  227. config
  228. ,

  229. DataStoreCallback
  230. *
  231. callback
  232. );

  233.    
  234. virtual

  235. int

  236. Init
  237. (
  238. Application
  239. *
  240. app
  241. ,

  242. Config
  243. *
  244. cfg
  245. ){
  246.     app_
  247. =
  248. app
  249. ;
  250.    
  251. return

  252. Init
  253. (
  254. cfg
  255. ,
  256. callback_
  257. );
  258.    
  259. }

  260.    
  261. virtual
  262. std
  263. ::
  264. string
  265. GetName
  266. ()

  267. {
  268.    
  269. return

  270. "den::DataStore"
  271. ;
  272.    
  273. }

  274.    
  275. virtual

  276. int

  277. Stop
  278. ()

  279. {
  280.    
  281. Close
  282. ();
  283.    
  284. return

  285. 0
  286. ;
  287.    
  288.    
  289. }

  290.    
  291. /**
  292.      * 驱动存储接口程序运行,触发回调函数。
  293.      * @return 返回 0 表示此次没有进行任何操作,通知上层本次调用后可以 sleep 一下。
  294.      */
  295.    
  296. virtual

  297. int

  298. Update
  299. ()

  300. =

  301. 0
  302. ;

  303.    
  304. /**
  305.      * 关闭程序,关闭动作虽然是异步的,但不再返回结果,直接关闭就好。
  306.      */
  307.    
  308. virtual

  309. void

  310. Close
  311. ()

  312. =

  313. 0
  314. ;

  315.    
  316. /**
  317.      * 读取一个数据对象,通过 key ,把数据放入到 value,结果会回调通知 callback。
  318.      * 发起调用前,必须把 callback 的 value 字段设置为输出参数。
  319.      * @param key
  320.      * @param callback 如果NULL,则会回调从 Init() 中传入的 callback 对象。
  321.      * @return 0 表示发起请求成功,其他值为失败
  322.      */
  323.    
  324. virtual

  325. int

  326. Get
  327. (
  328. const
  329. std
  330. ::
  331. string
  332. &
  333. key
  334. ,

  335. DataStoreCallback
  336. *
  337. callback
  338. =
  339. NULL
  340. )

  341. =

  342. 0
  343. ;

  344.    
  345. /**
  346.      * 写入一个数据对象,写入 key ,value,写入结果会回调通知 callback。
  347.      * @param key
  348.      * @param value
  349.      * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。
  350.      * @return 表示发起请求成功,其他值为失败
  351.      */
  352.    
  353. virtual

  354. int

  355. Put
  356. (
  357. const
  358. std
  359. ::
  360. string
  361. &
  362. key
  363. ,

  364. const

  365. Serializable
  366. &
  367. value
  368. ,
  369.                     
  370. DataStoreCallback
  371. *
  372. callback
  373. =
  374. NULL
  375. )

  376. =

  377. 0
  378. ;

  379.    
  380. /**
  381.      * 删除一个数据对象,通过 key ,结果会回调通知 callback。
  382.      * @param key
  383.      * @param callback 如果是 NULL,则会回调从 Init() 中传入的 callback 对象。
  384.      * @return 表示发起请求成功,其他
最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多