第七章:L2JMobius学习 – 登录服务LoginServer讲解

在上一个章节中,我们学习了网络数据传输的封装network。那么,在本章的登录服务LoginServer的讲解中,我们就来使用一下这个封装好的功能。Network的封装需要我们继承很多的接口或类。我们首先查看一下登录服务LoginServer的文件结构,如下所示

enums:枚举目录,里面放置了一些枚举类型,主要是一些返回结果状态数据。

model:游戏模型目录,里面就一个AccountInfo类,代表玩家的登录账号和密码。

network:网络数据传输目录,继承commons\network下的接口或父类

LoginController.java:登录控制器,主要用于验证登录账号和密码是否合法。

LoginServer.java:登录服务,我们之前就是启动的这个类。

SessionKey.java:登录会话类,玩家客户端的身份标识。

GameServerTable.java:用来管理GameServer的信息(例如IP地址)。

FloodProtectedListener.java:是一个纪录GameServer连接数量的保护措施

GameServerListener.java:GameServer监听器,继承自上面的FloodProtectedListener

GameServerThread.java:连接GameServer服务线程,与GameServer保持通信。

HackingException.java:受攻击的异常封装类,纪录攻击者的IP地址(没有使用)。

ui:图形界面目录,我们一般不使用它。

简单介绍目录和类之后,我们就来重点查看network目录,如下所示

clientpackets:玩家客户端发送过来的数据包(继承LoginClientPacket接口)

serverpackets:发送给玩家客户端的数据包(继承WritablePacket)

LoginClientPackets.java:玩家客户端数据包枚举(根据ID实例化clientpackets数据包)

LoginServerPackets.java:发送给玩家客户端的数据包枚举(用来标记serverpackets的ID)

ConnectionState.java:记录玩家客户端的登录状态,就是一个枚举类型而已

LoginClient.java:玩家客户端,继承NetClient

LoginPacketHandler.java:客户端数据包处理器,继承PacketHandlerInterface

LoginEncryption.java:加密解密类(用于数据包的加密和解密)

ScrambledKeyPair.java:加密解密的秘钥(用户账号密码的加密和解密)

gameserverpackets:这是GameServer服务发送过来的数据包(不用讲解)

loginserverpackets:这是发送给GameServer服务的数据包(不用讲解)

GameServerPacketHandler.java:GameServer数据包处理器(不用讲解)。

这里简单解释一下GameServer和LoginServer的通信问题。LoginServer在启动的时候会实例化GameServerListener(GameServer监听器),这个监听器继承自FloodProtectedListener。这个监听器就是一个线程,在该线程中开启一个ServerSocket,接受来自GameServer的连接。在FloodProtectedListener中会有一个Map<String, ForeignConnection>集合,里面存储了每一个GameServer的连接次数。当连接次数出现问题的时候,就不允许GameServer连接LoginServer了。因此,这是一个保护措施。GameServer不能连接LoginServer的话,玩家就无法正常登录GameServer,也就是不能进入游戏里面了。这是因为,LoginServer要维护所有的GameServer,包括注册新的GameServer等等。GameServerListener中会有一个Collection<GameServerThread> _gameServers集合,每一个GameServer都会对应一个GameServerThread线程类,该线程类用来实时的与GameServer保持通信。通信的内容就是GameServer告诉LoginServer,我是一个“正常”的服务。当玩家输入账号和密码验证成功之后,LoginServer就会将这些“正常”的GameServer展示出来(游戏大区)。玩家就可以选择其中一个GameServer(游戏大区),然后就能进入游戏世界了。此时LoginServer的工作任务就结束了,后面就交个玩家选择的GameServer来处理玩家的请求数据包。请注意,LoginServer和GameServer可以是IP地址不同的服务器,并且GameServer可以是多个。由于我们只是本地测试,因此LoginServer和GameServer都在我们同一台电脑上,但是他们监听的端口是不一样的。默认情况,LoginServer监听2106端口,GameServer监听7777端口。

接下来,我们介绍LoginServer与玩家客户端之间的通信,这才是我们本章节的重点。我们首先大概介绍一下整体的流程。首先,在LoginServer在启动的时候会实例化并启动NetServer。这个NetServer会在本机的2106端口进行监听。当有玩家客户端连接过来的时候,我们就会实例化一个LoginClient类(继承NetClient),它就代表了玩家客户端。这个LoginClient类非常的重要,我们LoginServer向玩家客户端发送数据包,就是通过该类的sendPacket方法来实现的。同时,LoginClient类还持有LoginEncryption加密解密类,以及加密解密的秘钥。这个秘钥有两个,一个是数据包的秘钥SecretKey _blowfishKey,另一个是账号和密码的秘钥ScrambledKeyPair _scrambledPair。两者加密的方式是不一样的。关于加密算法,我们这里不做过多的解释,大家可以去其他地方学习一下。总之,这个LoginClient类非常的重要。实例化LoginClient完毕之后,NetServer就会使用多线程(ReadThread)来读取玩家客户端的数据包。读取完毕的数据包被存储在NetClient中的Queue队列中。接下来,NetServer会再次使用多线程(ExecuteThread)来解密和处理数据包。解密的方式就是调用LoginClient类中的LoginEncryption的decrypt方法(本质是Blowfish算法,由org.l2jmobius.commons.crypt.BlowfishEngine类提供)。解密完毕之后,就将数据包交给LoginPacketHandler来处理。其实就是调用LoginPacketHandler的handle方法。该方法第一个参数就是LoginClient类,第二个参数就是数据包(字节数组格式)。那么,如何将字节数组格式的数据包转化成正常的clientpackets呢?每一个数据包的第一个字节代表了该数据包的唯一标识ID。这个ID就代表了不同的clientpackets。

我们首先看看clientpackets中有哪些“游戏业务数据包”吧。

AuthGameGuard.java:请求 GameGuard 授权,ID是0x07

RequestAuthLogin.java:请求账号密码登录,ID是0x00

RequestServerList.java:请求GameServer游戏服务列表,ID是0x05

RequestServerLogin.java:请求登录选定GameServer游戏服务,ID是0x02

我们再来看看serverpackets中有哪些“游戏业务数据包”吧。

Init.java:返回会话ID,密钥以及密钥对,ID是0x00

GGAuth.java:返回GameGuard 授权,ID是0x0b

LoginOk.java:返回账号密码登录成功,ID是0x03

LoginFail.java:返回账号密码登录失败,ID是0x01

ServerList.java:返回GameServer 游戏服务器列表,ID是0x04

PlayOk.java:返回登录选定GameServer游戏服务成功,ID是0x07

PlayFail.java:返回登录选定GameServer游戏服务失败,ID是0x06

我们上面已经说明了,每一个数据包的第一个字节代表了该数据包的唯一标识ID(上展示的ID都是十六进制)。我们获取这个ID之后,就能知道他对应的是哪个“游戏业务数据包”。这个对应关系是通过LoginClientPackets.java和LoginServerPackets.java两个枚举来实现的。

我们首先介绍clientpackets的处理过程。首先,所有的clientpackets都继承LoginClientPacket接口,这个接口只有两个方法,一个是read方法,一个是run方法。read方法就是将字节数组格式的数据包逐一转化成类的属性变量。而run方式则是游戏业务处理。我们接下来详细查看LoginPacketHandler的handle方法代码:

final int packetId;
packetId = packet.readByte();

这是读取“游戏业务数据包”ID的代码,然后去LoginClientPackets中寻找。

// Find packet enum.
final LoginClientPackets packetEnum = LoginClientPackets.PACKET_ARRAY[packetId];

找到之后,就可以实例化了。

// Create new LoginClientPacket.
final LoginClientPacket newPacket = packetEnum.newPacket();

虽然我们声明的是LoginClientPacket类型,但是本质上就是AuthGameGuard类,RequestAuthLogin类,或者RequestServerList类等等。有了这些真正的“游戏业务数据包”,就可以调用他们的read方法和run方法了。

ThreadPool.execute(new ExecuteTask(client, packet, newPacket, packetId));

以上代码是使用线程池技术执行一个ExecuteTask线程任务。在这个任务中,我们就会调用他们的read方法和run方法。在run方法中,我们会向玩家客户端发送serverpackets“游戏业务数据包”。也就是说,我们重点关注run方法的处理逻辑,就可以了。这里需要注意的是,serverpackets中的“游戏业务数据包”是直接实例化的,而它对应的ID则是由LoginServerPackets.java枚举来提供的。

LoginServer处理客户端请求是从实例化LoginClient类开始的。

_blowfishKey = LoginController.getInstance().generateBlowfishKey();
_encryption.setKey(_blowfishKey.getEncoded());
_scrambledPair = LoginController.getInstance().getScrambledRSAKeyPair();
_sessionId = Rnd.nextInt();
_connectionStartTime = System.currentTimeMillis();
sendPacket(new Init(_scrambledPair.getScrambledModulus(), _blowfishKey.getEncoded(),_sessionId));

以上就是LoginClient类的构造方法,它主要任务就是创建秘钥,创建会话sessionId。最重要的就是将秘钥和会话sessionId发送给客户端,也就是Init数据包。这个数据包属于serverpackets,也就是服务器端发送给玩家客户端的数据包。他们都要继承WritablePacket。这种数据包基本上就是两个重要的方法,一个是构造方法,一个是write方法。构造方法是为了接受处理好的数据(数据包类的属性变量),write方法就是将数据(数据包类的属性变量)转化成字节数组。最终就会使用LoginClient类的sendPacket将字节数组发送给客户端。我们具体来看一下Init数据包。首先是他的构造方法。

public Init(byte[] publickey, byte[] blowfishkey, int sessionId)
{
    _sessionId = sessionId;
	_publicKey = publickey;
	_blowfishKey = blowfishkey;
}

接受秘钥和会话sessionId,然后就是write方法将类的属性变量转化成字节数组,

LoginServerPackets.INIT.writeId(this);
writeInt(_sessionId); 
writeInt(0x0000c621); 
writeBytes(_publicKey); 
writeInt(0x29DD954E);
writeInt(0x77C39CFC);
writeInt(0x97ADB620);
writeInt(0x07BDE0F7);
writeBytes(_blowfishKey);
writeByte(0);

至于为什么要按照这种格式组织字节数组,我们就不需要了解太多了。因为这些字节数组的解析是游戏客户端要做的事情了。这里需要注意的是,返回给客户端的数据包第一个字节同样必须也是ID。因此,我们看到第一行代码就是写入这个ID。我们上文也提到过,这个ID是由LoginServerPackets枚举来提供的。

接下来,玩家客户端收到Init数据包之后,就会向LoginServer服务器发送AuthGameGuard数据包,在这个数据包的run方法中,直接返回GGAuth数据包,参数为会话sessionId。

接下来,玩家客户端就是展示登录界面,玩家输入账号和密码,点击“登入”按钮。其实就是向LoginServer服务器发送RequestAuthLogin数据包。在这个数据包的run方法中,我们可以通过ScrambledKeyPair _scrambledPair 秘钥来解密账号和密码。接下来,就是检查账号和密码的代码逻辑,如下所示:

final String clientAddr = client.getIp();
final LoginController lc = LoginController.getInstance();
final AccountInfo info = lc.retriveAccountInfo(clientAddr, user, password);

其实就是调用LoginController类的retriveAccountInfo方法。这个方法其实就是查询accounts数据表,其中login字段就是账号,password字段就是密码。如果账户存在,并且密码匹配,那么就登录成功了。如果账户不存在,就自动写入一条新纪录,相当于注册新账户了。处理这些完毕之后,就可以向客户端发送ServerList数据包了。

client.setAccount(info.getLogin());
client.setConnectionState(ConnectionState.AUTHED_LOGIN);
client.setSessionKey(lc.assignSessionKeyToClient(info.getLogin(), client));
client.sendPacket(new ServerList(client));

我们来看一看ServerList数据包是如何构建出来的。在这个数据包中,有一个List<ServerData> _servers 列表,里面存放了多个GameServer服务器信息。这些信息从哪里来的呢?非常的简单,就是查询gameservers数据表获取的,其中server_id字段代表该服务的ID,hexid字段也是它的ID,host字段则是它的IP地址。那么,gameservers数据表中的纪录从哪里来的呢?还记得LoginServer与GameServer之间的通信吗?当GameServer连接成功LoginServer的时候,就会向gameservers数据表添加GameServer的信息(IP地址)。大致理解这些之后,我们就不再详细介绍了。剩下的write方法就是将List<ServerData> _servers 列表中的GameServer信息发送给玩家客户端。

玩家客户端收到GameServer信息之后,就会展示“游戏大区”。然后玩家就可以选择一个。当然,由于我们目前只有一个GameServer,所以只显示一个“游戏大区”。我们直接点击选择这个唯一的“游戏大区”就可以了。紧接着,玩家客户端会向LoginServer发送RequestServerLogin数据包,这数据包中就包含了玩家选择“游戏大区”的ID。然后,我们就想玩家客户端发送PlayOk数据包,告诉玩家客户端可以登录GameServer服务了。到此为止,LoginServer的工作任务就结束了,剩下的就是GameServer该上场了。我们将在下一章介绍。

本章节涉及的内容均已上传百度网盘:

https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwd=avd4

欢迎加企鹅交流裙:874700842(裙文件里面也可以下载所有内容)。

猜你喜欢

转载自blog.csdn.net/konkon2012/article/details/131615486