Minecraft(我的世界)中文论坛

 找回密码
 注册(register)
查看: 3983|回复: 12

[其它开发教程] [Tutor][PCD]Forge与Bukkit|Sponge之间的通信|数据传输——Messenger类

[复制链接]
发表于 2017-8-28 20:26:00 | 显示全部楼层 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 754503921 于 2018-7-25 18:15 编辑

Forge | LiteLoader 与 Bukkit / Sponge 之间的通信
—— PluginMessengeChannel 与 FMLNetworkEvent
在实际开发中,如果你有一些天才的设想,比如借助 Forge 让你的插件服务器变得更加有特色,但是苦于无法进行数据传输的话,那么现在你就可以学习如何让 Bukkit / Sponge 和 Forge 之间传输信息了。
此教程适用于 1.7.10-1.12 (已测试),1.7.10就把包名改成 cpw 那个就行,Bukkit的插件全版本通用。

概述
Bukkit / Sponge 与 Forge 通信的原理为:
服务端发送 PluginMessage 到 Forge 客户端,客户端使用 FMLNetworkEvent.ClientCustomPacketEvent 接受处理信息。
Forge 使用 FMLEventChannel 将 FMLNetworkPacket 发送至 Bukkit,Bukkit 服务器使用 PluginMessageListener 接受处理消息,Sponge 使用注册的频道添加的监听器 RawDataListener 处理消息。
教程使用的包名为 com.ilummc.msgtutor,主类为 MessageMain。

Bukkit 接收消息部分

不知道你在查阅 Bukkit 的 Javadocs 时有没有注意到这样一个包 org.bukkit.plugin.messaging,这就是用于通信的包。
首先你需要一个实现了 PluginMessageListener 的类,本教程我们将其命名为 MessageListener:
package com.ilummc.msgtutor;

import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;

public class MessageListener implements PluginMessageListener {

        @Override
        public void onPluginMessageReceived(String channel, Player player, byte[] data) {

        }
}
自动补全的方法 onPluginMessageReceived 为接收到消息时调用的方法,channel 为通道名称,data 为具体的数据内容。
(2018/7/25 补充) 自 1.13 后,bukkit 对 channel 的名称做出了限制,需要使用 namespace:name 的格式,比如 fmltutor:fmltutor
接着,你需要注册消息输入和输出的通道,通过 Messenger 类的 registerIncomingPluginChannelregisterOutgoingPluginChannel 方法完成,代码如下:
package com.ilummc.msgtutor;

import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

public class MessageMain extends JavaPlugin {

        @Override
        public void onEnable() {
                // 注册消息接受通道
                Bukkit.getMessenger().registerIncomingPluginChannel(this, "msgtutor", new MessageListener());
                // 注册消息发送通道
                Bukkit.getMessenger().registerOutgoingPluginChannel(this, "msgtutor");
        }
}
registerIncomingPluginChannel 注册了接受消息的通道,和使用的 PluginMessageListener 实例,registerOutgoingPluginChannel 则注册了发送消息使用的通道。
到此你完成了Bukkit 接收消息的部分。

Forge 客户端接收消息部分

我们需要先注册一个通道,使用 NetworkRegistry 类的 newEventDrivenChannel 方法:
package com.ilummc.msgtutor;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.network.FMLEventChannel;
import net.minecraftforge.fml.common.network.FMLNetworkEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;

@SuppressWarnings("all")
@Mod(modid="msgtutor", version="tutor", name="MessageTutor")
public class MessageMain {
        static FMLEventChannel channel;
        
        @EventHandler
        public void preload(FMLPreInitializationEvent evt) {
                // 注册事件
                MinecraftForge.EVENT_BUS.register(this);
                FMLCommonHandler.instance().bus().register(this);
                // 注册通道
                channel = NetworkRegistry.INSTANCE.newEventDrivenChannel("msgtutor");
                channel.register(this);
        }
}
接着,我们添加一个监听器,监听 FMLNetworkEvent.ClientCustomPacketEvent 事件:
        @SubscribeEvent
        public void onClientPacket(FMLNetworkEvent.ClientCustomPacketEvent evt) {
                FMLLog.getLogger().info(new String(evt.getPacket().payload().array()));
        }
此事件的 getPacket 方法可以获得一个 FMLProxyPacket 实例数据包,这个实例的方法 payload 可以获得数据包携带的内容 ByteBuf,而 ByteBuf 实例的方法 array 则可以得到 byte[] 类型的数据。
(2018/7/8 补充) 在 forge 的 1.12.2 以后的版本,该事件的 ByteBuf 变成了一个 netty 魔性优化的实例,导致性能的上升以及 array() 方法的失效,你需要手动 new 一个数组然后用 readBytes 来读数据。
到此,你就可以接收来自服务器的消息了。

Sponge 接收消息部分

Sponge.getChannelRegistrar() 方法返回 message channel 的注册器,然后通过其 createRawChannel 方法注册一个新的 channel。
通过 addListener 添加新的监听器。
package com.ilummc.msgtutor;
// 省略导入
@Plugin(id = "msgtutor",
        name = "MessageTutor",
        version = "1.0-SNAPSHOT",
        authors = {"IzzelAliz"})
public class ServerGui {

    private static ChannelBinding.RawDataChannel channel;

    @Listener
    public void onServerStart(GameStartedServerEvent event) {
        // 注册频道
        channel = Sponge.getChannelRegistrar().createRawChannel(this, "msgtutor");
        // 添加监听器
        // PlatformType 指定监听来自哪里的信息,我们监听的是客户端,所以使用 CLIENT

        channel.addListener(Platform.Type.CLIENT, (data, connection, side) -> {
            // 将连接类型转换为 PlayerConnection
            if (connection instanceof PlayerConnection) {
                PlayerConnection conn = (PlayerConnection) connection;
                // 示例给玩家发送消息
                conn.getPlayer().sendMessage(Text.of(new String(data.array())));
            }
        });
    }
}

发送消息
Bukkit 发送消息给客户端的方法为
Bukkit.getPlayer("Izzel_Aliz").sendPluginMessage(Plugin plugin, String channel, byte[] data);
Forge 发送消息给服务器的方法为
byte[] array = ...; // 你要发送的消息的 byte 数组
ByteBuf buf = Unpooled.wrappedBuffer(array);
FMLProxyPacket packet = new FMLProxyPacket(new PacketBuffer(buf), "msgtutor"); // 数据包
channel  // FMLEventChannel 实例
    .sendToServer(packet);
Sponge 发送消息给客户端的方法为
ChannelBinding.RawDataChannel channel = ... ; // 你注册的 channel 实例
Player player = .... ; // 目标玩家
channel.sendTo(player, channelBuf -> channelBuf.writeByteArray("发送的消息").getBytes());
// ChannelBuf 有大量方法,可以写入读取不同种类的数据,使用与 ByteBuf 类似
你可以发送任何东西,只要能将其作为 byte 数组发送。byte 数组的长度限制为 32766 字节。

LiteLoader 接受/发送
请移步 @ustc_zzzz 大佬的帖子的章节 与服务端插件交互 http://www.mcbbs.net/thread-659755-1-1.html
关于线程安全

以上接收时的事件全部是在网络线程被触发,所以对于线程不安全的Minecraft来说,线程安全问题需要额外注意。
由于本人对 Forge 的操作并不是很熟练,所以只能以 Bukkit 作为例子,如果你接收到的信息只是一条字符串,并且你只是想将其发送给玩家(Player#sendMessage),那么你可以随意在网络线程中使用,因为这个方法是线程安全的;但是,如果你需要进行踢出(Player#kickPlayer),那么你必须在主线程(Server Thread)进行这个操作,否则可能得到一个报错、崩溃或者意想不到的结果(尽管 Bukkit 会阻止 Async Kick 的行为并发出警告)。
那么,我们可以用以下的方法将数据转交给主线程处理:
  • 使用 Bukkit 或者 Sponge 的调度器

  • 利用管道IO

ByteBuf 的简单使用

ByteBuf 类被提供于 io.netty 包中,使用可以通过 Maven 导入
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-buffer</artifactId>
    <version>4.1.20.Final</version>
</dependency>
使用 Gradle 导入
compile group: 'io.netty', name: 'netty-buffer', version: '4.1.20.Final'
ByteBuf 可以说是 java.nio.ByteBuffer 类的加强,主要有以下优点:
  • 读写指针分离,不用调用 flip() 方法来切换读写状态
  • 写入时可以自动增加容量
  • 提供了 Unpooled 和 Pooled 两种用于不同的场景

创建一个 ByteBuf 的方法很多,比如
Unpooled.buffer() // 创建一个普通的 ByteBuf
Unpooled.wrappedBuffer(byte[]) // 从已有 byte 数组创建
Pooled.buffer() // 创建一个高并发优化的 ByteBuf
Unpooled 和 Pooled 类重载的方法还有很多,详情可以查阅 Javadocs
另及:Minecraft 还叫不上高并发,Pooled 的高并发优化对于 Minecraft 大概没啥用

写入/读取
ByteBuf buf = ... ;
buf.writeInt(int); // 写入 int 型数据
buf.writeBytes(byte[], int, int); // 写入 byte 数组,后两个参数分别是 offset 和 length
buf.readLong(); // 返回一个 long 型数据
buf.toString(int, int, Charset); // 将 ByteBuf 内部的 byte[] 转换为 String 型数据,前两个参数分别是 offset 和 length
重载的方法还有很多,基本什么数据都能往里写,这里介绍一些实用的示例。

ByteBuf 里的两个读写指针,分别可以通过 readerIndex 和 writerIndex 方法获得
ByteBuf 内部维护了一个 byte 数组,其中 ByteBuf 的 capacity 为数组长度,可以通过 getCapacity 方法获得
0 <= readerIndex <= writerIndex <= capacity
  • 0 到 readerIndex 之间的数组区域称为 discardable bytes,discardReadBytes 方法可以将数组从 readerIndex 之后的部分移动到 0,从而增大可用区。
  • readerIndex 到 writerIndex 之间的数组区域称为 readable bytes,这一部分可以进行读取,每次读取之后,readerIndex 都会相应增加(增加数据长度,如 readLong 就会增加 Long.BYTES)。
  • writerIndex 到 capacity 之间的数组区域称为 writable bytes,这一部分可以写入,每次写入之后,writerIndex 都会相应增加(如 writeShort 就会增加 Short.BYTES)。如果写入的长度大于 capacity - writerIndex,则自动扩容。


后记 & 常用链接 & 补充阅读

Forge的通信 http://www.mcbbs.net/thread-711966-1-1.html
Spigot Javadocs https://hub.spigotmc.org/javadocs/spigot/
Sponge Javadocs https://jd.spongepowered.org/5.1.0/?overview-summary.html
Forge 1.7.10的Javadocs http://jd.ddmcloud.com/forge/1.7.10/
Netty Javadocs http://netty.io/4.1/api/index.html
ByteBuf http://blog.csdn.net/z69183787/article/details/52980426
http://www.2cto.com/kf/201604/500997.html
线程通信 http://www.cnblogs.com/hapjin/p/5492619.html
你可以用你的天才般的设想,让插件服务器玩起来像 Mod 一样,一切都是有可能的
我是一名 Bukkit 插件开发者,碰巧会一丁点的 Forge 开发,甚至刚学了一点 Sponge 开发。
使用 Forge 1.8.9 作为示范。


来自群组: PluginsCDTribe

评分

参与人数 10人气 +18 金粒 +125 贡献 +3 收起 理由
GiNYAi + 2 MCBBS有你更精彩~
Ynglife + 1 感谢方法!
Mangocraft + 1 Ssssssssssssssssssss
ustc_zzzz + 5 + 60 + 3 神乎其技,不服不行!
清晨w + 1 + 20 神乎其技,不服不行!
V乐乐 + 1 + 15 !!~~~太赞
1582952890 + 3 + 25 MCBBS有你更精彩~
qzz740827 + 2 神乎其技,不服不行!
耗子 + 2 MCBBS有你更精彩~
mimimis + 5 以后做个插件MOD联动的

查看全部评分

发表于 2017-9-6 10:47:28 | 显示全部楼层
支持技术!!!!
回复

使用道具 举报

发表于 2017-9-7 11:14:59 | 显示全部楼层
支持一下 学习一下
回复

使用道具 举报

发表于 2017-9-7 20:06:33 | 显示全部楼层
哇,这样子的话以后的插件和mod是不是可以联合在一起去实现一些功能了!
回复

使用道具 举报

发表于 2017-9-8 12:01:27 | 显示全部楼层
支持下大佬!感谢分享。。。终于有一篇系统地讲述Forge和Bukkit通过Plm进行通讯的教程了!
为什么不早点写出来呢??回忆起我当初请教了N个大佬才勉强整会,当时要是有这个教程就好了

回复

使用道具 举报

 楼主| 发表于 2017-9-8 12:48:51 | 显示全部楼层
1582952890 发表于 2017-9-8 12:01
支持下大佬!感谢分享。。。终于有一篇系统地讲述Forge和Bukkit通过Plm进行通讯的教程了!
为什么不早点 ...

早点。。
我也想写,但是感觉没啥人想要呢
其实是之前没想过来编程版写教程,而且论坛有一个验证机器码的插件来着,可以用那个学习的
回复

使用道具 举报

发表于 2017-10-10 10:37:38 | 显示全部楼层
问题是,在spigot的服务端里。能往独立的MOD里发消息吗?
回复

使用道具 举报

发表于 2017-10-10 10:51:41 | 显示全部楼层
li709854423 发表于 2017-10-10 10:37
问题是,在spigot的服务端里。能往独立的MOD里发消息吗?

Spigot 服务器插件 -> Forge 客户端的 Mod 是可以的。
Forge 客户端的 Mod -> Spigot 服务器插件也是可以的。
甚至 Spigot 服务器插件 <-> LiteLoader 客户端 Mod也是可以的。(理论上是这样。我没有试过。)

但是如果你是说 Spigot 服务器插件 -> Forge 服务器的 Mod.. 等等你是怎么把 MinecraftForge 装到 Spigot 服务器端上的
回复

使用道具 举报

发表于 2017-10-10 11:07:23 | 显示全部楼层
u.s.knowledge 发表于 2017-10-10 10:51
Spigot 服务器插件 -> Forge 客户端的 Mod 是可以的。
Forge 客户端的 Mod -> Spigot 服务器插件也是可以 ...

我的意思就是独立MOD。。独立MOD的意思就是不存在服务端。。。
也就说你应该是解决了我的一个误区。。我一直以为spigot服务端是无法往客户端MOD发消息的。。现在看来大有可为了
回复

使用道具 举报

发表于 2017-10-10 11:47:20 | 显示全部楼层
li709854423 发表于 2017-10-10 11:07
我的意思就是独立MOD。。独立MOD的意思就是不存在服务端。。。
也就说你应该是解决了我的一个误区。。我 ...

我从未听说过有这样使用“独立Mod”这个词的。
独立Mod这个词通常指不需要任何前置(不含library mod)的Mod,比如像IC2、暮色这样的就可以用独立Mod这个词来描述,但是IC2高级机器这个Mod需要IC2作为前置,就不能称之为独立Mod了。

我一直以为spigot服务端是无法往客户端MOD发消息的。。


这篇文章讲述的就是 Bukkit 服务器插件与 MinecraftForge 客户端 Mod 之间如何通信。Spigot 仍然是 Bukkit
的超集
。难道 Spigot 摆脱了Bukkit API?
回复

使用道具 举报

发表于 2017-10-10 18:37:37 | 显示全部楼层
u.s.knowledge 发表于 2017-10-10 11:47
我从未听说过有这样使用“独立Mod”这个词的。
独立Mod这个词通常指不需要任何前置(不含library mod)的 ...

事实上我之前一直用的在KC端进行插件控制MOD发包。当时我以为是因为KC端才允许这样..也没去spigot试过仔细想想在通道一致地址一致的情况下。。肯定是可以互相收包发包的
至于独立MOD。。个人理解or误解的问题就没必要讨论了
回复

使用道具 举报

发表于 2018-2-4 20:57:28 | 显示全部楼层
        神乎其技,不服不行! 这个教程很易懂
回复

使用道具 举报

发表于 2018-7-10 05:13:18 | 显示全部楼层
非常感谢,刚好脱离了1.7.10的bukkit,正愁该怎么翻Sponge的文档就看到了这个。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 手机版本

GMT+8, 2018-10-21 18:28 , Processed in 0.127373 second(s), 7 queries , Memcache On.

"Minecraft"以及"我的世界"为Mojang Synergies AB的商标。本站与Mojang以及微软公司没有从属关系。

© 2010-2017 我的世界中文论坛 版权所有。本站原创图文内容版权属于原创作者,未经许可不得转载。

快速回复 返回顶部 返回列表