A multi-module Java framework for packet-based communication with support for different transport implementations (Netty) and serialization codecs (Kryo).
Hermes provides a clean abstraction for sending and receiving typed packets over network connections. It handles serialization, event dispatching, and request-response patterns out of the box.
| Module | Description |
|---|---|
core |
Core framework: Hermes, Packet, Event system, Response handling |
kryo |
Kryo-based serialization codec |
netty |
Netty server and client implementations |
All packets implement the Packet interface:
public interface Packet<RESPONSE> {
boolean expectsResponse();
int getId();
}expectsResponse()- Whether the packet expects a response from the handlergetId()- Unique identifier for the packet type
Encodes packets to bytes and decodes bytes back to packets:
public interface PacketCodec {
Packet<?> decode(byte[] data);
byte[] encode(Packet<?> packet);
}The main abstract class that handles:
- Sending packets
- Registering listeners
- Handling incoming packets
- Managing responses
public class ChatMessagePacket implements Packet<ChatResponse> {
private final String message;
private final UUID senderId;
public ChatMessagePacket(String message, UUID senderId) {
this.message = message;
this.senderId = senderId;
}
@Override
public boolean expectsResponse() {
return true; // Server should acknowledge
}
@Override
public int getId() {
return 1;
}
public String getMessage() { return message; }
public UUID getSenderId() { return senderId; }
}
// Response is just a regular class - no need to implement Packet
public class ChatResponse {
private final boolean success;
private final String serverMessage;
public ChatResponse(boolean success, String serverMessage) {
this.success = success;
this.serverMessage = serverMessage;
}
public boolean isSuccess() { return success; }
public String getServerMessage() { return serverMessage; }
}// Create Kryo codec with custom configuration
Supplier<Kryo> kryoSupplier = () -> {
Kryo kryo = new Kryo();
kryo.register(ChatMessagePacket.class);
kryo.register(ChatResponse.class);
return kryo;
};
PacketCodec codec = new KryoPacketCodec(kryoSupplier);
// Create and start server
NettyServerPlatform server = new NettyServerPlatform(codec);
server.start(new InetSocketAddress(8080));
// Register packet listener
server.listen(ChatMessagePacket.class, (channel, packet) -> {
System.out.println("Received: " + packet.getMessage());
return new ChatResponse(true, "Message received!");
});// Create client with same codec
NettyClientPlatform client = new NettyClientPlatform(codec);
// Connect to server
client.connect(new InetSocketAddress("localhost", 8080));
// Send packet without expecting response
client.sendPacket(new ChatMessagePacket("Hello!", UUID.randomUUID()));
// Send packet with response (async)
CompletableFuture<ChatResponse> response = client.sendResponsePacket(
new ChatMessagePacket("Hello!", UUID.randomUUID()),
ChatResponse.class
);
ChatResponse result = response.get(5, TimeUnit.SECONDS);// Using listenIgnore for packets that don't return responses
server.listenIgnore(TestPacket.class, (channel, packet) -> {
System.out.println("Received from " + channel.getId() + ": " + packet.message);
});
// Using full listener for packets that expect responses
server.listen(ChatMessagePacket.class, (channel, packet) -> {
// Process packet and return response
return new ChatResponse(true, "Processed: " + packet.getMessage());
});// Access all registered channels from the server
for (Map.Entry<HermesChannel, Channel> entry : server.getHermesToNettyChannel().entrySet()) {
HermesChannel channel = entry.getKey();
server.sendPacket(channel, new BroadcastPacket("Hello everyone!"));
}server.listenIgnore(ChannelClosePacket.class, (channel, packet) -> {
System.out.println("Client disconnected: " + channel.getId());
// Clean up resources associated with this client
});// On client side - send request and wait for response
CompletableFuture<ServerResponse> future = client.sendResponsePacket(
new MyRequestPacket(data),
ServerResponse.class
);
ServerResponse response = future.get(10, TimeUnit.SECONDS);
// On server side - handle request and return response
hermes.listen(MyRequestPacket.class, (channel, packet) -> {
// Process request
return new ServerResponse(result);
});// Register multiple listeners for the same packet type
hermes.listen(PlayerJoinPacket.class, (channel, packet) -> {
// First listener: notify admins
notifyAdmins(packet.getPlayerName());
return null;
});
hermes.listen(PlayerJoinPacket.class, (channel, packet) -> {
// Second listener: update player list
updatePlayerList(packet.getPlayerName());
return null;
});public class JsonPacketCodec implements PacketCodec {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public Packet<?> decode(byte[] data) {
return mapper.readValue(data, Packet.class);
}
@Override
public byte[] encode(Packet<?> packet) {
return mapper.writeValueAsBytes(packet);
}
}- SLF4J API
- JetBrains Annotations
- Kryo (serialization)
- Netty (transport)
- Depends on: core, kryo
./gradlew build./gradlew testPacketEventManageruses concurrent collections for thread-safe listener registrationResponseManageruses concurrent maps for thread-safe response handlingNettyPlatformuses concurrent maps for channel management
- Register all packet types in your Kryo configuration before use
- Use meaningful packet IDs to avoid conflicts
- Handle timeouts when waiting for responses
- Clean up resources by calling
shutdown()on platforms - Use response packets for bidirectional communication