| author | b0in
<b0in@proton.me> 2023-11-27 00:22:25 UTC |
| committer | b0in
<b0in@proton.me> 2023-11-27 00:32:23 UTC |
| parent | 916459367c641b6087afdf60a828d5fa781ab5e1 |
| crow.game.codec.api/src/main/java/crow/game/codec/Context.java | +10 | -5 |
| crow.game.codec.api/src/main/java/crow/game/codec/PacketRule.java | +6 | -6 |
| crow.game.codec.api/src/main/java/crow/game/codec/package-info.java | +2 | -0 |
| crow.game.codec.netty/pom.xml | +29 | -0 |
| crow.game.codec.netty/src/main/java/crow/game/codec/netty/FrameCodec.java | +122 | -0 |
| crow.game.codec.netty/src/main/java/crow/game/codec/netty/NettyWrappedContext.java | +53 | -0 |
| crow.game.codec.netty/src/main/java/crow/game/codec/netty/PacketCodec.java | +76 | -0 |
| crow.game.codec.netty/src/main/java/crow/game/codec/netty/package-info.java | +5 | -0 |
| crow.game.examples.pingpong/pom.xml | +28 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/MainClient.java | +55 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/PingPongClientInitializer.java | +68 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/package-info.java | +4 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Constants.java | +9 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Ping.java | +6 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PingPacketHandler.java | +28 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Pong.java | +6 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PongPacketHandler.java | +27 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/RootContext.java | +65 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/package-info.java | +16 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/MainServer.java | +56 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/PingPongServerInitializer.java | +78 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/package-info.java | +4 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/Main.java | +26 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/PrefixThreadFactory.java | +30 | -0 |
| crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/package-info.java | +4 | -0 |
| crow.game.site/src/site/markdown/devlog.md | +3 | -1 |
| crow.game.site/src/site/markdown/devlog/2023-11-24.md | +2 | -1 |
| crow.game.site/src/site/markdown/devlog/2023-11-26.md | +70 | -0 |
| pom.xml | +8 | -0 |
diff --git a/crow.game.codec.api/src/main/java/crow/game/codec/Context.java b/crow.game.codec.api/src/main/java/crow/game/codec/Context.java index f13e72f..14da0b3 100644 --- a/crow.game.codec.api/src/main/java/crow/game/codec/Context.java +++ b/crow.game.codec.api/src/main/java/crow/game/codec/Context.java @@ -1,5 +1,7 @@ package crow.game.codec; +import java.util.List; + /** * Context wraps the calling context when performing packet logic. * <p> @@ -11,30 +13,33 @@ public interface Context { /** * @return the current RO protocol version as an integer. */ - public int supportedProtocolVersion(); + int supportedProtocolVersion(); + + List<PacketHandler<?>> lookupPacketHandler(int prefix); + List<PacketHandler<?>> lookupPacketHandler(Class<?> clz); /** * @return the underlying object, usually a Netty connection object. */ - public Object underlyingObject(); + Object underlyingObject(); /** * @return the location of the codec workflow this context was built in. */ - public Source callingSource(); + Source callingSource(); /** * Source determines where the context was constructed and called into. Packet Rules * can be constructed in the INIT phase, the DECODE phase, or the ENCODE phase * and packet handlers may want to know which is being called for now. */ - public static enum Source { + enum Source { INIT, DECODE, ENCODE, } - public default boolean matchesPacketVersion(PacketRule pr) { + default boolean matchesPacketVersion(PacketRule pr) { return ( this.supportedProtocolVersion() >= pr.minVersion() && this.supportedProtocolVersion() <= pr.maxVersion() diff --git a/crow.game.codec.api/src/main/java/crow/game/codec/PacketRule.java b/crow.game.codec.api/src/main/java/crow/game/codec/PacketRule.java index 522fefb..1273cdd 100644 --- a/crow.game.codec.api/src/main/java/crow/game/codec/PacketRule.java +++ b/crow.game.codec.api/src/main/java/crow/game/codec/PacketRule.java @@ -7,7 +7,7 @@ package crow.game.codec; * */ public record PacketRule( - int minVersion, int maxVersion, int prefix, int data, Type type, String name, Class<?> clz + int minVersion, int maxVersion, short prefix, int data, Type type, String name, Class<?> clz ) { /** * The type of packet, currently only 2. see classdoc. @@ -25,7 +25,7 @@ public record PacketRule( * @param type The type of the packet, STATIC or DYNAMIC * @param clz The class of the POJO we are serializing and deserializing. */ - public PacketRule(int prefix, int data, Type type, Class<?> clz) { + public PacketRule(short prefix, int data, Type type, Class<?> clz) { this(-1, -1, prefix, data, type, clz.getSimpleName(), clz); } @@ -39,7 +39,7 @@ public record PacketRule( * @param clz The class of the POJO we are serializing and deserializing. */ public PacketRule( - int prefix, + short prefix, int data, Type type, String name, @@ -62,7 +62,7 @@ public record PacketRule( public PacketRule( int minVersion, int maxVersion, - int prefix, + short prefix, int data, Type type, String name, @@ -86,7 +86,7 @@ public record PacketRule( * @param clz The class that we are serializing and deserializing. * @return The packet rule object. */ - public static PacketRule Dynamic(int prefix, int sizeTag, Class<?> clz) { + public static PacketRule Dynamic(short prefix, int sizeTag, Class<?> clz) { return new PacketRule(prefix, sizeTag, Type.DYNAMIC, clz); } @@ -99,7 +99,7 @@ public record PacketRule( * @param clz The class that we are serializing and deserializing. * @return The packet rule object. */ - public static PacketRule Static(int prefix, int size, Class<?> clz) { + public static PacketRule Static(short prefix, int size, Class<?> clz) { return new PacketRule(prefix, size, Type.STATIC, clz); } } diff --git a/crow.game.codec.api/src/main/java/crow/game/codec/package-info.java b/crow.game.codec.api/src/main/java/crow/game/codec/package-info.java index 60c394f..a7e5470 100644 --- a/crow.game.codec.api/src/main/java/crow/game/codec/package-info.java +++ b/crow.game.codec.api/src/main/java/crow/game/codec/package-info.java @@ -5,6 +5,8 @@ * otherwise decoupled from Netty. sub-package contains the Netty-specific code * for building {@link crow.game.codec.Packet} objects. * <p> + * TODO: switch to {@link java.nio.ByteBuffer} which can remove the netty dependency. + * <p> * There are multiple hooks for handling differing packet versions in this API: * <p> * You can define your POJO as "MyPacket20NN0101" alongside {@link crow.game.codec.PacketHandler} "MyPacketHandler20NN0101" diff --git a/crow.game.codec.netty/pom.xml b/crow.game.codec.netty/pom.xml new file mode 100644 index 0000000..ec58861 --- /dev/null +++ b/crow.game.codec.netty/pom.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>crow.game</groupId> + <artifactId>crow.game.root</artifactId> + <version>0.1-SNAPSHOT</version> + </parent> + <artifactId>crow.game.codec.netty</artifactId> + <dependencies> + <dependency> + <groupId>crow.game</groupId> + <artifactId>crow.game.codec.api</artifactId> + <version>0.1-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-buffer</artifactId> + <version>4.1.101.Final</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.35</version> + <scope>provided</scope> + </dependency> + </dependencies> +</project> diff --git a/crow.game.codec.netty/src/main/java/crow/game/codec/netty/FrameCodec.java b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/FrameCodec.java new file mode 100644 index 0000000..55befc9 --- /dev/null +++ b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/FrameCodec.java @@ -0,0 +1,122 @@ +package crow.game.codec.netty; + +import crow.game.codec.Context; +import crow.game.codec.Packet; +import crow.game.codec.PacketHandler; +import crow.game.codec.PacketRule; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The FrameCodec converts to and from {@link ByteBuf} streams into individual + * {@link crow.game.codec.Packet} objects. The first two bytes of a packet determine which type + * of packet it is (called the packet ID). + * + * @see crow.game.codec.PacketRule + * @see crow.game.codec.Packet + * @see PacketCodec + */ +public class FrameCodec extends ByteToMessageCodec<Packet> { + + static Logger logger = LoggerFactory.getLogger(FrameCodec.class); + + // + protected final Context context; + + public FrameCodec(Context context) { + this.context = context; + } + + protected boolean packetRuleHasMoreData(ByteBuf in, PacketRule pr) { + PacketRule.Type s = pr.type(); + return switch (s) { + case DYNAMIC -> true; + case STATIC -> pr.data() <= in.readableBytes() + 2; + }; + } + + protected Optional<PacketRule> resolvePacketRule( + Context ctx, + int prefix, + ByteBuf in + ) { + return ctx + .lookupPacketHandler(prefix) + .stream() // + .map(((PacketHandler<?> ph) -> ph.rule(ctx))) + .filter((PacketRule pr) -> ctx.matchesPacketVersion(pr)) + .filter((PacketRule pr) -> packetRuleHasMoreData(in, pr)) + .findFirst(); + } + + @Override + protected void encode(ChannelHandlerContext ctx, Packet msg, ByteBuf out) { + PacketRule pr = msg.rule(); + + short prefix = (short) pr.prefix(); + out.writeShortLE(prefix); + if (pr.type() == PacketRule.Type.DYNAMIC) { + if (pr.data() == 2) { + out.writeShortLE(msg.buffer().readableBytes() + 2); + } else { + out.writeIntLE(msg.buffer().readableBytes() + 4); + } + } + + out.writeBytes(msg.buffer()); + } + + @Override + protected void decode( + ChannelHandlerContext ctx, + ByteBuf in, + List<Object> out + ) { + short x = in.getShortLE(in.readerIndex()); + String readableX = String.format("0x%x", x); + Context wrapped = new NettyWrappedContext( + Context.Source.DECODE, + this.context, + ctx + ); + + Optional<PacketRule> rule = resolvePacketRule(wrapped, x, in); + if (rule.isEmpty()) { + logger.warn("no packet rule for {} - {}", readableX, x); + throw new UnsupportedMessageTypeException(); + } + + PacketRule packetRule = rule.get(); + + if (packetRule.type().equals(PacketRule.Type.STATIC)) { + out.add( + new Packet( + in.readBytes(rule.get().data() + 2).copy(), + rule.get() + ) + ); + } else { + int size = -1; + // all packets sizes are just the 2 or 4 byte words AFTER the packet id + if (packetRule.data() == 2) { + if (in.readableBytes() >= 2 + 2) { + size = in.getShortLE(in.readerIndex() + 2); + } + } else { + if (in.readableBytes() > 2 + 4) { + size = in.getIntLE(in.readerIndex() + 2); + } + } + if (in.readableBytes() < 2 + size) { + return; + } + out.add(new Packet(in.readBytes(size), rule.get())); + } + } +} diff --git a/crow.game.codec.netty/src/main/java/crow/game/codec/netty/NettyWrappedContext.java b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/NettyWrappedContext.java new file mode 100644 index 0000000..09e1bb6 --- /dev/null +++ b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/NettyWrappedContext.java @@ -0,0 +1,53 @@ +package crow.game.codec.netty; + +import crow.game.codec.Context; +import crow.game.codec.PacketHandler; +import io.netty.channel.ChannelHandlerContext; +import java.util.List; + +/** + * Implementation of {@link Context} which wraps an upper-level context and sets the + * {@link crow.game.codec.Context.Source} to the given value and sets the underlying object to an + * instance of {@link ChannelHandlerContext}. + */ +public class NettyWrappedContext implements Context { + + protected final Context context; + protected final ChannelHandlerContext nettyContext; + protected final Context.Source source; + + public NettyWrappedContext( + Context.Source src, + Context context, + ChannelHandlerContext nettyContext + ) { + this.context = context; + this.nettyContext = nettyContext; + this.source = src; + } + + @Override + public int supportedProtocolVersion() { + return this.context.supportedProtocolVersion(); + } + + @Override + public List<PacketHandler<?>> lookupPacketHandler(Class<?> clz) { + return this.context.lookupPacketHandler(clz); + } + + @Override + public List<PacketHandler<?>> lookupPacketHandler(int prefix) { + return this.context.lookupPacketHandler(prefix); + } + + @Override + public Object underlyingObject() { + return this.nettyContext; + } + + @Override + public Source callingSource() { + return this.source; + } +} diff --git a/crow.game.codec.netty/src/main/java/crow/game/codec/netty/PacketCodec.java b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/PacketCodec.java new file mode 100644 index 0000000..3e9e912 --- /dev/null +++ b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/PacketCodec.java @@ -0,0 +1,76 @@ +package crow.game.codec.netty; + +import crow.game.codec.Context; +import crow.game.codec.Packet; +import crow.game.codec.PacketHandler; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +import java.util.List; + +/** + * Codec responsible for converting to and from {@link crow.game.codec.Packet} objects and the + * business-logic specific POJOs. + */ +public class PacketCodec extends MessageToMessageCodec<Packet, Object> { + + protected final Context context; + + public PacketCodec(Context ctx) { + this.context = ctx; + } + + @Override + protected void decode( + ChannelHandlerContext ctx, + Packet msg, + List<Object> out + ) throws Exception { + Context wrCtx = new NettyWrappedContext( + Context.Source.DECODE, + context, + ctx + ); + PacketHandler<?> h = + ( + wrCtx + .lookupPacketHandler(msg.rule().prefix()) // + .stream() // + .filter(hx -> wrCtx.matchesPacketVersion(hx.rule(wrCtx))) // + .findFirst() // + .orElseThrow() + ); + + msg.buffer().readShortLE(); // drop the packet ID + out.add(h.fromPacket(wrCtx, msg.buffer())); + } + + @Override + protected void encode( + ChannelHandlerContext ctx, + Object msg, + List<Object> out + ) throws Exception { + Context wrCtx = new NettyWrappedContext( + Context.Source.ENCODE, + this.context, + ctx + ); + + @SuppressWarnings("unchecked") + PacketHandler<Object> h = (PacketHandler<Object>) wrCtx + .lookupPacketHandler(msg.getClass()) + .stream() // + .filter(hx -> wrCtx.matchesPacketVersion(hx.rule(wrCtx))) // + .findFirst() // + .orElseThrow(); + + // TODO: idk, pool this? use better buffer sizes?? + // TODO: does this leak + ByteBuf directBuffer = Unpooled.directBuffer(1024); + + h.toPacket(wrCtx, directBuffer, msg); + out.add(new Packet(directBuffer, h.rule(wrCtx))); + } +} diff --git a/crow.game.codec.netty/src/main/java/crow/game/codec/netty/package-info.java b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/package-info.java new file mode 100644 index 0000000..92c9c67 --- /dev/null +++ b/crow.game.codec.netty/src/main/java/crow/game/codec/netty/package-info.java @@ -0,0 +1,5 @@ +/** + * Package containing all the serialization and deserialization logic for the + * protocol. + */ +package crow.game.codec.netty; diff --git a/crow.game.examples.pingpong/pom.xml b/crow.game.examples.pingpong/pom.xml new file mode 100644 index 0000000..334750c --- /dev/null +++ b/crow.game.examples.pingpong/pom.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>crow.game</groupId> + <artifactId>crow.game.root</artifactId> + <version>0.1-SNAPSHOT</version> + </parent> + <artifactId>crow.game.examples.pingpong</artifactId> + <dependencies> + <dependency> + <groupId>crow.game</groupId> + <artifactId>crow.game.codec.netty</artifactId> + <version>0.1-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.36</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.36</version> + </dependency> + </dependencies> +</project> diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/MainClient.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/MainClient.java new file mode 100644 index 0000000..7287ebc --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/MainClient.java @@ -0,0 +1,55 @@ +package crow.game.examples.pingpong.client; + +import crow.game.codec.Context; +import crow.game.examples.pingpong.proto.Constants; +import crow.game.examples.pingpong.proto.RootContext; +import crow.game.examples.pingpong.utils.PrefixThreadFactory; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This wraps the client into a runnable that can be run as a main entrypoint + * OR passed into a Thread. + * + * @see crow.game.examples.pingpong.utils.Main + */ +public class MainClient implements Runnable { + + protected static final Logger logger = LoggerFactory.getLogger( + MainClient.class + ); + + public static void main(String[] args) { + new MainClient().run(); + } + + public void run() { + EventLoopGroup workerGroup = new NioEventLoopGroup( + new PrefixThreadFactory("client-worker") + ); + Context context = new RootContext(); + try { + Bootstrap b = new Bootstrap(); // + + b + .group(workerGroup) // + .channel(NioSocketChannel.class) // + .handler(new PingPongClientInitializer(context)); + + ChannelFuture f = b + .connect("127.0.0.1", Constants.LISTEN_PORT) + .sync(); + f.channel().closeFuture().sync(); + } catch (InterruptedException e) { + //e.printStackTrace(); + } finally { + workerGroup.shutdownGracefully(); + } + logger.info("client finished"); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/PingPongClientInitializer.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/PingPongClientInitializer.java new file mode 100644 index 0000000..cac4f26 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/PingPongClientInitializer.java @@ -0,0 +1,68 @@ +package crow.game.examples.pingpong.client; + +import crow.game.codec.Context; +import crow.game.codec.netty.FrameCodec; +import crow.game.codec.netty.PacketCodec; +import crow.game.examples.pingpong.proto.Ping; +import crow.game.examples.pingpong.proto.Pong; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Netty clients logic for the ping pong example. it registers + * the standard {@link FrameCodec} and {@link PacketCodec} then + * just sends a {@link Ping} message once the connection is established, finally + * drops the connection once a {@link Pong} is received. + */ +public class PingPongClientInitializer + extends ChannelInitializer<SocketChannel> { + + public static final Logger logger = LoggerFactory.getLogger( + PingPongClientInitializer.class + ); + + protected final Context context; + + public PingPongClientInitializer(Context ctx) { + this.context = ctx; + } + + @Override + protected void initChannel(SocketChannel ch) throws Exception { + logger.info("initializing channel"); + ch + .pipeline() // + .addLast(new FrameCodec(context)) // + .addLast(new PacketCodec(context)) // + .addLast( + new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) + throws Exception { + ctx.writeAndFlush(new Ping()).sync(); + } + + @Override + public void channelRead( + ChannelHandlerContext ctx, + Object msg + ) throws Exception { + if (msg instanceof Pong) { + logger.info("GOT PONG"); + ctx.close(); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) + throws Exception { + logger.info("channel done"); + } + } + ); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/package-info.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/package-info.java new file mode 100644 index 0000000..b9e9579 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/client/package-info.java @@ -0,0 +1,4 @@ +/** + * The client code for the ping/pong example. + */ +package crow.game.examples.pingpong.client; diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Constants.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Constants.java new file mode 100644 index 0000000..74237bc --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Constants.java @@ -0,0 +1,9 @@ +package crow.game.examples.pingpong.proto; + +public class Constants { + + public static final int LISTEN_PORT = 9991; + + public static final short PING_PREFIX = (short) 0x9901; + public static final short PONG_PREFIX = (short) 0x9902; +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Ping.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Ping.java new file mode 100644 index 0000000..5bb84d2 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Ping.java @@ -0,0 +1,6 @@ +package crow.game.examples.pingpong.proto; + +/** + * The super simple Ping request POJO + */ +public class Ping {} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PingPacketHandler.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PingPacketHandler.java new file mode 100644 index 0000000..2c3e83e --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PingPacketHandler.java @@ -0,0 +1,28 @@ +package crow.game.examples.pingpong.proto; + +import crow.game.codec.Context; +import crow.game.codec.PacketHandler; +import crow.game.codec.PacketRule; +import io.netty.buffer.ByteBuf; + +/** + * This is the packet handler for the Ping message. It, ironically, is empty + * since Ping is a 0-byte message (ignoring packet id) so this handler is + * only responsible for constructing and defining the packet details. + */ +public class PingPacketHandler implements PacketHandler<Ping> { + + @Override + public Ping fromPacket(Context ctx, ByteBuf buffer) throws Exception { + return new Ping(); + } + + @Override + public void toPacket(Context ctx, ByteBuf buffer, Ping src) + throws Exception {} + + @Override + public PacketRule rule(Context ctx) { + return PacketRule.Static(Constants.PING_PREFIX, 0, Ping.class); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Pong.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Pong.java new file mode 100644 index 0000000..bc1f71f --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/Pong.java @@ -0,0 +1,6 @@ +package crow.game.examples.pingpong.proto; + +/** + * The super simple Pong response POJO + */ +public class Pong {} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PongPacketHandler.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PongPacketHandler.java new file mode 100644 index 0000000..4854fa8 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/PongPacketHandler.java @@ -0,0 +1,27 @@ +package crow.game.examples.pingpong.proto; + +import crow.game.codec.Context; +import crow.game.codec.PacketHandler; +import crow.game.codec.PacketRule; +import io.netty.buffer.ByteBuf; + +/** + * This is the packet handler for the Pong message. It, ironically, is empty + * since Pong is a 0-byte message (ignoring packet id) so this handler is + * only responsible for constructing and defining the packet details. + */ +public class PongPacketHandler implements PacketHandler<Pong> { + + @Override + public Pong fromPacket(Context ctx, ByteBuf buffer) { + return new Pong(); + } + + @Override + public void toPacket(Context ctx, ByteBuf buffer, Pong src) {} + + @Override + public PacketRule rule(Context ctx) { + return PacketRule.Static(Constants.PONG_PREFIX, 0, Ping.class); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/RootContext.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/RootContext.java new file mode 100644 index 0000000..0de4000 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/RootContext.java @@ -0,0 +1,65 @@ +package crow.game.examples.pingpong.proto; + +import crow.game.codec.Context; +import crow.game.codec.PacketHandler; +import java.util.Arrays; +import java.util.List; + +/** + * The root context is the context implementation for the Ping/Pong + * server and defines the packet version and provides the {@link PacketHandler}s. + * <p> + * More complicated client/server implementations could use spring or guice or SPI + * to lookup packet POJOs and {@link PacketHandler}s but this one is too simple + * to justify that level of work. + * <p> + * Ideally we will have a common server or client module that implements a Context + * that does use guice or spring and then that module is just marked as "requiring + * injection library". + * + */ +public class RootContext implements Context { + + protected final PacketHandler<Ping> pingPacketHandler; + protected final PacketHandler<Pong> pongPacketHandler; + + public RootContext() { + this.pingPacketHandler = new PingPacketHandler(); + this.pongPacketHandler = new PongPacketHandler(); + } + + public Context.Source callingSource() { + return Context.Source.INIT; + } + + public int supportedProtocolVersion() { + return 20231115; + } + + @Override + public List<PacketHandler<?>> lookupPacketHandler(int prefix) { + if (prefix == Constants.PING_PREFIX) { + return Arrays.asList(pingPacketHandler); + } else if (prefix == Constants.PONG_PREFIX) { + return Arrays.asList(pongPacketHandler); + } else { + return Arrays.asList(); + } + } + + @Override + public List<PacketHandler<?>> lookupPacketHandler(Class<?> clz) { + if (clz.isAssignableFrom(Ping.class)) { + return Arrays.asList(pingPacketHandler); + } else if (clz.isAssignableFrom(Pong.class)) { + return Arrays.asList(pongPacketHandler); + } else { + return Arrays.asList(); + } + } + + @Override + public Object underlyingObject() { + return this; + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/package-info.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/package-info.java new file mode 100644 index 0000000..48ca944 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/proto/package-info.java @@ -0,0 +1,16 @@ +/** + * This is the ping pong protocol package and would, in more complex scenarios, + * be in its own JAR. It contains the POJOs and PacketHandlers for both client + * and server needs and is meant to be included in both sides to support bidirectional + * communication. + * <p> + * If you look at the {@link crow.game.examples.pingpong.proto.Ping} and {@link crow.game.examples.pingpong.proto.Pong} + * objects and their corresponding PacketHandlers, you'll notice there is no real + * different between the two. The request/response model in this system is not defined(1) so + * objects can be both, providing their PacketHandlers implement both decode and encode. + * <p> + * (1) NOTE: This may change in the future! + * <p> + * @see crow.game.examples.pingpong.proto.RootContext + */ +package crow.game.examples.pingpong.proto; diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/MainServer.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/MainServer.java new file mode 100644 index 0000000..4536d2b --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/MainServer.java @@ -0,0 +1,56 @@ +package crow.game.examples.pingpong.server; + +import crow.game.codec.Context; +import crow.game.examples.pingpong.proto.Constants; +import crow.game.examples.pingpong.proto.RootContext; +import crow.game.examples.pingpong.utils.PrefixThreadFactory; +import io.netty.bootstrap.ServerBootstrap; +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.NioServerSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This wraps the server into a runnable that can be run as a main entrypoint + * OR passed into a Thread. + * + * @see crow.game.examples.pingpong.utils.Main + */ +public class MainServer implements Runnable { + + public static final Logger logger = LoggerFactory.getLogger( + MainServer.class + ); + + public static void main(String[] args) { + new MainServer().run(); + } + + public void run() { + EventLoopGroup bossGroup = new NioEventLoopGroup( + new PrefixThreadFactory("server-boss") + ); + EventLoopGroup workerGroup = new NioEventLoopGroup( + new PrefixThreadFactory("server-worker") + ); + Context context = new RootContext(); + try { + ServerBootstrap b = new ServerBootstrap(); // + b + .group(bossGroup, workerGroup) // + .channel(NioServerSocketChannel.class) // + .childHandler(new PingPongServerInitializer(context)) // + .option(ChannelOption.SO_BACKLOG, 128) // + .childOption(ChannelOption.SO_KEEPALIVE, true); + + ChannelFuture f = b.bind(Constants.LISTEN_PORT).sync(); // (7) + f.channel().closeFuture().sync(); + } catch (InterruptedException e) {} finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/PingPongServerInitializer.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/PingPongServerInitializer.java new file mode 100644 index 0000000..df76cd9 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/PingPongServerInitializer.java @@ -0,0 +1,78 @@ +package crow.game.examples.pingpong.server; + +import crow.game.codec.Context; +import crow.game.codec.netty.FrameCodec; +import crow.game.codec.netty.PacketCodec; +import crow.game.examples.pingpong.proto.Ping; +import crow.game.examples.pingpong.proto.Pong; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Netty server logic for the ping pong example. it registers + * the standard {@link FrameCodec} and {@link PacketCodec} then + * just waits for a {@link Ping} object to come from a client. + * <p> + * The bottom of the pipeline is a {@link SimpleChannelInboundHandler} with + * two functions: listen for Ping messages and handle exceptions thrown by the + * codecs and report and drop the connection as needed. + */ +public class PingPongServerInitializer + extends ChannelInitializer<SocketChannel> { + + public static final Logger logger = LoggerFactory.getLogger( + PingPongServerInitializer.class + ); + + protected final Context context; + + public PingPongServerInitializer(Context ctx) { + this.context = ctx; + } + + @Override + protected void initChannel(SocketChannel ch) { + ch + .pipeline() // + .addLast(new FrameCodec(context)) // + .addLast(new PacketCodec(context)) // + .addLast( + new SimpleChannelInboundHandler<Ping>() { + @Override + protected void channelRead0( + ChannelHandlerContext channelHandlerContext, + Ping ping + ) { + logger.info("GOT PING, RESPONDING"); + channelHandlerContext.writeAndFlush(new Pong()); + } + + @Override + public void exceptionCaught( + ChannelHandlerContext ctx, + Throwable cause + ) { + if (cause instanceof DecoderException) { + if ( + cause.getCause() instanceof UnsupportedMessageTypeException + ) { + logger.warn( + "dropping connection due to malformed packet" + ); + ctx.close(); + return; + } + } + + logger.error("error", cause); + } + } + ); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/package-info.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/package-info.java new file mode 100644 index 0000000..e4bc059 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/server/package-info.java @@ -0,0 +1,4 @@ +/** + * The server code for the ping/pong example. + */ +package crow.game.examples.pingpong.server; diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/Main.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/Main.java new file mode 100644 index 0000000..f942842 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/Main.java @@ -0,0 +1,26 @@ +package crow.game.examples.pingpong.utils; + +import crow.game.examples.pingpong.client.MainClient; +import crow.game.examples.pingpong.server.MainServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This main runs both client and server as separate threads. + */ +public class Main { + + public static void main(String[] args) { + Logger l = LoggerFactory.getLogger(Main.class); + l.info("running..."); + Thread thr1 = new Thread(new MainServer(), "server"); + thr1.start(); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + new MainClient().run(); + thr1.interrupt(); + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/PrefixThreadFactory.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/PrefixThreadFactory.java new file mode 100644 index 0000000..a2d397e --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/PrefixThreadFactory.java @@ -0,0 +1,30 @@ +package crow.game.examples.pingpong.utils; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Thread factory which prefixes the thread with the given + * name and an incremental integer. Useful for debugging + * thread pools built by Netty. + */ +public class PrefixThreadFactory implements ThreadFactory { + + AtomicInteger at; + + String prefix; + + public PrefixThreadFactory(String p) { + at = new AtomicInteger(0); + prefix = p; + } + + @Override + public Thread newThread(Runnable arg0) { + Thread thr = new Thread( + arg0, + String.format("%s-%d", prefix, at.getAndIncrement()) + ); + return thr; + } +} diff --git a/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/package-info.java b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/package-info.java new file mode 100644 index 0000000..bcfb2f8 --- /dev/null +++ b/crow.game.examples.pingpong/src/main/java/crow/game/examples/pingpong/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * Shared utilities and launcher for the ping pong example. + */ +package crow.game.examples.pingpong.utils; diff --git a/crow.game.site/src/site/markdown/devlog.md b/crow.game.site/src/site/markdown/devlog.md index dbac497..2bd9b72 100644 --- a/crow.game.site/src/site/markdown/devlog.md +++ b/crow.game.site/src/site/markdown/devlog.md @@ -1,3 +1,5 @@ ## Development Log - * [devlog/2023-11-24.html](devlog/2023-11-24.html) - b0in \ No newline at end of file +* [devlog/2023-11-24.html](devlog/2023-11-24.html) - b0in +* [devlog/2023-11-26.html](devlog/2023-11-26.html) - b0in + diff --git a/crow.game.site/src/site/markdown/devlog/2023-11-24.md b/crow.game.site/src/site/markdown/devlog/2023-11-24.md index aca624c..6bfbd94 100644 --- a/crow.game.site/src/site/markdown/devlog/2023-11-24.md +++ b/crow.game.site/src/site/markdown/devlog/2023-11-24.md @@ -100,4 +100,5 @@ public class PingPacketHandler implements PacketHandler<Ping> { return PacketRule.Static(0x2719, /*length=*/0, Ping.class); } } -``` \ No newline at end of file +``` + diff --git a/crow.game.site/src/site/markdown/devlog/2023-11-26.md b/crow.game.site/src/site/markdown/devlog/2023-11-26.md new file mode 100644 index 0000000..bda181c --- /dev/null +++ b/crow.game.site/src/site/markdown/devlog/2023-11-26.md @@ -0,0 +1,70 @@ +## Development Log - 2023-11-26 + +Author: b0in + +### The second and third modules! + +The second module is now done. The first module, `crow.game.codec.netty`, is the FrameCodec and PacketCodec implementations +which allow a netty client and server listen for messages. The second module, +`crow.game.examples.pingpong` is an example of a TCP client and server using +the two core modules implemented now. + +### Module Statuses + +From the previous post [./2023-11-24.html](./2023-11-24.html), but just +the modules which have changed or added. + +#### crow.game.codec.api - [95%] + +We tweaked this one to use 'short' type for packet ID everywhere, though +I'm worried this is going to be a pain due to signed/unsigned issues. My +sample packets were 0x99NN which rolled over. This might not be a problem +in the future but, we will see. + +#### crow.game.codec.netty - [90%] + +Working! Not much to say as this package is incredibly simple. I would like +to move all ByteBuf references in codec.api into this package, and either use +a shared interface in codec.api OR use nio ByteBuffer. Not sure which way +I will go yet. + +#### crow.game.examples.pingpong - [90%] + +An example application using Netty, slf4j, and not much else. +The server merely waits for Ping packets and responds with Pongs and +the client just connects, sends Ping, then disconnects when it receives +the Pong. + +Here is the results of running this example: + +``` +[main] INFO utils.Main - running... +[client-worker-0] INFO client.PingPongClientInitializer - initializing channel +[server-worker-0] INFO server.PingPongServerInitializer - GOT PING, RESPONDING +[client-worker-0] INFO client.PingPongClientInitializer - GOT PONG +[client-worker-0] INFO client.PingPongClientInitializer - channel done +[main] INFO client.MainClient - client finished + +Process finished with exit code 0 +``` + +and to show how you would put it all together, here is the initChannel implementation +for the server (without the context and exception handling): + +```java +class PingPongServerInitializer { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline() // + .addLast(new FrameCodec(context)) // + .addLast(new PacketCodec(context)) // + .addLast(new SimpleChannelInboundHandler<Ping>() { + @Override + void channelRead0(ChannelHandlerContext ctx, Ping ping) { + ctx.writeAndFlush(new Pong()); + } + }); + } +} +``` + diff --git a/pom.xml b/pom.xml index 0a65414..a8c83ff 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,8 @@ <modules> <module>crow.game.site</module> <module>crow.game.codec.api</module> + <module>crow.game.codec.netty</module> + <module>crow.game.examples.pingpong</module> </modules> <scm> <connection>scm:git:https://b0in.xyz/crow/crow-latest-git.tar.gz</connection> @@ -58,6 +60,12 @@ <sortPlugins>groupId,artifactId</sortPlugins> </sortPom> </pom> + <markdown> + <includes> + <include>**/*.md</include> + </includes> + <flexmark></flexmark> + </markdown> <formats> <format> <includes>