本章节我们来讲解GameServer服务,首先来查看它的文件结构
ai:游戏角色自动化处理,比如说,自动攻击。
cache:数据缓存,里面就一个HtmCache.java类,缓存HTML文件内容。
communitybbs:bbs的管理。
data:对游戏数据的管理,涉及文件和数据库的操作。
enums:枚举类型,都是与游戏数据相关的,例如职业枚举数据。
geoengine:地图数据管理,例如三维场景下高度数据的获取。
handler:处理器,例如使用技能的效果处理。
instancemanager:各种类实例化管理,比如游戏对象实例化。
model:游戏业务模型,比如各种游戏对象类的实现。
network:网络数据通信,继承commons\network下的接口或父类
script:游戏脚本的管理
scripting:游戏脚本的管理
taskmanager:定时任务的管理
ui:图形界面目录,我们一般不使用它。
util:工具栏,一些辅助性功能的实现。
GameServer.java:GameServer服务启动入口,我们之前就是其他的它。
LoginServerThread.java:与LoginServer保持通信的线程
Shutdown.java:关闭GameServer服务
接下来,我们还是先研究network网络数据通信部分。
clientpackets:玩家客户端发送过来的数据包(继承ClientPacket接口)。
serverpackets:发送给玩家客户端的数据包(继承ServerPacket,继承WritablePacket)。
ClientPackets.java:客户端数据包数据枚举(根据ID实例化clientpackets数据包)。
ExClientPackets.java:客户端数据包数据枚举(解决ID数值超出byte范围问题)
ServerPackets.java:服务器数据包枚举(用来标记serverpackets的ID)
SystemMessageId.java:发送给客户端的消息ID
PacketLogger.java:数据包日志类。
PacketHandler.java:数据包处理器,继承PacketHandlerInterface接口。
BlowFishKeygen.java:秘钥生成(加密和解密数据包)
Encryption.java:加密解密类,继承EncryptionInterface接口。
GameClient.java:玩家客户端类,继承NetClient类。
ClientString.java:一个自定义的注解
ConnectionState.java:玩家客户端连接状态枚举。
loginserverpackets:发送给LoginServer服务的数据包(不讲解)。
接下来,我们介绍GameServer与玩家客户端之间的通信。当我们启动GameServer服务的时候,他会启动一个NetServer服务类,这个类我们之前已经讲过了。它里面有一个ServerSocketChannel,用来监听本机的7777端口。当有玩家客户端连接到GameServer的时候,我们就创建一个GameClient类,它代表了玩家客户端,继承自NetClient类。这个类非常重要,它会持有加密和解密的Encryption类,当然我们依然可以通过他的sendPacket方法向玩家客户端发送数据包。NetServer收到客户端发送的数据包之后,就会调用PacketHandler的handle方法来进行处理,这个类继承自PacketHandlerInterface接口。处理的方式就是,读取数据包的第一个字节,它就代表了数据包的ID。每一个数据包都有一个唯一标识ID,根据这个ID我们就能与真正的“游戏业务数据包”对应上了。这个对应关系是在ClientPackets和ServerPackets两个枚举类里面实现的。
这里,我们简单列举几个clientpackets游戏业务数据包
ProtocolVersion.java:客户端请求密钥,ID为0x00
AuthLogin.java:登录游戏服务器,返回玩家的角色列表,ID为0x08
NewCharacter.java:进入创建角色界面,发送角色模板数据,ID为0x0E(14)
CharacterCreate.java:创建并保存新角色,ID为0x0B (11)
CharacterSelected.java:选择角色,ID为0x0D (13)
EnterWorld.java:进入游戏世界,ID为0x03
Logout.java:退出游戏,ID为0x09
我们再来看一些serverpackets游戏业务数据包吧。
KeyPacket.java:向客户端发送密钥,ID为0x00
CharSelectInfo.java:返回玩家的角色列表,ID为0x13(19)
---------------------------------------------------------------------------
CharTemplates.java:角色模板,用于新建角色,ID为0x17(23)
CharCreateOk.java:创建角色成功,ID为0x19(25)
CharCreateFail.java:创建角色失败,ID为0x1A(26)
---------------------------------------------------------------------------
CharSelected.java:返回选中的角色信息,准备进入游戏世界,ID为0x15(21)
UserInfo.java:进入游戏世界,发送角色主要信息,ID为0x04
LeaveWorld.java:退出游戏世界,ID为0x7E(126)
我们上面已经说明了,每一个数据包的第一个字节代表了该数据包的唯一标识ID(上展示的ID都是十六进制)。当然,还有很多很多的数据包。我们获取这个ID之后,就能知道他对应的是哪个“游戏业务数据包”。这个是根据ClientPackets.java,ExClientPackets.java和ServerPackets.java枚举类型来定义对应关系的。这里需要单独说明一下ClientPackets.java和ExClientPackets.java的关系,他们两个都是clientpackets游戏业务数据包的ID。只不过后者是为了解决ID数值超出Byte字节大小的问题。因为我们的clientpackets非常的多,以至于它必然会超出byte自己的大小范围。于是,我们就规定当ID = 0xD0的时候,我们就继续读取数据包中下一个ID的数值,使用它来继续确定是哪个clientpackets游戏业务数据包。那么这个第二个ID数值中个对应的数据包就由ExClientPackets.java枚举来确定了。
接下来,我们继续研究PacketHandler的handle方法,如何处理clientpackets游戏业务数据包。首先是读取数据包中的第一个字节数据
final int packetId;
packetId = packet.readByte();
根据ID找到枚举类型,也就是对应的clientpackets游戏业务数据包
final ClientPackets packetEnum = ClientPackets.PACKET_ARRAY[packetId];
然后就可以实例化了
final ClientPackets packetEnum = ClientPackets.PACKET_ARRAY[packetId];
虽然声明的是ClientPacket接口类型,但是实际上就是clientpackets游戏业务数据包。接下来,我们就使用线程池技术来执行ClientPacket里面的read和run方法。这两个方法,前者是进行Byte数组数据转化类属性变量的,后者则是执行具体的游戏业务代码。如果需要向客户端发送数据包的话,也是在这个run方法中执行的。
ThreadPool.execute(new ExecuteTask(client, packet, newPacket, packetId));
我们可以查看一下ExecuteTask任务内容
_newPacket.read(_packet);
_newPacket.run(_client);
就是依次执行了read方法和run方法。当ID值为0xD0的时候,我们就会实例化ExPacket这个数据包。它不是一个游戏业务数据包。我们查看它的read方法
final int exPacketId = packet.readShort() & 0xFFFF;
_packetEnum = ExClientPackets.PACKET_ARRAY[exPacketId];
_newPacket = _packetEnum.newPacket();
_newPacket.read(packet);
看到了吧,它实际是继续读取下一个ID数值,在根据这个ID数值去ExClientPackets.java枚举中找真正的游戏业务数据包。找到之后,就实例化newPacket,然后执行实例化后newPacket的read方法。然后在run方法中,也是同样执行实例化后newPacket的run方法。也就是说,我们对应游戏的处理,就重点查看clientpackets游戏业务数据包中的run方法就行了。如果需要向客户端返回serverpackets游戏业务数据包,也是在这里执行的。这里需要注意的是,serverpackets中的数据包是直接实例化的,而它的ID则是由ServerPackets.java枚举来提供的。这一点大家要明白。
接下来,我们就来根据数据包来大致介绍一下GameServer与玩家客户端的数据通信。首先,我们仍然是先对GameClient进行实例化,这个没有太多的业务代码。然后,玩家客户端会请求ProtocolVersion数据包,该数据包中包含了客户端的版本号,然后我们在run方法中向客户端返回KeyPacket数据包,这个数据包里面包含的就是加密和解密的秘钥。有了秘钥,客户端和服务器端才能进行数据通信。
client.setProtocolVersion(_version);
client.sendPacket(new KeyPacket(client.enableCrypt(), 1));
这个KeyPacket数据包还是比较简单的,首先是他的构造方法
public KeyPacket(byte[] key, int result)
{
_key = key;
_result = result;
}
就是将传递过来的数据,赋值给自己类的属性变量上面。接下来就是write方法。
ServerPackets.KEY_PACKET.writeId(this);
writeByte(_result);
for (int i = 0; i < 8; i++)
{
writeByte(_key[i]);
}
writeInt(Config.PACKET_ENCRYPTION);
writeInt(Config.SERVER_ID);
writeByte(1);
我们不用过多的理解返回客户端的Byte数据中的所有详细内容。因为这些数据是让客户端程序来解读的。我们看到的第一句代码,就是从ServerPackets.java枚举中获取ID。
接下来,客户端获取了秘钥之后,就会继续发送AuthLogin请求数据包,里面包含了会话SessionKey数据对象和账号信息。然后在run方法中,会与LoginServer进行通信,告诉有玩家登录游戏了。然后GameServer收到LoginServer的回复之后,会想客户端发送CharSelectInfo数据包。这个数据包就是从数据库中查询玩家的所有游戏角色,然后玩家选择其中一个角色,就可以进入游戏世界了。当然,由于我们是第一次运行程序,因此,我们是没有角色的。所以,我们需要创建游戏角色。因此,我们需要在游戏客户端里面点击“创建角色”的按钮,进入到创建角色的界面。此时,客户端会向服务端发送NewCharacter数据包,这个数据包会返回客户端CharTemplates数据包,这个数据包包含了游戏的基础职业信息(人类法师和战士,精灵法师和战士等等)。关于如何创建角色,我们下一个章节介绍。
本章节涉及的内容均已上传百度网盘:
https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwd=avd4
欢迎加企鹅交流裙:874700842(裙文件里面也可以下载所有内容)。