feat: 适配新版API更新
This commit is contained in:
54
ball-server/build.gradle
Normal file
54
ball-server/build.gradle
Normal file
@@ -0,0 +1,54 @@
|
||||
setArchivesBaseName("HamsterBall-Server")
|
||||
|
||||
evaluationDependsOn(':ball-common')
|
||||
|
||||
dependencies {
|
||||
apiShade project(":ball-common") transitive false
|
||||
shade "cn.hamster3.mc.plugin:core-common:${hamster_core_version}"
|
||||
|
||||
// // https://mvnrepository.com/artifact/org.slf4j/slf4j-api
|
||||
// implementation 'org.slf4j:slf4j-api:2.0.3'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
|
||||
implementationShade 'org.apache.logging.log4j:log4j-core:2.19.0'
|
||||
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
|
||||
implementationShade 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
|
||||
|
||||
// https://mvnrepository.com/artifact/io.netty/netty-all
|
||||
implementationShade 'io.netty:netty-all:4.1.86.Final'
|
||||
// https://mvnrepository.com/artifact/org.yaml/snakeyaml
|
||||
implementationShade 'org.yaml:snakeyaml:2.0'
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
//noinspection GradlePackageUpdate
|
||||
implementationShade 'com.google.code.gson:gson:2.8.9'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.compileJava.dependsOn(":ball-common:build")
|
||||
tasks.register("shadowJar", Jar) {
|
||||
dependsOn("jar")
|
||||
manifest.attributes('Main-Class': 'cn.hamster3.mc.plugin.ball.server.Bootstrap')
|
||||
manifest.attributes('ball-version': project.version)
|
||||
from([
|
||||
tasks.jar.outputs.files.collect {
|
||||
it.isDirectory() ? it : zipTree(it)
|
||||
},
|
||||
configurations.shade.collect {
|
||||
it.isDirectory() ? it : zipTree(it)
|
||||
},
|
||||
configurations.apiShade.collect {
|
||||
it.isDirectory() ? it : zipTree(it)
|
||||
},
|
||||
configurations.implementationShade.collect {
|
||||
it.isDirectory() ? it : zipTree(it)
|
||||
}
|
||||
])
|
||||
destinationDir(rootProject.buildDir)
|
||||
}
|
||||
tasks.build.dependsOn(shadowJar)
|
@@ -0,0 +1,77 @@
|
||||
package cn.hamster3.mc.plugin.ball.server;
|
||||
|
||||
import cn.hamster3.mc.plugin.ball.common.utils.OS;
|
||||
import cn.hamster3.mc.plugin.ball.server.command.CommandHandler;
|
||||
import cn.hamster3.mc.plugin.ball.server.config.ServerConfig;
|
||||
import cn.hamster3.mc.plugin.ball.server.connector.BallServerChannelInitializer;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public class Bootstrap {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Bootstrap");
|
||||
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
if (initDefaultFile()) {
|
||||
LOGGER.info("请重新启动该程序.");
|
||||
return;
|
||||
}
|
||||
ServerConfig.init();
|
||||
LOGGER.info("配置文件加载完成.");
|
||||
OS currentOS = OS.getCurrentOS();
|
||||
LOGGER.info("当前操作系统为: {}. 选择 IO 模式为: {}", currentOS.name(), currentOS.getIOModeName());
|
||||
EventLoopGroup loopGroup = currentOS.getEventLoopGroup(ServerConfig.getEventLoopThread());
|
||||
ServerBootstrap bootstrap = new ServerBootstrap()
|
||||
.group(loopGroup)
|
||||
.channel(currentOS.getServerSocketChannel())
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childHandler(new BallServerChannelInitializer());
|
||||
ChannelFuture future = bootstrap.bind(ServerConfig.getHost(), ServerConfig.getPort());
|
||||
future.await();
|
||||
if (future.isSuccess()) {
|
||||
LOGGER.info("进程信息: {}", ManagementFactory.getRuntimeMXBean().getName());
|
||||
LOGGER.info("服务器已启动. 输入 stop 来关闭该程序.");
|
||||
} else {
|
||||
LOGGER.error("仓鼠球服务器启动失败!", future.cause());
|
||||
loopGroup.shutdownGracefully();
|
||||
return;
|
||||
}
|
||||
CommandHandler.INSTANCE.start(loopGroup);
|
||||
}
|
||||
|
||||
private static boolean initDefaultFile() throws IOException {
|
||||
boolean saved = false;
|
||||
File log4jFile = new File("log4j2.xml");
|
||||
if (!log4jFile.exists()) {
|
||||
InputStream stream = ServerConfig.class.getResourceAsStream("/log4j2.xml");
|
||||
if (stream == null) {
|
||||
throw new IOException("log4j2.xml 文件损坏!");
|
||||
}
|
||||
Files.copy(stream, log4jFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
LOGGER.info("已生成默认 log4j2.xml 文件!");
|
||||
saved = true;
|
||||
}
|
||||
|
||||
File configFile = new File("config.yml");
|
||||
if (!configFile.exists()) {
|
||||
InputStream stream = ServerConfig.class.getResourceAsStream("/config.yml");
|
||||
if (stream == null) {
|
||||
throw new IOException("config.yml 文件损坏!");
|
||||
}
|
||||
Files.copy(stream, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
LOGGER.info("已生成默认 config.yml 文件!");
|
||||
saved = true;
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.command;
|
||||
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Scanner;
|
||||
|
||||
public class CommandHandler {
|
||||
public static final CommandHandler INSTANCE = new CommandHandler();
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("CommandHandler");
|
||||
private EventLoopGroup loopGroup;
|
||||
|
||||
private boolean started;
|
||||
|
||||
public void start(EventLoopGroup loopGroup) {
|
||||
this.loopGroup = loopGroup;
|
||||
|
||||
started = true;
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
LOGGER.info("命令执行器准备就绪. 输入 help 查看命令帮助.");
|
||||
while (started) {
|
||||
String command = scanner.nextLine();
|
||||
try {
|
||||
executeCommand(command);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("执行命令 " + command + " 时遇到了一个异常: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void executeCommand(String command) throws Exception {
|
||||
String[] args = command.split(" ");
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "?":
|
||||
case "help": {
|
||||
help();
|
||||
break;
|
||||
}
|
||||
case "end":
|
||||
case "stop": {
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LOGGER.info("未知指令. 请输入 help 查看帮助.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void help() {
|
||||
LOGGER.info("===============================================================");
|
||||
LOGGER.info("help - 查看帮助.");
|
||||
LOGGER.info("stop - 关闭该程序.");
|
||||
LOGGER.info("===============================================================");
|
||||
}
|
||||
|
||||
public void stop() throws Exception {
|
||||
started = false;
|
||||
LOGGER.info("准备关闭服务器...");
|
||||
loopGroup.shutdownGracefully().await();
|
||||
LOGGER.info("服务器已关闭!");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ServerConfig {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("ServerConfig");
|
||||
|
||||
private static String host;
|
||||
private static int port;
|
||||
private static int eventLoopThread;
|
||||
private static boolean enableAcceptList;
|
||||
private static List<String> acceptList;
|
||||
|
||||
private ServerConfig() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void init() throws IOException {
|
||||
File configFile = new File("config.yml");
|
||||
Map<String, Object> map;
|
||||
try (InputStream stream = Files.newInputStream(configFile.toPath())) {
|
||||
map = new Yaml().load(stream);
|
||||
}
|
||||
|
||||
host = map.getOrDefault("host", "localhost").toString();
|
||||
port = (int) map.getOrDefault("port", 58888);
|
||||
eventLoopThread = (int) map.getOrDefault("event-loop-thread", 5);
|
||||
enableAcceptList = (boolean) map.get("enable-accept-list");
|
||||
acceptList = (List<String>) map.get("accept-list");
|
||||
|
||||
LOGGER.info("host: {}", host);
|
||||
LOGGER.info("port: {}", port);
|
||||
LOGGER.info("eventLoopThread: {}", eventLoopThread);
|
||||
LOGGER.info("enableAcceptList: {}", enableAcceptList);
|
||||
LOGGER.info("acceptList: {}", acceptList);
|
||||
}
|
||||
|
||||
public static String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public static int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public static int getEventLoopThread() {
|
||||
return eventLoopThread;
|
||||
}
|
||||
|
||||
public static boolean isEnableAcceptList() {
|
||||
return enableAcceptList;
|
||||
}
|
||||
|
||||
public static List<String> getAcceptList() {
|
||||
return acceptList;
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.connector;
|
||||
|
||||
import cn.hamster3.mc.plugin.ball.common.data.BallMessageInfo;
|
||||
import cn.hamster3.mc.plugin.ball.server.constant.ConstantObjects;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public class BallServerChannelHandler extends SimpleChannelInboundHandler<String> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("BallServerChannelHandler");
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext context, String message) {
|
||||
if ("ping".equals(message)) {
|
||||
context.channel().writeAndFlush("pong");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BallMessageInfo messageInfo = ConstantObjects.GSON.fromJson(message, BallMessageInfo.class);
|
||||
LOGGER.info("从服务器 {} 上收到一条消息: {}", context.channel().remoteAddress(), messageInfo);
|
||||
BallServerChannelInitializer.broadcastMessage(messageInfo);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("处理消息 %s 时出现错误: ", message), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(@NotNull ChannelHandlerContext context) {
|
||||
LOGGER.info("与服务器 {} 建立了连接.", context.channel().remoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext context) {
|
||||
context.close();
|
||||
synchronized (BallServerChannelInitializer.CHANNELS) {
|
||||
BallServerChannelInitializer.CHANNELS.remove(context.channel());
|
||||
}
|
||||
LOGGER.warn("与服务器 {} 的连接已断开.", context.channel().remoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
|
||||
LOGGER.warn("与服务器 {} 通信时出现了一个错误: ", context.channel().remoteAddress(), cause);
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.connector;
|
||||
|
||||
import cn.hamster3.mc.plugin.ball.common.data.BallMessageInfo;
|
||||
import cn.hamster3.mc.plugin.ball.server.config.ServerConfig;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
|
||||
import io.netty.handler.codec.LengthFieldPrepender;
|
||||
import io.netty.handler.codec.string.StringDecoder;
|
||||
import io.netty.handler.codec.string.StringEncoder;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class BallServerChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||
public static final List<Channel> CHANNELS = new ArrayList<>();
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("BallServerChannelInitializer");
|
||||
|
||||
public static void broadcastMessage(BallMessageInfo messageInfo) {
|
||||
String string = messageInfo.toString();
|
||||
synchronized (CHANNELS) {
|
||||
for (Channel channel : CHANNELS) {
|
||||
channel.writeAndFlush(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(@NotNull SocketChannel channel) {
|
||||
InetSocketAddress remoteAddress = channel.remoteAddress();
|
||||
LOGGER.info("远程地址 {} 请求建立连接.", remoteAddress.toString());
|
||||
|
||||
String hostAddress = remoteAddress.getAddress().getHostAddress();
|
||||
if (ServerConfig.isEnableAcceptList() && !ServerConfig.getAcceptList().contains(hostAddress)) {
|
||||
LOGGER.warn("{} 不在白名单列表中, 断开连接!", hostAddress);
|
||||
|
||||
channel.pipeline()
|
||||
.addLast(new LengthFieldPrepender(8))
|
||||
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 8, 0, 8))
|
||||
.addLast(new StringDecoder(StandardCharsets.UTF_8))
|
||||
.addLast(new StringEncoder(StandardCharsets.UTF_8));
|
||||
|
||||
try {
|
||||
channel.writeAndFlush("connection refused").await();
|
||||
channel.disconnect().await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pipeline()
|
||||
.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS))
|
||||
.addLast(new BallServerKeepAliveHandler())
|
||||
.addLast(new LengthFieldPrepender(8))
|
||||
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 8, 0, 8))
|
||||
.addLast(new StringDecoder(StandardCharsets.UTF_8))
|
||||
.addLast(new StringEncoder(StandardCharsets.UTF_8))
|
||||
.addLast(new BallServerChannelHandler());
|
||||
|
||||
synchronized (CHANNELS) {
|
||||
CHANNELS.add(channel);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.connector;
|
||||
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleUserEventChannelHandler;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public class BallServerKeepAliveHandler extends SimpleUserEventChannelHandler<IdleStateEvent> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("BallServerKeepAliveHandler");
|
||||
|
||||
@Override
|
||||
protected void eventReceived(ChannelHandlerContext context, IdleStateEvent event) {
|
||||
synchronized (BallServerChannelInitializer.CHANNELS) {
|
||||
BallServerChannelInitializer.CHANNELS.remove(context.channel());
|
||||
}
|
||||
context.close();
|
||||
LOGGER.warn("由于无法验证连接存活,与服务器 {} 的连接已断开.", context.channel().remoteAddress());
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package cn.hamster3.mc.plugin.ball.server.constant;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
public interface ConstantObjects {
|
||||
/**
|
||||
* GSON 工具
|
||||
*/
|
||||
Gson GSON = new GsonBuilder().create();
|
||||
|
||||
}
|
17
ball-server/src/main/resources/config.yml
Normal file
17
ball-server/src/main/resources/config.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# 绑定网卡地址
|
||||
host: "0.0.0.0"
|
||||
# 绑定端口
|
||||
port: 58888
|
||||
|
||||
# 线程池数量
|
||||
# 建议设置为全服最大玩家数 / 20
|
||||
# 不建议低于 4
|
||||
event-loop-thread: 5
|
||||
|
||||
# 是否启用 IP 白名单
|
||||
enable-accept-list: true
|
||||
|
||||
# 允许连接至服务的 ip 名单
|
||||
accept-list:
|
||||
- "127.0.0.1"
|
||||
- "0:0:0:0:0:0:0:1"
|
@@ -0,0 +1,2 @@
|
||||
log4j2.loggerContextFactory=org.apache.logging.log4j.core.impl.Log4jContextFactory
|
||||
log4j.configurationFile=log4j2.xml
|
22
ball-server/src/main/resources/log4j2.xml
Normal file
22
ball-server/src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN" packages="com.mojang.util">
|
||||
<Appenders>
|
||||
<Console name="SysOut" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{HH:mm:ss}] [%logger/%level]: %msg%n"/>
|
||||
</Console>
|
||||
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout charset="UTF-8" pattern="[%d{HH:mm:ss}] [%logger/%level]: %msg%n"/>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<OnStartupTriggeringPolicy/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="1000"/>
|
||||
</RollingRandomAccessFile>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="SysOut" level="info"/>
|
||||
<AppenderRef ref="File"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
Reference in New Issue
Block a user