自定义封包很常用的场景就是跳出客户端原有设计,进行数据通讯。
比如 全队的 Boss 伤害统计,你可以用自定义封包把其他人的伤害发送给玩家,在客户端上实时显示。
比如 你把一个专属的签到网址发送给玩家的客户端,让客户端跳转到签到网站。
比如 。。。 充分发挥你的想象力
本教程以北斗服务端 + 北斗 Ijl15 插件为基础进行制作,如果你使用其他端或插件请自行适当调整里面的调用方法。
现在教大家如何制作自定义的封包
请记住以下概念:每个封包都有封包 ID 这是所有封包的头
首先是服务端部分
我们要先找到定义封包 ID 的 Enum,在服务端中分别是 SendOpcode.java 和 RecvOpcode.java,只需要在 idea 中双击 shift 就很容易找到这两个文件了。
我们先以发封包为例
打开 SendOpcode.java
拉到最下面
添加新的 ENUM,格式为 名称(id), 其中id保持格式统一用十六进制,并且不要和前面的重复,为了防止客户端存在这个封包 ID 但是服务端没有收录的情况,建议把 id 值设置高一点,但是不要超过 short 类型的上限。你可以像我一样从 0x1000 开始
如图我们以 UPDATE_HPMPAALERT 为例(1. 这是北斗添加的自定义封包;2. 记得要在分号前面添加)

添加完封包 ID 以后,我们要添加一个用于创建封包的方法,通常这类方法保存在 PacketCreator.java 中(实际上这个文件太长了,anyway 你可以自行安排放在哪,记得住即可)。

首先是初始化一个封包对象,初始化时给的参数 SendOpcode.UPDATE_HPMPAALERT 就是前面新添加的封包 ID
代码登录后可见
然后就是开始插入封包数据了。在这里例子里,我们插入了两个 byte 值,分别表示 hp 和 mp 的百分比。
实际上这个封包的内容就是:一个ID(两个byte) + 一个 byte + 一个 byte,总共 4 个 byte 而已。在我们读取封包的时候需要自己根据这里的结构赋予这些 byte 意义。
除了 writeByte 方法外,OutPacket 还提供了如下方法供我们使用
writeByte(byte value);
writeByte(int value);
writeBytes(byte[] value);
writeShort(int value);
writeInt(int value);
writeLong(long value);
writeBool(boolean value);
writeString(String value);
writeFixedString(String value);
writePos(Point value);
skip(int numberOfBytes);
写好创建封包的函数以后,我们要发送封包的话只需要调用发送函数和这个创建函数即可。
由于发送封包的方法有很多,不同的方法有不同的使用场景,有给全地图的玩家发送,有给全服务器的玩家发送,也有只给当前客户端发送的,我就不一一举例了,这里仅以给当前客户端发送为例。
调用方法如下,其中 client 为 Client 对象
代码登录后可见
合起来就是
代码登录后可见
执行这个方法就可以把这个封包发送给客户端了。
现在讲讲服务端是如何收取封包的
先注册封包 ID,打开 RecvOpcode.java,添加封包 ID,添加方法和 SendOpcode 一样,但是这里的 ID 是客户端那边发送过来的,和前面 SendOpcode 中注册的不一定相同。当然,自定义封包嘛,客户端我们说了算,我们也可以设置成相同的。
然后创建一个处理接收到的封包的方法,我们先在 net/server/channel/handlers 下新建一个类
比如 SetHpMpAlertHandler.java

这个类继承自 AbstractPacketHandler
我们重载 handlePacket 方法,参数是固定的 InPacket p, Client c,然后读取这个封包
由于封包的 ID 已经在 handler 之前被解析掉了,所以这里直接读取 ID 之后的数据就可以了
读取数据的方法有这些
代码登录后可见
我们执行 byte data = p.readByte(); 就可以读取一个字节存放到 data 变量中了
在这个例子中,我在客户端构造的封包和服务端构造的封包是一模一样的,所以直接拿前面构造的用于发送给客户端的封包结构为参考。
由于封包的ID已经被解析过了,所以封包内容只剩下两个字节,第一个字节是hp百分比,第二个字节是mp百分比,所以第一次p.readByte() 读出来的数据要用于设置hp,第二次读出来的数据要用于mp。这个顺序很重要不能乱。
由于这个方法的逻辑很简单,所以不需要创建变量来存放封包的数据,而是直接调用方法获取数据使用了。
光有这个类还不够,我们还要把这个类注册到拦截器中
打开 PacketProcessor.java
如果你的封包是玩家登录以后使用的,就在 registerChannelHandlers 方法下面添加
代码登录后可见
注册以后,拦截器就会把接收到的 ID 为 RecvOpcode.SET_HPMPALERT 的封包转发给 SetHpMpAlertHandler 的 handlePacket 方法。
这样服务端就创建了一组收发封包的方法,接下来讲讲
客户端是如何收发封包的
文件准备
下载附件 解压密码为
代码登录后可见
将所有文件(不含文件夹)复制到插件的 ezorsia 目录下,并在 vs 中将新增的文件包含到项目中
编辑 Memory.h 添加
代码登录后可见
编辑 Memory.cpp 添加
代码登录后可见
在 vs 项目中 右键 Header Files —— 添加 —— 新建项 —— 创建 Packet.h
代码登录后可见
在 vs 项目中 右键 Source Files —— 添加 —— 新建项 —— 创建 Packet.cpp
代码登录后可见
在头部的 #include 下面插入
代码登录后可见
查找
代码登录后可见
在这个方法内添加
代码登录后可见
至此文件准备完毕。
接收并处理服务器发过来的封包
回到 Packet.cpp
在准备添加的注释处开始处处理接收的封包
首先我们要复制一个封包对象,避免我们的操作影响原来的封包。
代码登录后可见
接下来我们的操作都针对 in 进行就可以了
然后我们要解析封包的 id,封包 id 是一个 short 两个字节
代码登录后可见
注意解码封包的方法都是decode,<>内的类型为你需要解码的类型,读2个字节就是short,4个字节就是int,读字符串就是std::string
然后根据封包的 id 将封包转发给相应的处理方法
代码登录后可见
注意:凡是自定义封包,处理完要用 return 0 直接返回;如果是客户端自带的方法,你想在处理完数据后继续走原有的客户端处理封包的流程,那就用 break 跳出 switch 执行之后的代码
最后是让原封包 in_pack 回到客户端原本的执行流程去
代码登录后可见
最终结果

具体的方法 比如 SetServerAlert 就很简单了
代码登录后可见
其实跟服务端一样的,拿到封包以后,由于前面 switch 前已经解析过 id 了,所以封包只剩下两个字节,一个字节是HP的百分比,一个字节是MP的百分比
直接解析出来使用就好了。(c++原生没有 byte 类型,char 就代表一个字节)
客户端发封包
代码登录后可见
其实跟服务端一样,先写一个创建封包的方法
初始化一个封包对象,参数为封包 id
代码登录后可见
然后写入数据,支持的方法有
代码登录后可见
最后执行发送命令
代码登录后可见