第七章: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(裙文件里面也可以下载所有内容)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/35639.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DotNet VOL.Core框架学习使用笔记(二)(持续更新)

2023-7-5 生成代码的列表界面&#xff0c;在数据行里增加一个操作列 查看按钮&#xff0c;打开编辑框&#xff0c;然后让编辑框成为一个只读的查看界面。 页面对应的js文件中增加如下 this.columns.push 函数内容。 按钮的点击事件 重点代码 this.edit(row); 这就是框架里编…

天翎群晖NAS为全文检索插翅起飞

编者按&#xff1a;企业的文档资料随着企业的业务发展会越来越多&#xff0c;想要某个资料的时候&#xff0c;最怕找不到想要的资料&#xff0c;这时KMS的全文检索功能就非常重要了&#xff0c;只需只言片语的零星关键字&#xff0c;查找文档没压力。 关键词&#xff1a;全文检…

MySQL练习题(1)

1,创建如下学生表 mysql> create table student( -> id int, -> name varchar(20), -> gender varchar(20), -> chinese int, -> math int, -> english int -> ); 插入如图数据 1-- 查询表中所有学生的信息 select *from student;2-- 查询表中所有学…

TypeScript 总结

文章目录 TypeScript 总结概述运行ts文件方式一方式二 基础声明变量类型数组元组联合类型取值限制 枚举类型any & unknownvoid & undefined类型适配 面向对象函数普通函数箭头函数可选参数默认参数 对象创建对象对象的类型限制 类和接口泛型简单使用多个泛型默认泛型类…

集合处理常用Stream流

集合处理常用Stream流 1、Stream API介绍2、List集合常用Stream方法 stream流经常使用&#xff0c;但是遇到一些流操作时&#xff0c;会一下想不到用哪种&#xff0c;这里总结一下&#xff0c;方便自己或者读者查找 1、Stream API介绍 Stream API是Java 8引入的一项重要特性&a…

vue对于数组的数据监听变化和object是不一样的吗?

我们知道vue对于数组的数据监听变化和object是不一样的&#xff0c;因为我们常说的Object.defineProperty是对象上面的方法&#xff0c;所以对于array数组需要实现另外一套变化侦测机制。 今天我们就来研究下。 在哪里收集依赖 array数据设计了新的变化侦测机制&#xff0c;…

10个图像处理的Python库

在这篇文章中&#xff0c;我们将整理计算机视觉项目中常用的Python库&#xff0c;如果你想进入计算机视觉领域&#xff0c;可以先了解下本文介绍的库&#xff0c;这会对你的工作很有帮助。 1、PIL/Pillow Pillow是一个通用且用户友好的Python库&#xff0c;提供了丰富的函数集…

QT day1简单登录界面

widget.cpp文件代码&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {qDebug()<<this->size();qDebug()<<this->frameSize();this->setFixedSize(50…

如何用Python搭建监控平台

监控和运维&#xff0c;是互联网工业链上非常重要的一环。监控的目的就是防患于未然。通过监控&#xff0c;我们能够及时了解到企业网络的运行状态。一旦出现安全隐患&#xff0c;你就可以及时预警&#xff0c;或者是以其他方式通知运维人员&#xff0c;让运维监控人员有时间处…

MATLAB 之 可视化图形用户界面设计

这里写目录标题 一、可视化图形用户界面设计1. 图形用户界面设计窗口1.1 图形用户界面设计模板1.2 图形用户界面设计窗口 2. 可视化图形用户界面设计工具1.1 对象属性检查器2.2 菜单编辑器2.3 工具栏编辑器2.4 对齐对象工具2.5 对象浏览器2.6 Tab 键顺序编辑器 3. 可视化图形用…

多元分类预测 | Matlab基于北方苍鹰优化深度置信网络(NGO-DBN)的分类预测,多输入模型,NGO-DBN分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab基于北方苍鹰优化深度置信网络(NGO-DBN)的分类预测,多输入模型,NGO-DBN分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab,程序可…

地下饮用水除硝酸盐技术、生活用水提质增效

项目名称 北京某地下水除硝酸盐项目&#xff0c;出水未检出 工艺选择 两处水源&#xff0c;运行方式为一用一备 工艺原理 树脂官能团耐受硫酸盐、氯离子的干扰&#xff0c;实现选择性吸附硝酸根 项目背景 近年来由于农业活动及排污物的影响&#xff0c;部分地表…

【STM32】STM32G系列使用CORDIC模块加速计算

1.前言 STM32G431系列产品内置了CORDIC运算单元&#xff0c;可以用来加速数学计算&#xff0c;如三角函数、取模、开方等。适合大量数据进行相同的运算操作。配合DMA可以大大节省CPU计算开销。 2.CubeMX配置 使用CORDIC模块无需配置参数&#xff0c;若采用DMA方式则只需配置…

多元分类预测 | Matlab基于灰狼优化深度置信网络(GWO-DBN)的分类预测,多特征输入模型,GWO-DBN分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab基于灰狼优化深度置信网络(GWO-DBN)的分类预测,多特征输入模型,GWO-DBN分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab,程序可…

如何提高OAK相机在树莓派和JETSON上的运行帧率?

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手君。 最…

【嵌入式Qt开发入门】如何使用Qt进行绘制实时图表——QChart 图表

要想使用 Qt Charts&#xff0c;我们的 Qt 版本得使用 Qt 5.7 之后的版本。其实 Qt Charts 并不是 Qt 5.7 才有的&#xff0c;是在 Qt 5.7 以前只有商业版本的 Qt 才有 Qt Charts。我们能免费下载的 Qt 版本都是社区&#xff08;开源&#xff09;版本。 Qt Charts 很方便的绘制…

密码学入门——DES与AES

文章目录 参考书目一、编码与异或1.1 编码1.2 异或 二、DES与三重DES三、AES 参考书目 图解密码技术&#xff0c;第三版 一、编码与异或 1.1 编码 计算机的操作对象并不是文字&#xff0c;而是由0和1排列而成的比特序列。无论是文字、图像、声音、视频还是程序&#xff0c;…

win系统删除oracle数据文件恢复---惜分飞

有客户联系我们,说win平台下的数据库,在由于空间紧张,在关闭数据库的情况下删除的两个数据文件,导致数据库无法正常访问很多业务表,需要对其进行恢复,查看alert日志发现大概操作,删除文件之后,启动数据库失败 Completed: alter database mount exclusive alter database open E…

【案例实战】SpringBoot整合Redis的GEO实现查找附近门店功能

像我们平常美团点外卖的时候&#xff0c;都会看到一个商家距离我们多少米。还有类似QQ附近的人&#xff0c;我们能看到附近的人距离我们有多少米。 那么这些业务是怎么做的呢&#xff1f;是如何实现 基于位置的附近服务系统呢。 在去了解基于位置的附近服务之前&#xff0c;我…

个人域名邮箱无法给gmail发邮件

问题描述 我注册了一个域名 mydomain.com, 并在此域名下创建了 mailbox&#xff0c;从该邮箱向外发送邮件和接收邮件会失败。 主要是一些配置工作没有做好。 接收邮件 当创建邮箱 xxxmydomain.com&#xff0c;尝试向该邮箱发送邮件时&#xff0c;邮件会被拒收&#xff0c;并…