背景
笔者一直以来做的都是普通的CRUD业务。近期产品经理突然奇想,想要在我们当前的产品中整合一个答题对战小游戏。对于常年只会CRUD的我还是提出了一些挑战。时至今日,项目已经接近开发完毕,至此总结下项目中的一些收获。
本文主要是总结我在做这个项目中的实战经验,所以各个模块均从业务面出发,毕竟脱离了业务的技术设计都是瞎扯淡。如果你想在这里找到能够拿来就用的代码,显然是没有的。并且我默认大家都已经知道了基础知识。不过我在文末补充了一些我在做项目过程中参考的不错的文章和博客,欢迎取阅。
通讯模式选择
这个答题对战小游戏姑且可以算作是一个网络游戏。在一个网络游戏中,一个很重要的点就是保持服务端与客户端、客户端与客户端之间的同步。游戏业界已经有了两个常用的同步模型:帧同步和状态同步。
帧同步
我们都很熟悉的王者荣耀使用的就是帧同步。简单来说,服务端规定了一个时序,在一个时序内,所有的客户端必须把自己这个时序内发送的动作都传递到服务端,服务端在收到所有客户端的动作之后,再把这些动作转发到所有当前战局内的客户端。客户端根据接收到的动作信号,在本地进行重现。
可以看得出,帧同步有着非常完整的时间轴控制,在客户端数量比较少的情况下,相互之间需要发送的消息数据量很小(只需要传递每一个客户端在这个时序内的动作)。
状态同步
状态同步就是客户端发送操作到服务端,服务端进行计算,并把结果传递给其他客户端。由于时序控制不严格,所以比较适合回合制游戏。
总结
帧同步和状态同步的实现策略不同,导致他们分别适合于不同的游戏类型,具体本文不深究。我们要做答题对战小游戏,属于回合制游戏,所以选择了状态同步。
通讯方式实现
无论采用哪种后端设计方案,我们都需要考虑一个问题,如何通过服务端向客户端发送消息。我之前很熟悉的HTTP接口,请求只能客户端发起,服务端只能对客户端的请求做响应。所以这时候必须得引入WebSocket了。
在Java里,常用的WebSocket框架有Spring和Netty,考虑到我对Spring相对更了解一些,所以采用了Spring。
Spring提供了两种不同的方案来使用WebSocket,有一种是STOMP,是一种特定消息协议,使用起来很清爽;另一种是原生WebSocket,虽然使用起来复杂一点,但是更加灵活。我选择了后者,因为他跟我们的小程序前端对接起来较为方便一些。
虽然WebSocket接入起来可能比较复杂,但是实际上它提供的功能就那么几个:
- 连接建立
- 消息监听
- 消息发送
也就是说,无论业务是怎样的,我们都需要把他们归纳为以上三个功能。
鉴权
对于网络上任何请求我们都需要做鉴权。WebSocket在建立连接之前,有个通过HTTP请求握手的过程。在Spring中,我们可以通过设置org.springframework.web.socket.server.HandshakeInterceptor
来对握手请求进行拦截。
所以我们可以在建立连接请求时,带上鉴权参数,比如生成的Token或者是用户的账号密码。我们还可以在鉴权的时候做一些其他的事情,比如检查当前已经建立的连接数量,控制服务器连接数量,以免耗尽了公网带宽。
由于这个环节时HTTP请求,所以可以响应特定的HTTP状态码给前端。
接收消息
对消息的监听就比较简单了,org.springframework.web.socket.WebSocketHandler#handleMessage
提供了消息监听的方法。我们可以在消息中带上对应的action动作,根据不同的action调用不同的业务方法即可。
消息发送
在Spring的原生WebSocket使用中,如果想要给客户端发消息,必须使用org.springframework.web.socket.WebSocketSession
。这本身问题不大,但是在分布式环境下就麻烦了。
比如我有两台服务器,服务器1接收到了客户端1的消息,然后做了响应处理,需要把消息转发到客户端2,但是客户端2当前与服务器2保持着连接,那么服务器1怎么直到客户端2于哪台服务器保持连接?即使知道了时服务器2,服务器1又怎样把消息转发到服务器2呢?
分布式环境下的WebSocket集群方案
现在随便一个业务都需要考虑在分布式环境下的使用和实现方案,单点永远都是不可靠的。在HTTP服务器上,我们可以将HTTP Session持久化到Redis中来实现分布式Session,或者是完全抛弃Session,采用Token的解决方案。但是WebSocket的Session并不能保存到Redis。这很好理解,WebSocket的连接其实是有状态的,客户端于服务端是建立的长连接,所以即使我把session持久化了,也不可能在另一台服务器上获取了这个session就可以向对应的客户端发送消息。
那么到底应该如何解决呢?
最简单的方法就是利用广播消息,服务器发现需要给客户端发送消息的时候,直接发送一个广播消息,所有的服务器都可以接收这个广播消息。当服务器收到此消息时,检查要推送的客户端是否于自己保持连接,如果未保持,直接丢弃,如果保持着,就推送消息给相应的客户端。广播消息的实现也很简单,可以直接使用MQ,目前主流的MQ都提供了广播消息的功能,我使用的是RocketMQ。也可以使用Redis的订阅发布功能,原理其实是一样的。
当然,广播消息显然会比较浪费性能。更加高逼格的策略是使用Hash路由,让具有一些特征的客户端连接到指定的服务端,服务端需要发消息的时候,只需要根据路由表找到与这个客户端保持连接的服务端即可。但是这样的实现需要运维的支撑和配合,如果请求量不大或者是自己的服务器节点本身就不多,使用广播消息完全就够了。
扩展阅读
学习WebSocket基础知识:WebSocket教程-阮一峰
如果在Spring项目中增加WebSocket实现:springmvc+websocket的全部实现方式
分布式WebSocket实现:分布式WebSocket集群解决方案
了解一些游戏后端设计思路:Play Cards: 探索通用的游戏后端方案
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。