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

news2024/9/25 7:23:36

在上一个章节中,我们学习了网络数据传输的封装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(裙文件里面也可以下载所有内容)。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Android 13]Binder系列--获取ServiceManager

获取ServiceManager hongxi.zhu 2023-7-1 以SurfaceFlinger为例&#xff0c;分析客户端进程如何获取ServiceManager代理服务对象 主要流程 SurfaceFlinger中获取SM服务 frameworks/native/services/surfaceflinger/main_surfaceflinger.cpp // publish surface flingersp<…

mysql单表查询,排序,分组查询,运算符

CREATE TABLE emp (empno int(4) NOT NULL, --员工编号ename varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,--员工名字job varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,--员工工作mgr int(4) NULL DEFAULT NU…

随机产生50个100以内的不重复的整数,设计位图排序算法进行排序。

1.问题 随机产生50个100以内的不重复的整数&#xff0c;设计位图排序算法进行排序。 2.设计思路 阶段1&#xff1a; 初始化一个空集合    for i[0,n)    bit[i]0 阶段2&#xff1a; 读入数据i&#xff0c;并设置bit[i]1    for each i in the input file    bit[i]1…

Tomcat 应用服务 WEB服务

简述&#xff1a; 中间件产品介绍 目前来说IBM的WebSphere&#xff0c;Oracle的Weblogic占据了市场上Java语言Web站点的部分份额&#xff0c;该两种软件由于无与伦比的性能及可靠性等优势被广泛应用于大型互联网公司的Web场景中&#xff0c;但是其高昂的价格也使得中小型互联…

DL-FWI:数据(第二次培训作业)

代码&#xff1a; import scipy.io import matplotlib import numpy as np import matplotlib.pylab as plt matplotlib.use(TkAgg) from mpl_toolkits.axes_grid1 import make_axes_locatable import cv2font21 {family: Times New Roman,weight: normal,size: 21,}font18 …

【通览一百个大模型】LaMDA(Google)

【通览一百个大模型】LaMDA&#xff08;Google&#xff09; 作者&#xff1a;王嘉宁&#xff0c;本文章内容为原创&#xff0c;仓库链接&#xff1a;https://github.com/wjn1996/LLMs-NLP-Algo 订阅专栏【大模型&NLP&算法】可获得博主多年积累的全部NLP、大模型和算法干…

图像金字塔、滑动条、鼠标事件响应

1、拉普拉斯图像金字塔 1.1 原理 1.2 实现 //拉普拉斯图像金字塔 void test1() {//高斯图像金字塔构建Mat img imread("F:/testMap/lena.png");vector<Mat> Guass;int level 3;Guass.push_back(img);for (int i 0; i < level; i){Mat guass;pyrDown(Gua…

蓝桥杯每日一练专栏导读2

之前一直更新的是C、C相关的题目&#xff0c;但作为一名前端工程师&#xff0c;还是对Js了解的更多一些&#xff0c;所以从此以后停止更新C/C相关内容&#xff0c;改为更新Js相关的练习题。 内容 更新的内容依旧是蓝桥杯大赛官网提供的习题。每一道题都会提供详细的解题思路&a…

方案编制要求--模版--可以借鉴

写方案的标题要求的编写&#xff0c;可以参照这个进行编写&#xff1b; 附录2&#xff1a;方案编制要求及模板 一、封面格式要求 封面内容应包括项目名称、需求单位&#xff08;盖章&#xff09;、建设单位&#xff08;盖章&#xff09;、设计单位&#xff08;盖章&#xff0…

线段树:一遍通透线段树

线段树有关的操作&#xff08;先大体上知道什么意思&#xff09;: 1 2 3 4 5 线段树前置知识&#xff1a; 由于线段树是一个完美二叉树&#xff0c;所以我们选择的是一维数组来存储线段树的相关知识&#xff1a; 所以&#xff1a; 1如果一个结点是X&#xff0c;则父节…

基于MATLAB的简单线性回归详解

概要 在机器学习领域中大多数任务通常都与预测&#xff08;prediction&#xff09;有关。当我们想预测一个数值时&#xff0c;就会涉及到回归问题。常见的例子包括&#xff1a;预测价格&#xff08;房屋、股票等&#xff09;、预测住院时间&#xff08;针对住院病人等&#xff…

如何用python编写3D游戏

Vizard是一款虚拟现实开发平台软件&#xff0c;从开发至今已走过十个年头。它基于C/C&#xff0c;运用新近OpenGL拓展模块开发出的高性能图形引擎。当运用Python语言执行开发时&#xff0c;Vizard同时自动将编写的程式转换为字节码抽象层(LAXMI)&#xff0c;进而运行渲染核心。…

当型循环和直到型循环(精讲)

目录 背景概念当型循环直到型循环 二维表对比图示与代码当型循环流程图N-S图&#xff08;盒图&#xff09; 直到型循环流程图N-S图&#xff08;盒图&#xff09; 例子当型图示代码 直到型图示代码 Do–Loop 和For –Next相同点&#xff1a;不同点&#xff1a;代码 总结 背景 两…

day02 重新学python——判断语句和循坏语句

文章目录 一、python中的判断语句1.布尔类型和比较运算符2.if语句的基本格式3.if else 语句4.if elif else 语句5.判断语句的嵌套6.实战案例 二、循环语句1.while循环的基础语法2.while循环的基础案例3.while循环的嵌套应用4.while循环的嵌套案例5.for循环的基础语法6.for循环的…

【计算机网络】第三章 数据链路层(点对点协议 媒体介入控制)

文章目录 3.5 点对点协议PPP3.6 媒体接入控制3.6.1 媒体接入控制的基本概念3.6.2 媒体接入控制——静态划分信道3.6.3 随机接入——CSMA/CD协议3.6.4 随机接入——CSMA/CA协议 3.5 点对点协议PPP 点对点协议是目前使用最广泛的点对点数据链路层协议。PPP协议为在点对点链路传输…

JAVA Email

Email就是电子邮件。电子邮件的应用已经有几十年的历史了&#xff0c;我们熟悉的邮箱地址比如aaaa22222163.com&#xff0c;邮件软件比如Outlook、网易闪电邮、Foxmail都是用来收发邮件的。当然&#xff0c;使用Java程序也可以收发电子邮件。 传统的邮件就是通过邮局投递&#…

目标检测+车道线识别+追踪+测距(代码+部署运行)

目标检测车道线识别追踪测距 本文主要讲述项目集成&#xff1a;从车道线识别、测距、到追踪&#xff0c;集各种流行模型于一体&#xff01; 不讲原理&#xff0c;直接上干货&#xff01; 把下文环境配置学会&#xff0c;受益终生&#xff01; 各大项目皆适用&#xff01; …

具有音调控制功能的25W混合式Hi—Fi放大器

现代电子技术应用中电子管的使用虽然已经较少&#xff0c;但由于电子管有晶体管不可替代的一些优越特性&#xff0c;所以在部分领域特别是音响电路中还受到人们的亲睐。这是一款由“靓”音电子管和音响集成电路联合组成的混合放大器。该放大器由电子管作前级&#xff0c;音响专…

7.8(wmi命令+ServiceName+CobaltStrike Loader)

优先级&#xff0c;进程ID&#xff0c;线程计数 优先级&#xff08;Priority&#xff09;是操作系统对进程或线程分配处理器资源的重要性排序。较高的优先级意味着进程或线程更有可能在竞争处理器资源时被调度执行。 进程ID&#xff08;Process ID&#xff09;是唯一标识系统中…

Python读取Excel文件并复制指定的数据行

本文介绍基于Python语言&#xff0c;读取Excel表格文件数据&#xff0c;并基于其中某一列数据的值&#xff0c;将这一数据处于指定范围的那一行加以复制&#xff0c;并将所得结果保存为新的Excel表格文件的方法。 首先&#xff0c;我们来明确一下本文的具体需求。现有一个Excel…