Minimal working client-server

No cleanup logic yet
This commit is contained in:
s809
2023-06-28 22:46:49 +05:00
parent aee752a112
commit 2358797a82
12 changed files with 189 additions and 117 deletions
@@ -77,8 +77,10 @@ public class ChunkToLodBuilder implements AutoCloseable
{
return;
}
else if (!MC.playerExists())
else if (MC == null || !MC.playerExists())
{
// TODO handle server side properly
// MC hasn't finished loading (or is currently unloaded)
// can be uncommented if tasks aren't being cleared correctly
@@ -1,24 +0,0 @@
package com.seibel.distanthorizons.core.network;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
//import net.minecraft.client.Minecraft;
public class LodClient {
// public static final LodClient INSTANCE = new LodClient();
//
// private final Minecraft client;
// private final NetworkHandler networkHandler;
//
// private LodClient() {
// client = Minecraft.getInstance();
// this.networkHandler = new NetworkHandler();
// }
public void connect() {
}
public void disconnect() {
}
}
@@ -1,66 +0,0 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.network.protocol.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.jetbrains.annotations.NotNull;
public class LodServer {
// TODO move to config of some sort
static final int PORT = 25049;
public LodServer(/* initial settings */) {
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(getInitializer());
b.bind(PORT).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public void stop() {
}
private ChannelInitializer<SocketChannel> getInitializer() {
return new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(@NotNull SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
MessageRegistry messageRegistry = new MessageRegistry();
// Encoding
pipeline.addLast(new LengthFieldBasedFrameDecoder(Short.MAX_VALUE, 0, Short.BYTES, 0, Short.BYTES));
pipeline.addLast(new MessageDecoder(messageRegistry));
// Encoder
pipeline.addLast(new LengthFieldPrepender(Short.BYTES));
pipeline.addLast(new MessageEncoder(messageRegistry));
pipeline.addLast(new MessageHandler(MessageHandlerSide.SERVER));
}
};
}
}
@@ -0,0 +1,77 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.util.NetworkUtil;
import com.seibel.distanthorizons.core.network.protocol.MessageHandlerSide;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class NetworkClient implements Closeable {
private enum State {
OPEN,
RECONNECT,
CLOSED
}
private static final int FAILURE_RECONNECT_DELAY_SEC = 5;
// TODO move to config of some sort
private final String host;
private final int port;
private State state = State.OPEN;
private final Bootstrap clientBootstrap;
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
public NetworkClient(String host, int port) {
this.host = host;
this.port = port;
clientBootstrap = new Bootstrap()
.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(NetworkUtil.getChannelInitializer(MessageHandlerSide.CLIENT));
connect();
}
private void connect() {
ChannelFuture connectFuture = clientBootstrap.connect(host, port);
connectFuture.addListener((ChannelFuture channelFuture) -> {
if (!channelFuture.isSuccess()) return;
channel = channelFuture.channel();
channel.writeAndFlush(new HelloMessage());
});
channel = connectFuture.channel();
channel.closeFuture().addListener((ChannelFuture channelFuture) -> {
if (state == State.CLOSED) return;
workerGroup.schedule(this::connect, state == State.RECONNECT ? 0 : FAILURE_RECONNECT_DELAY_SEC, TimeUnit.SECONDS);
state = State.OPEN;
});
}
/** Kills the current connection, triggering auto-reconnection immediately. */
public void reconnect() {
state = State.RECONNECT;
channel.disconnect();
}
@Override
public void close() throws IOException {
state = State.CLOSED;
workerGroup.shutdownGracefully();
}
}
@@ -0,0 +1,45 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.network.protocol.*;
import com.seibel.distanthorizons.core.util.NetworkUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.io.Closeable;
import java.io.IOException;
public class NetworkServer implements Closeable {
// TODO move to config of some sort
private final int port;
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
public NetworkServer(int port) {
this.port = port;
ServerBootstrap b = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(NetworkUtil.getChannelInitializer(MessageHandlerSide.SERVER));
b.bind(port)
.addListener((ChannelFuture channelFuture) -> {
if (!channelFuture.isSuccess())
throw new RuntimeException("Failed to bind: " + channelFuture);
})
.channel().closeFuture().addListener(future -> close());
}
@Override
public void close() throws IOException {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
@@ -1,12 +1,17 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
// This message is critical to maintain backwards compatibility
// as it's used to receive version before everything else.
public class HelloMessage extends Message {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public int version = ModInfo.PROTOCOL_VERSION;
@Override
@@ -21,6 +26,11 @@ public class HelloMessage extends Message {
@Override
public void handle_Server(ChannelHandlerContext ctx) {
// TODO Adjust message handling to client's version
LOGGER.log(Level.INFO, "Client version: " + version);
}
@Override
public void handle_Client(ChannelHandlerContext ctx) {
LOGGER.log(Level.INFO, "Server version: " + version);
}
}
@@ -8,15 +8,9 @@ import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MessageDecoder extends ByteToMessageDecoder {
private final MessageRegistry messageRegistry;
public MessageDecoder(MessageRegistry messageRegistry) {
this.messageRegistry = messageRegistry;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
Message message = messageRegistry.createMessage(in.readShort());
Message message = MessageRegistry.INSTANCE.createMessage(in.readShort());
message.decode(ctx, in);
out.add(message);
}
@@ -6,15 +6,9 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MessageEncoder extends MessageToByteEncoder<Message> {
private final MessageRegistry messageRegistry;
public MessageEncoder(MessageRegistry messageRegistry) {
this.messageRegistry = messageRegistry;
}
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws IllegalArgumentException {
out.writeShort(messageRegistry.getMessageId(msg));
out.writeShort(MessageRegistry.INSTANCE.getMessageId(msg));
msg.encode(ctx, out);
}
}
@@ -6,24 +6,27 @@ import com.seibel.distanthorizons.core.network.messages.Message;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class MessageRegistry {
private final Map<Integer, Supplier<Message>> idToConstructor = new HashMap<Integer, Supplier<Message>>() {{
public static final MessageRegistry INSTANCE = new MessageRegistry() {{
// Note: Make sure IDs are unique
// Also note: Removing IDs will break backwards compatibility
put(1, HelloMessage::new);
registerMessage(1, HelloMessage.class, HelloMessage::new);
}};
private final Map<Class<?>, Integer> classToId = idToConstructor.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getValue().getClass(),
Map.Entry::getKey
));
private final Map<Integer, Supplier<Message>> idToSupplier = new HashMap<>();
private final Map<Class<?>, Integer> classToId = new HashMap<>();
private MessageRegistry() { }
public <T extends Message> void registerMessage(int id, Class<T> clazz, Supplier<T> supplier) {
idToSupplier.put(id, (Supplier<Message>) supplier);
classToId.put(clazz, id);
}
public Message createMessage(int id) throws IllegalArgumentException {
try {
return idToConstructor.get(id).get();
return idToSupplier.get(id).get();
} catch (NullPointerException e) {
throw new IllegalArgumentException("Invalid message ID");
}
@@ -0,0 +1,30 @@
package com.seibel.distanthorizons.core.util;
import com.seibel.distanthorizons.core.network.protocol.*;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
public class NetworkUtil {
public static ChannelInitializer<SocketChannel> getChannelInitializer(MessageHandlerSide side) {
return new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(@NotNull SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// Encoding
pipeline.addLast(new LengthFieldBasedFrameDecoder(Short.MAX_VALUE, 0, Short.BYTES, 0, Short.BYTES));
pipeline.addLast(new MessageDecoder());
// Encoder
pipeline.addLast(new LengthFieldPrepender(Short.BYTES));
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new MessageHandler(side));
}
};
}
}
@@ -3,6 +3,7 @@ package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.network.NetworkClient;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.EventLoop;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
@@ -17,6 +18,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{
private final HashMap<IClientLevelWrapper, DhClientLevel> levels;
public final ClientOnlySaveStructure saveStructure;
private final NetworkClient networkClient;
// TODO why does this executor have 2 threads?
public ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("DH Client World Ticker Thread", 2);
@@ -27,8 +29,11 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
public DhClientWorld()
{
super(EWorldEnvironment.Client_Only);
this.saveStructure = new ClientOnlySaveStructure();
this.saveStructure = new ClientOnlySaveStructure();
this.levels = new HashMap<>();
this.networkClient = new NetworkClient("127.0.0.1", 25049);
LOGGER.info("Started DhWorld of type "+this.environment);
}
@@ -3,6 +3,7 @@ package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.network.NetworkServer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
@@ -15,7 +16,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
{
private final HashMap<IServerLevelWrapper, DhServerLevel> levels;
public final LocalSaveStructure saveStructure;
private final NetworkServer networkServer;
public DhServerWorld()
@@ -24,6 +25,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
this.saveStructure = new LocalSaveStructure();
this.levels = new HashMap<>();
this.networkServer = new NetworkServer(25049);
LOGGER.info("Started "+DhServerWorld.class.getSimpleName()+" of type "+this.environment);
}