游戏聊天功能如何设计
游戏聊天框
曾经被人问过一个这一的问题,如何开发设计一个游戏聊天框/公屏
当时因为在学习期间有开发过一个模仿QQ的即时通讯系统,很多的逻辑设计被限制在了聊天室的范畴中;
现在想想,一个小小的游戏聊天框,其实会衍生出很多很多需求及场景,本篇所想即所知,聊一聊如果使用JAVA语言,如何对游戏中的聊天消息系统进行开发;
背景
聊天动作非常简单,只需两个:收 和 发
在不同的游戏中,聊天的类型也各有不同;大体上是三种
- 最常见的是通讯式的点对点,点对多聊天
- 基于游戏公屏的世界大喇叭
- 系统消息
三者的收发群体各有不同,我与你,我与你们,我与指定人,系统指定人
不管哪种输入展示性质的对话类功能都逃不开这三者的收发,游戏聊天框更必须在本身业务复杂,线程安全的杂合中考虑其三。
设计方案
前面说到三种聊天类型,下面将针对三者从简单方案分析,再到涉及到的安全、并发、等问题;
流程
点对点
玩家A与玩家B的私聊模式,只需要通过连接玩家A与玩家B的各自唯一标识符,例如玩家ID或用户名。
这样建立聊天框,收发只需通过:
- 请求私聊,发送第一条消息的同时,生成一个只存在玩家A与玩家B的小型聊天室,聊天室ID,玩家AID,玩家BID,都是唯一
- 玩家A与玩家B各自订阅本身ID的消息队列
- 发送,消费消息,消息通过网络传输在目标玩家的客户端上消费或显示
点对多
聊天室的多收模式,与点对点不同的是,消息收发的唯一标识符变成了各点对多聊天室对象ID
于是收发就变成:
- 聊天室管理聊天对象,聊天对象发送消息,消息体中唯一标识符为该聊天室ID
- 玩家A订阅自身加入的所有聊天室的ID主题
- 发送,消费消息,消息通过网络传输在聊天室中所有在线玩家客户端上消费或显示
世界公屏
群发模式的消息,覆盖面为游戏中的一个服务器或某张地图,某块区域,因此发送的标识符会随着玩家的地理信息实时变化;
更加的是,除了发送者本身的唯一标识符变化,所有收发者的标识符也只会在发送者发送消息的那一刻进行快照型的记录;
因此玩家存储的标识符除了当前位置的,考虑到网络因素影响,一般还需要考虑上一条时间戳上的消息;
于是收发就变成:
- 发送者发送带有当前位置唯一的消息标识符消息
- 玩家A订阅当前位置的标识符队列
- 在某些游戏中,公屏消息需要有必达的特性,如果发生网络延时,或玩家不在线;等待重连/上线时,将公屏消息随着时间戳顺序发送。
- 玩家A客户端接收,进行消费或显示
系统信息
每个玩家都拥有一个唯一的标识,系统消息也只需要将消息打到对应的玩家上即可;
最终一个玩家,他需要订阅的消息体一般是四种:
- 自身的私聊消息队列
- 自身拥有的所有聊天室的消息队列
- 当前位置的消息队列
- 系统的消息队列
当然了,实际设计中,可以通过type,ids,进行队列的耦合,减少客户端的带宽压力
问题
有涉及过这类需求的小伙伴可能感受过,消息收发的本身并不复杂,很多时间与心思是在 聊天室
的各种并发病中;
历史记录
比如历史记录,在各种游戏中,聊天记录有两种,本地保存,云端服务器保存;
前者很简单,只需要本地客户端将所有的 聊天室
中的消息体进行记录,再一次打开时直接访问文件内存渲染即可;
后者则有很多需要考量的地方,主要是两个问题,怎么存信息 ,需要存多久信息
直接说结论,按照我目前的水平,我可能考虑到的方案如下:
将消息随时间分为3,6,9等:
- 3等消息,两年前的消息,因为再次访问的可能性低,因此将两年前的记录进行文件性的冷保存,直接写入.xxx文件中上传至oss;
- 6等消息,一年前的消息,考虑到再读取,以及即将变为3等消息需要进行文件冷保存,因此可直接保存至关系型数据库中;
- 9等消息,需要频繁读取的消息,可以使用高性能,高可读的内存存储结构,这里推荐
mongodb
这种直接读取内存的NOSQL型数据库
最后是什么时候划分,怎么划分?
要知道不管是什么游戏都有一个一周一次的维护时间,在那个时间里去将这些记录3,6,9等化,不是正好算维护的一种:)
发布订阅时并发问题
因为是消息收发,所以无论如何就避不开重复消息、重复发送的原则级问题;
在设计模式中,也正好有订阅模式这一对症下药的理念,而当前市场上的各类消息中间件几乎都是发布订阅型的消息处理器。
因此在对游戏规模、性能、成本等因素综合考虑,选择了适合项目的消息中间件后,就只需要考虑以下情况:
- 消息排序,在客户端中为每个玩家维护一个消息队列,将收到的消息按照顺序存储,并在合适的时机进行显示,这个可以使用已经成熟的有序消息队列插件直接实现;
- 竞态顺序,与消息排序类似,当聊天功能有着像抢红包/道具等等需要线性点击消费的需求时,需要考虑玩家A发hello,随后玩家B发word,最终的期望值一定需要控制在 "hello word" 的范畴中,最佳的方案可以采取
秒杀业务
的设计思路。 - 预防死状态,用户离线判断与发送消息,是两个互斥的条件;在一个聊天室的消息队列中,玩家A收到消息队列中的消息与向消息队列发送消息的状态一定需要一致,并且一定是在用户在线这把锁为true的前提下
- 消息的存续,当聊天室中,一条消息可以是简单的文本消息,也可以是一个文档的下载动作。因此所过存在消息撤回的动作,那么对于已经收到这些内容的人来说,只需要将撤回动作当作消息队列中的一条新消息,通过Type进行区分。就可以对简单的文本信息进行唯一标识符的删除,针对文件类型,则是通知下载中断型的删除,不过大部分下载功能,即使发送者已经撤回,下载中的人也不会暂停。
- 重复消费、重复发送、重发,这三个在收发场景中不可避免的问题,一般来说可以使用唯一标识、ACK机制、客户端确认的原理就可以实现。需要注意的是,因为聊天的时效性,对于接收方队列来说,发送者发送失败或有异常的消息,是要求重新发送还是直接丢弃,需要到具体业务具体讨论
- 安全性,一个聊天室中,发送人发送消息到订阅者收到这个链路上,双方的状态一定是一致且账号安全的。对于消息过滤以及路由转发,可以在消息队列中添加相应的逻辑进行处理。并且消息的授权机制,应该是在客户端以及服务端和另一个客户端上各做各人鉴权逻辑。
- 负载均衡,随着消息量的增大,一个消息队列肯定是无法满足多人的吞吐量,因此在主消息队列上,可以通过开启,子消息集以及平行队列直接从根本上增大消息的吞吐量,那么这里就会涉及到一个小型的负载均衡式分配消息的动作。
总结
因为是游戏聊天,感觉上一定是,也只能是基于消息组件式的订阅模型的设计。
像QQ,微信这种即时通讯系统,虽然没有了解过早期,但我猜测,多少和UDP、TCP沾边;像开发过的哪个模仿QQ的玩意,就是登录 = TCP连接,聊天 = UDP。
但游戏中因为消息量大,消费性的多样等等因素考虑,UDP这种邮箱发包的方式性能极低。所以在猜疑考虑的基础上,对游戏聊天框进行了本次的讨论