第六章:L2JMobius学习 – 源码讲解网络数据通信

news2024/9/19 10:36:44

本章节介绍客户端和服务器端的网络数据通信,使用的技术是Java NIO(也就是套接字Socket)。服务器端和客户端使用Socket通信的原因在于,它是双向的,持久的。也就是说,服务器端可以随时的向客户端发送数据,客户端也可以随时的向服务端发送数据。

请注意,不同于HTTP这样的高级协议,使用Socket通信的数据格式往往是Byte字节。当我们收到客户端发来的Byte字节数据的时候,我们就需要将这些字节数据转化为相应数据类型的数据。例如,真实的数据是一个int类型的话,我们就需要将4个字节的数据转化成一个int类型数据。同样的,服务器端发给客户端的数据,也有统一转化成字节数据。

那么,这一堆堆的字节数据如何转化为真实数据呢?我们怎么知道哪几个字节数据需要转化成那些类型的数据呢?这就要求我们对双方通信的数据进行“格式约定”。例如,当我们接收到一个数据的时候,我们“固定读取”前两个字节转化成一个short类型数值数据,该数值数据就代表了当前数据包的长度,我们接下来就需要根据这个长度来获取后面的数据即可。

当获取完整的数据包之后,我们继续读取一个int类型数值数据,这个数值数据代表了“业务模型类”。接下来,我们就可以将数据包中的数据,按照“业务模型类”里面定义的属性(变量)进行转化了。这些类属性(变量)的顺序与数据包中的字节数据是一一对应的关系。例如,当前“业务模型类”中有一个int类型的a变量和short类型的b变量,那么我们就讲前4个字节转化成int类型赋值给a变量,后面2个字节转化成short类型赋值给b变量。对于String类型的话,还要约定它的长度,然后将这个长度的字节数据整体转化成字符串类型数据。

成功获取“业务模型类”之后,我们就可以根据“游戏业务逻辑”对它进行下一步的处理。例如,这个“业务模型类”是登录请求的话,那么里面就包含了账号和密码数据。那么,我们下一步的处理就应该是验证账号和密码是否正确。如果不正确,就要向客户端发送失败数据包;如果正确,就要向客户端发送成功数据包。当然,这里返回给客户端的数据包,就需要将各种数据类型的数据按照顺序逐一放进byte数组中,最后再通过Socket发送给客户端。这就是服务端和客户端的一个简单通信流程。

这里面需要注意的是,因为Socket通信的字节数据发送并不是“有序”的,它不会一个一个数据包的进行发送,而是将一个或多个,甚至半个数据包进行发送。因此,当我们接收到Byte数据的时候,一定要按照“格式约定”来读取完整的数据包。如果不是完整的数据包,我们就需要等待读取后面的数据,拼凑成完整的。

接下来,我们回到“L2J_Mobius”工程里面。

在上面的目录结构中,很多文件实际是没有用的。我们重点说一下几个目录。

config是配置文件目录,里面有很多配置文件,其中就包括数据连接的配置(我们之前改过)。

data是游戏数据目录,里面有很多的游戏数据,比如NPC对话等等。

libs是数据库链接驱动文件,这个我们之前也介绍过。

log是日志目录,服务启动后,很多日志都是在这里纪录的。

src是源码目录,这是我们要重点讲解的。

接下来,我们就进入到src/org/l2jmobius 目录下

commons是公共包,里面提供了一些封装好的实现某种特定功能的类,供其他模块使用。

gameserver游戏服务包,启动里面的GameServer.java就能处理来自客户端的数据包。

log日志包,负责完成日志记录的功能。

loginserver登录服务包,启动里面的LoginServer.java就能处理客户端的登录操作。

tools工具包,包含游戏账号管理和数据库初始化等等,这个我们暂时不用。

Config.java配置类,其实就对应了我们上面介绍的config配置文件目录。

这里稍微说明一下gameserver和loginserver的区别。loginserver用来处理玩家的账号登录,然后玩家选择完游戏大区之后,就返回该游戏大区的IP地址,然后玩家就能进入到指定游戏大区的游戏世界里面了,这就对应了gameserver。很明显,loginserver只有一个,而游戏大区有很多,他们都对应一个个的gameserver。也就是说,他们是一对多的关系。当然,我们本地测试的话,只需要一个loginserver和一个gameserver,并且他们在同一台电脑上。在实际的游戏部署的时候,loginserver和gameserver都会独占一台服务器,都拥有独立的IP地址。当然,这些不是我们章节介绍的内容。

本章节要介绍网络数据通信的部分,它对应的代码位于commons\network目录下。

ReadablePacket.java:客户端发送给服务器端的数据包父类。
WritablePacket.java:服务器端发送给客户端的数据包父类。

ReadThread.java:读取线程,用来读取客户端发送过来的数据包。
ExecuteThread.java:执行线程,主要用来解密数据包,在进行游戏逻辑处理。

EncryptionInterface.java:加密和解密的接口而已,需要子类来实现。
PacketHandlerInterface.java:数据包游戏逻辑处理接口,需要子类来实现。

NetConfig.java:网络数据通信的配置参数,例如线程池的大小配置。
NetClient.java:客户端父类,持有SocketChannel通道对象。
NetServer.java:服务端类,就是ServerSocketChannel类。

首先,我们介绍一下ReadablePacket.java和WritablePacket.java两个数据包父类。他们只是完成基础的数据功能,不包括与游戏相关的业务数据。他们两个里面都有一个byte数组,这是客户端和服务器端通信的底层字节数据。其次,ReadablePacket.java包含了将byte转化成各种数据类型的方法,而WritablePacket.java而是包含了将各种数据类型转化成byte的方法。这个我们在本章节开始的位置就讲解过,应该很容易理解。在游戏开发过程中,数据包的处理是非常多的,他们都要继承ReadablePacket或者WritablePacket。

接下来,我们详细介绍一下ReadThread.java读取线程。该线程里面有一个set集合,集合中存放了NetClient客户端对象。在这个NetClient.java类中,有三个重要的属性变量。

// 完整的数据包队列,需要下一步解密
private Queue<byte[]> _pendingPacketData;
// 不完整的数据包,需要继续从客户端读取剩余数据
private ByteBuffer _pendingByteBuffer;
// 不完整的数据包的长度,根据这个长度来读取
private int _pendingPacketSize;

有了这三个属性变量的理解之后,我们就很容易理解ReadThread.java读取线程了。首先,我们要循环遍历set集合,获取到里面的每一个NetClient客户端对象,然后获取对应的SocketChannel通道对象,然后就可以通过read方法读取客户端发送过来的数据了。这里分两种情况,第一种就是“半包”的情况,第二种就是“非半包”的情况。

如果是“半包”的情况的话。我们就需要将这个不完整的数据包放入到NetClient中的pendingByteBuffer中,并且还要设置该数据包的完整长度pendingPacketSize。所以,我们再读取客户端发送过来的数据的时候,就要考虑pendingByteBuffer中是否数据。如果存在数据的话,就需要先获取pendingByteBuffer的数据,然后在根据pendingPacketSize获取剩余的数据。这个就非常简单了,使用pendingPacketSize减去pendingByteBuffer的长度。

final ByteBuffer pendingByteBuffer = client.getPendingByteBuffer();
final int pendingPacketSize = client.getPendingPacketSize();
final ByteBuffer additionalData = ByteBuffer.allocate(pendingPacketSize - pendingByteBuffer.position());
channel.read(additionalData)

读取完毕之后,就可以将完整的数据包放入到NetClient中的pendingPacketData队列中了。当然不要忘记清除缓存的“半包”数据。

client.addPacketData(pendingByteBuffer.array());
client.setPendingByteBuffer(null);

接下来,我们继续读取客户端的数据包。首先要读取2个字节的长度_sizeBuffer,这是接下来的数据包的完整长度。接下来,就按照sizeBuffer的长度来读取数据包。

final int packetSize = calculatePacketSize();
final ByteBuffer packetByteBuffer = ByteBuffer.allocate(packetSize);
channel.read(packetByteBuffer)

如果能够读取完毕,那就是一个完整的数据包,我们将其放入到NetClient中的pendingPacketData队列中就可以了。如果实际读取的数据不完整,也就是出现了“半包”的情况,我们就只能将读取的数据放入到NetClient中的pendingByteBuffer中,并且还要设置该数据包的完整长度pendingPacketSize。

client.setPendingByteBuffer(packetByteBuffer);
client.setPendingPacketSize(packetSize);

这样,就又回到了刚刚开始的地方。我们要记住的就是,读取完整的数据包是放置在NetClient中的pendingPacketData队列中就可以了。

接下来,我们介绍ExecuteThread执行线程。他里面也有一个Set集合,里面同样存放着NetClient客户端对象。同时在线程中,还有一个PacketHandlerInterface子类,它用来对数据包进行游戏逻辑的处理。但是,在进行游戏逻辑处理之前,还需要对数据包进行解密。这就需要借助EncryptionInterface子类的实现。我们还是回到ExecuteThread线程中。首先就是循环遍历Set集合,然后获取到每一个NetClient客户端对象。然后获取一个完整的数据包,再对其进行解密,最后交给PacketHandlerInterface子类来处理。

final byte[] data = client.getPacketData().poll();
client.getEncryption().decrypt(data, 0, data.length);
_packetHandler.handle(client, new ReadablePacket(data));

最后我们来介绍一下NetServer服务端类,他里面持有ServerSocketChannel对象,可以监听指定的端口。在这个类里面,有两个重要的List列表对象,如下所示

protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>();

看名称就知道,一个是读取客户端列表,一个是执行客户端列表。两个列表里面存放的都是Set集合。这个Set集合里面放的就是NetClient客户端对象。而每一个Set集合会对应一个ReadThread读取线程或者ExecuteThread执行线程。我们可以这样理解,有两个列表,里面存放了很多的ReadThread读取线程或者ExecuteThread执行线程,每一个线程对应一个Set集合,这个Set集合里面放了一定数量的NetClient客户端对象。为什么要这样设计呢?其实非常的容易理解。我们处理客户端的请求,肯定是需要借助多线程的。所以,我们要实例化出来很多的线程,这些线程可以分为读取线程和执行线程两种。这些线程肯定要放到List列表中,或者使用线程池也是可以的。每一个线程不可能只处理一个NetClient客户端对象,那样就太浪费服务器端的资源了,所以每个线程都会处理一定数量的NetClient客户端对象。这些NetClient客户端对象就需要放置到Set集合中。这样就很容易理解了吧。

NetServer服务端类的主要代码是用来接收新的客户端链接,然后实例化NetClient客户端对象。然后将NetClient客户端对象放入到Set集合中。如果不存在Set集合的话,就实例化一个新的Set集合,同时在实例化一个读取或执行线程,将Set集合传递给该线程。最后将我们的Set集合放入到List中就行了。NetServer服务端类的代码就是这些了。

我们总结一下,客户端和服务器端的网络数据通信的代码位于commons\network目录下,它是封装好的公共模块。我们的loginServer和gameServer都要借助它才能实现数据通信。使用network包的方式就是继承里面的父类。例如,读取客户端的数据包类要继承ReadablePacket.java;而发送给客户端的数据包要继承WritablePacket.java;数据的加密解密要继承EncryptionInterface.java;处理游戏数据包要继承PacketHandlerInterface.java;客户端页要继承NetClient.java;服务器端同样要继承NetServer.java(也可以直接使用该类)。这里,我们没有介绍如何向客户端发送数据包,这个非常的简单,只需要调用NetClient客户端对象的SocketChannel通道对象的write方法即可。它的执行实际是在游戏数据包被实例化出来之后由一个线程来执行的。实例化的过程就是byte数据转化成类属性变量。这部分内容我们在后面的章节中再详细介绍。

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

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

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

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

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

相关文章

Tomcat7部署war包getshell 漏洞复现

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 漏洞描述02 验证方式03 利用方式04 修复方案 01 漏洞描述 Tomcat支持在后台部署war文件&#xff0c;可以直接将webshell部署到web目录下。其中&#xff0c;欲访问后台&#xff0c;需要对应用户有相…

pycharm如何给一串中文快捷加引号(方法一)

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 门前冷落鞍马稀&#xff0c;老大嫁作商人妇。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python白银群【此类生物】问了一个Pycharm基础的问题&a…

Linux文件管理常用命令补充

&#xff08;该图由AI绘制 关注我 学习AI画图&#xff09; 目录 一、查看文件内容 more分屏显示文件内容&#xff08;了解&#xff09; less分屏显示文件内容&#xff08;重点&#xff09; 二、文件统计命令 1、wc命令 2、du命令 三、文件处理命令 1、find命令 2、gr…

unity相机视角平移一段距离

using System.Collections; using System.Collections.Generic; using UnityEngine;public class ControlCamera : MonoBehaviour {//相机视角焦点平移一段距离//需求 相机视角 内的3D对象A 在视角内平移到屏幕C点 public Vector3 InitCameraPos;// Start is called be…

代码随想录算法训练营第十一天 | 二叉树系列2

二叉树系列2 101 对称二叉树二叉树很重要的一点&#xff1a;确定遍历顺序关于递归代码随想录的代码我的代码(理解后编写) 100 相同的树我的代码 572 另一个树的子树我的代码录友的代码&#xff0c;只用递归&#xff01;&#xff01;&#xff01; 104 二叉树的最大深度重点代码随…

OA办公自动化系统哪个好?办公系统oa排名及对比

一、什么是OA办公自动化系统 OA&#xff08;Office Automation办公自动化&#xff09;是一种将智能化科技应用于企业管理中的应用系统。它可以通过电脑网络、互联网等技术手段&#xff0c;将企业的各种业务流程、各种业务数据进行集成和处理&#xff0c;将各种业务流程和各种业…

winfrom 利用反射 加载窗体(单例)

①新建一个项目, 程序集名称为: AssemblyForm (下面要用到的) ②新建一个Form窗体,窗体名称为: Form1 (下面也要用到), Form1里的代码: using System; using System.Windows.Forms;namespace AssemblyForm {public partial class Form1 : Form{public Form1(){InitializeCo…

web-html的基本用法

web前端代码基本用法 <html> <head><meta charset"utf-8"><!-- charset 属性规定 HTML 文档的字符编码。要是没有规定字符编码的话是有可能乱码的 -->待到秋来九月八&#xff08;head&#xff09;<!-- 头部就是直接写在最上面的文字&…

Css:高级技巧

1.精灵图使用 用ps的切片功能测量图片大小和位置 2.字体图标 3.CSS三角形 4.元素显示隐藏 5.鼠标样式 6.输入框input 轮廓线 7.防止文本域拖拽 8 vertical-align实现行内块和文字垂直居中对齐 9.单行文字溢出省略号显示 10.多行文字溢出省略号显示 11.布局技巧 1.相对定位会压…

实战|如何在Linux 系统上免费托管网站

动动发财的小手&#xff0c;点个赞吧&#xff01; Web 服务器可用于指代硬件和软件&#xff0c;或者两者一起工作。出于本指南的目的&#xff0c;我们将重点关注软件方面&#xff0c;并了解如何在 Linux 机器上托管网站。 Web 服务器是一种通过 HTTP/HTTPS 协议接收并响应客户端…

【算法集训之线性表篇】Day 03

文章目录 题目一思路分析代码实现效果 题目一 从有序顺序表中删除其值在给定值s和t之间(要求s<t)的所有元素&#xff0c;若s或t不合理或者线性表为空&#xff0c;则显示错误信息并退出运行。 思路分析 首先&#xff0c;题目给出数据结构为有序顺序表&#xff0c;故要查找…

B站创建视频分集播放列表

上传视频在B站上创建视频分集列表方法 上传时创建分集列表 1、打开B站 2、登录B站后&#xff0c;点击投稿上传视频。 3、上传视频或把视频直接拖拽到页面里&#xff1b; 4、点击上传第一个视频后页面下会出现一个号的按钮&#xff0c;点击继续上传 &#xff0c;上传视频就…

【Python】Selenium操作cookie实现免登录

文章目录 一、查看浏览器cookie二、获取cookie基本操作三、获取cookie并实现免登录四、封装成函数 一、查看浏览器cookie cookie、session、token的区别&#xff1a; cookie存储在浏览器本地客户端&#xff0c;发送的请求携带cookie时可以实现登录操作。session存放在服务器。…

MybatisPlus逆向生成实体类等

面试中遇到的一道机操题&#xff0c;so simple。 这是里SpringBoot项目&#xff0c;注意你的数据表中只有一个id字段是会生成失败的&#xff01; 添加maven依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter…

虚幻引擎程序化资源生成框架PCG 之 UPCGBlueprintElement源码笔记

UPCGBlueprintElement是PCGGraph中自定义节点的基类&#xff0c;但官方目前还没有给出详细的文档&#xff0c;所以从源代码里找点答案。 文章目录 可覆盖函数&#xff08;Override Functions&#xff09;Excute 和 Excute with ContextLoop Body函数和Loop函数Point Loop Body和…

SpringBoot+ Vue 家乡美食系统

&#x1f495;&#x1f495;作者&#xff1a;程序员徐师兄 个人简介&#xff1a;7 年大厂程序员经历&#xff0c;擅长Java、微信小程序、Python、Android等&#xff0c;大家有这一块的问题可以一起交流&#xff01; 各类成品java毕设 。javaweb&#xff0c;ssh&#xff0c;ssm&…

[期末网页作业]-精仿华为官网10个网页(html+css+js)

经过漫长的期末考试季节&#xff0c;我成功地完成了一个华为官网的仿写项目&#xff0c;并且非常高兴地与大家分享。这个项目包含了10个页面&#xff0c;每一个页面都经过了精心的设计和努力的填充。 首先&#xff0c;我注重了页面的整体布局与设计。借鉴了华为官网的风格&…

Unity3d-UGUI实现的贪食蛇小游戏

按鼠标WASD键来控制蛇的走向。 核心的代码如下&#xff1a; using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.UI;/// 《UGUI贪吃蛇》public class TCS2d : MonoBehaviour {public bool isOver false;public bool isStop…

【Docker 部署Minio】

Docker 部署Minio 一、拉取Minio镜像二、配置1、创建如下目录2、创建容器并运行 三、访问 一、拉取Minio镜像 访问Docker Hub镜像站找到自己需要的Minio镜像 运行以下命令 sudo docker pull minio/minio二、配置 1、创建如下目录 mkdir -p /home/zx/minio/config mkdir -p…

Java设计模式之一:建造者模式

目录 一、什么是建造者模式 二、建造者模式如何使用 三、建造者模式的优势和应用场景 一、什么是建造者模式 Java建造者模式是一种创建对象的设计模式&#xff0c;它通过将对象的构造过程分离出来&#xff0c;使得同样的构建过程可以创建不同的表示。建造者模式适用于创建复…