在确定了数据结构后,原本是打算直接开始写斗地主的游戏运行逻辑的。但是突然想到我本地写出来之后,也测试不了啊,所以还是先写通信模块了。
基本框架
在Java语言中搞网络通信,那么就得请出Netty这个老演员了。
主要分为两个端:服务器、客户端
其中,搭建的代码都是老八股代码了,这里就不展开具体的代码了,有兴趣的可以到Github上看看。
Github代码地址:
服务器:GameServer.java
客户端:GameClient.java
协议选型
搭建好服务器以及客户端后,就要往建立连接的Netty流水线中添加处理器了。
要首先添加的处理器那就是网络协议了,一般来说有以下几种选择:
- HTTP:请求-响应模型
请求-响应模型,需要等待响应,实时性低。
- WebSocket:全双工
无需二次开发,开箱即用。
- 私有协议:想咋弄咋弄,与WebSocket相似
自由度高,但需要自己处理数据的序列化以及安全问题。
最终还是选择了私有协议,因为在最初的构想中,整个斗地主的通信会有两种模式并存。登录以及加入游戏房间等操作时使用阻塞的请求响应模型、游戏过程中则是使用事件驱动的形式。单纯的HTTP以及WebSocket都不能满足我设想的。
明面上选择的理由是不使用Http的请求-响应模型以及需要使用自定义协议用于高性能的游戏通信。(斗地主也要高性能!)
私有协议
选择私有协议后,下一步要做的事情就是制定数据包的格式了。
数据包格式
数据包的前三个字节,这里参照了文件中的魔数(例如Java中class文件的java魔数),数据包的前三个字节就使用了FTL(FightTheLandlord)来做为魔数。
因为在TCP协议传输数据的过程中,会出现粘包、拆包的问题(老八股了)。所以设置完魔数后,之后的四个字节设置成本次数据包中数据字节的长度。紧接着的数据就是序列化后的事件字节了。
Netty编解码器:Codec.java
粘包、拆包处理器:FrameDecoder.java
序列化事件
咱们都知道,网络通信底层传输的都是字节,所以一个对象想要传输给其他的应用,都需要将这个对象序列化成字节数组。
在这一次的序列化框架中,我选择的是Kryo。这个序列化框架的性能较高并且使用起来也很方便。有一点比较特殊,要想使用最好性能时,需要事先声明可能需要序列化的类。
序列化工具类:KryoBuilder.java
数据加密
搭建完通信之后,我简单的测试了一下,发现有个比较明显的安全问题。在Wireshark抓包的过程中,能搞清晰的看到数据包中的数据,所以要在数据传输前对数据包进行加密的处理。
由于是一个游戏服务器,所以不能像普通的聊天服务一样,进行端到端的加密。并且由于是服务器与多个客户端的连接,于是采用了RSA+AES的处理。
密钥交换逻辑:
- 服务器生成并发送RSA公钥给客户端。
- 客户端生成AES密钥并使用RSA公钥加密,发送给服务器。
- 服务器使用RSA密钥解密得到AES密钥,密钥交换完成。
简单的来说就是服务器生成RSA公私钥,客户端生成AES密钥。
加密后的数据包:
连接监测
因为是一个游戏,所以对于网络的连接性需要更加敏感。于是在客户端设置了800ms的心跳事件定时任务,每隔800ms发送一个心跳事件给游戏服务器。
为什么不是服务器给客户端发送呢?因为这个定时发送心跳事件需要额外的资源,所以使用客户端最合适了😎
当有心跳数据包发送失败次数达到阈值时,重新尝试连接服务器并重置失败次数。
总结
到了这里,这一篇斗地主的网络通信就差不多写完了,就差往里面塞代码了。其中有一个非常严峻的问题等着我去做一个抉择,咱游戏的界面究竟要用啥技术呢?是Java的GUI呢还是常规的Web端呢?都是有待商榷的,好了,就说到这里了,下一篇文章再见!