멀티 서버에 접속한 모든 사용자에게 사용자의 메세지를 전달하는 확성기 기능을 가진 마인크래프트 플러그인을 개발하게 되었다.
서버의 구조는 다음과 같이 구성돼있다.

보통 확성기 기능은 해당 서버의 broadcast 메서드를 통해 구현하는 듯 하지만, 여기선 사용자가 접속한 서버에서만 전체 전달이 된다.
어느 서버에 접속해있든, 확성기를 사용하면 모든 서버에 메세지가 전달되도록 하는 방법이 뭐가 없을까 싶어서 구글링을 해보니, 플러그인 메세지 채널이라는 기능을 알게 되었다.
이에 대해서 좀 더 알아보는 시간을 가져보도록 하겠다.
BungeeCord을 활용할 수도 있고, 개발자가 직접 채널을 새로 만들어서 통신할 수도 있음(namespace):(name) 형태를 갖추는 것을 추천ChannelIdentifier를 만들기 위해, 위와 같은 형태로 사용 중BungeeCord 채널은 내부적으로 bungeecord:main로 변환됨class PluginMessageChannel : JavaPlugin(), Listener {
private val BUNGEE_CORD = "BungeeCord"
override fun onEnable() {
// 해당 클래스를 이벤트 리스너로 등록
server.pluginManager.registerEvents(this, this)
// BungeeCord 채널로 메세지를 보낼 수 있도록 설정
server.messenger.registerOutgoingPluginChannel(this, BUNGEE_CORD)
}
// 플레이어가 점프를 하면, wild 서버로 이동시키도록 BungeeCord 채널로 메세지 전송
// BungeeCord 채널에서만 적용되는 프로토콜이 존재함으로, 이를 활용한 모습을 나타냄
@EventHandler
fun onPlayerJump(event: PlayerJumpEvent) {
val player = event.getPlayer()
val out = ByteStreams.newDataOutput()
out.writeUTF("Connect")
out.writeUTF("wild")
player.sendPluginMessage(this, BUNGEE_CORD, out.toByteArray())
}
override fun onDisable() {}
}
// 플러그인 메세지 응답을 조회하기 위해선, PluginMessageListener를 구현해야 한다.
class PluginMessageChannel : JavaPlugin(), PluginMessageListener {
private val BUNGEE_CORD = "BungeeCord"
override fun onEnable() {
server.pluginManager.registerEvents(this, this)
server.messenger.registerOutgoingPluginChannel(this, BUNGEE_CORD)
// 해당 클래스에서 BungeeCord 채널 메세지를 수신할 수 있도록 등록
server.messenger.registerIncomingPluginChannel(this, BUNGEE_CORD, this)
// 메세지를 전송하기 위해선 플레이어 객체가 필수이다.
// 해당 예제에선 ... 으로 편의상 대체한다.
val player = ...
val output = ByteStreams.newDataOutput()
out.writeUTF("PlayerCount")
out.writeUTF("lobby")
player.sendPluginMessage(this, BUNGEE_CORD, output.toByteArray())
}
// PluginMessageListener 구현부
// 해당 메서드에서 채널에 대한 메세지를 조회한다.
override fun onPluginMessageReceived(channel: String, player: Player, message: ByteArray) {
if (channel != BUNGEE_CORD) {
return
}
val input = ByteStreams.newDataInput(message)
val subchannel = input.readUTF()
if (subchannel == "PlayerCount") {
val server = input.readUTF()
val playerCount = input.readInt()
}
}
override fun onDisable() {}
}
broadcast:main 채널명을 갖도록 진행@Plugin(
id = "broadcast",
name = "broadcast",
version = BuildConstants.VERSION
)
class Broadcast @Inject constructor(
private val logger: Logger,
private val proxy: ProxyServer
) {
// broadcast:main 채널 정보를 담은 객체
private val channel: ChannelIdentifier = MinecraftChannelIdentifier.create("broadcast", "main")
@Subscribe
fun onProxyInitialize(event: ProxyInitializeEvent) {
proxy.channelRegistrar.register(channel)
logger.info("Shout channel registered")
}
// 플러그인 채널 메세지 수신 리스너
@Subscribe
fun onPluginMessage(event: PluginMessageEvent) {
// broadcast:main 채널이 아니라면 무시
if (event.identifier != channel)
return
try {
val dataStream = DataInputStream(ByteArrayInputStream(event.data))
val message = dataStream.readUTF()
val broadcast = Component.text("[확성기] $message", NamedTextColor.LIGHT_PURPLE)
// 프록시에 연결된 모든 서버에 접속한 유저들에게 메세지 전송
proxy.allPlayers.forEach { it.sendMessage(broadcast) }
logger.info("Broadcasted shout: $message")
} catch (e: Exception) {
logger.error("메시지 수신 중 오류 발생", e)
}
}
}
class BroadcastCommand(val plugin: JavaPlugin): CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
if (sender !is Player) {
sender.sendMessage("[알림] 플레이어만 사용할 수 있습니다.")
return true
}
if (args.isEmpty()) {
sender.sendMessage("[알림] 사용법: /확성기 <메시지>")
return true
}
val message: String? = args.joinToString(" ")
// 플러그인 메시지 전송
try {
val output = ByteArrayOutputStream()
val data = DataOutputStream(output)
data.writeUTF(sender.name + ": " + message) // 메시지 내용
sender.sendPluginMessage(plugin, "broadcast:main", output.toByteArray())
sender.sendMessage("[알림] 확성기 메시지를 전송했습니다!")
} catch (e: IOException) {
sender.sendMessage("[알림] 전송 실패: " + e.message)
}
return true
}
}

public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
Player player = ...;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("MessageRaw");
out.writeUTF("ALL");
out.writeUTF(GsonComponentSerializer.gson().serialize(
Component.text("Click Me!").clickEvent(ClickEvent.openUrl("https://papermc.io"))
));
player.sendPluginMessage(this, "BungeeCord", out.toByteArray());
}
}
플러그인 메세지 채널을 활용하여 프록시 서버와 백엔드 서버간 통신이 가능하다.