A lightweight Java networking library for TCP packet-based communication with automatic codec negotiation.
- Java 17+
- Guava (provided at compile time)
repositories {
maven { url "https://repo.willomane.com/releases" }
}
dependencies {
implementation("me.will0mane.software.pack:api:1.0-SNAPSHOT")
implementation("com.google.guava:guava:33.4.8-jre")
}Pack is built around a few key abstractions:
- Packet — A marker interface for all data sent over the wire.
- PacketFactory — Serializes and deserializes a specific packet type using
PacketBuffer. - Codec — A named group of packet factories. Both sides negotiate which codec to use on connect.
- Peer — A client-side connection to a server.
- Server — Listens for incoming connections and hands you
Clientinstances. - Host — Utility for creating server sockets, with optional SSL.
- Pool — Manages a fixed-size set of reusable peer connections.
public record ChatMessage(String sender, String text) implements Packet {
public static class Factory implements PacketFactory<ChatMessage> {
@Override
public Class<ChatMessage> packet() {
return ChatMessage.class;
}
@Override
public ChatMessage create(PacketBuffer buffer) {
return new ChatMessage(buffer.readUTF(), buffer.readUTF());
}
@Override
public void serialize(ChatMessage packet, PacketBuffer buffer) {
buffer.writeUTF(packet.sender()).writeUTF(packet.text());
}
}
}A codec bundles all your packet factories under a shared identifier. Both the client and server must register the same codec for communication to work.
public class ChatCodec implements Codec {
@Override
public String identifier() {
return "chat-v1";
}
@Override
public void registerAll(PacketRegistrar registrar) {
registrar.register(new ChatMessage.Factory());
}
}CodecInfo codecInfo = new CodecInfo(new ChatCodec());
CodecRegistry registry = new CodecRegistry(codecInfo);
ServerSocket serverSocket = Host.socket(8080);
BaseServer server = new BaseServer(registry, serverSocket);
server.loop(client -> {
// Called for each new connection
client.registrar().register(new PacketListener<ChatMessage>() {
@Override
public Class<ChatMessage> packetClass() {
return ChatMessage.class;
}
@Override
public void onPacket(ChatMessage packet) {
System.out.println(packet.sender() + ": " + packet.text());
}
});
});CodecInfo codecInfo = new CodecInfo(new ChatCodec());
CodecRegistry registry = new CodecRegistry(codecInfo);
Peer peer = Servers.at("localhost", 8080);
peer.connect(registry);
peer.send(new ChatMessage("Alice", "Hello!"));Both Peer and Server implement AutoCloseable:
peer.close();
server.close(); // also closes all connected clientsPacketBuffer handles all serialization. Write-mode buffers are created with the no-arg constructor, read-mode buffers are created from a byte array.
// Writing
PacketBuffer buf = new PacketBuffer();
buf.writeUTF("hello").writeInt(42).writeBoolean(true);
byte[] bytes = buf.writeFully();
// Reading
PacketBuffer buf = new PacketBuffer(bytes);
String s = buf.readUTF(); // "hello"
int n = buf.readInt(); // 42
boolean b = buf.readBoolean(); // trueSupported types: byte, short, int, long, float, double, char, boolean, String, Enum, and arrays of all primitive/String types.
Pack supports request/response patterns with automatic correlation and timeouts via sendRequest:
CompletableFuture<ResponseMessage> future = peer.sendRequest(
new MyRequest("data"),
ResponseMessage.class,
TimeUnit.SECONDS, 5
);
ResponseMessage response = future.get();On the receiving side, use PacketListener.withResponse() to return a response packet:
client.registrar().register(new PacketListener<MyRequest>() {
@Override
public Class<MyRequest> packetClass() {
return MyRequest.class;
}
@Override
public Packet withResponse(MyRequest packet) {
return new ResponseMessage("ok");
}
@Override
public void onPacket(MyRequest packet) {}
});FixedSizePool maintains a set of reusable peer connections:
ConnectionInfo info = new ConnectionInfo("localhost", 8080);
CodecInfo codecInfo = new CodecInfo(new ChatCodec());
FixedSizePool pool = new FixedSizePool(5, info, codecInfo);
// Borrow a peer, use it, and return it automatically
pool.retrieve(peer -> {
peer.send(new ChatMessage("Bob", "Hi!"));
});
// Borrow a peer and take ownership (not returned to pool)
pool.withdraw(peer -> {
// peer is yours to manage
});
pool.shutdown(); // closes all pooled connectionsOn connect, the client and server automatically negotiate a codec:
- The client sends its preferred codec and list of supported codecs.
- The server checks for a match, preferring the client's preferred codec.
- If a match is found, both sides switch to that codec.
- If no match is found, the connection is rejected with
CodecMismatchException.
You can support multiple codecs by passing them to CodecInfo:
CodecInfo codecInfo = new CodecInfo(
new ChatCodecV2(), // preferred
new ChatCodecV1() // fallback
);Pack supports native SSL/TLS encryption. Enable it by passing true as the SSL flag — no other API changes required.
ServerSocket serverSocket = Host.socket(8443, true);
BaseServer server = new BaseServer(registry, serverSocket);
server.loop(client -> { /* same as plain TCP */ });Peer peer = Servers.at("localhost", 8443, true);
peer.connect(registry);ConnectionInfo info = new ConnectionInfo("localhost", 8443, true);
CodecInfo codecInfo = new CodecInfo(new ChatCodec());
FixedSizePool pool = new FixedSizePool(5, info, codecInfo);SSL uses Java's default SSLSocketFactory / SSLServerSocketFactory, configured via standard JVM system properties:
-Djavax.net.ssl.keyStore=keystore.jks
-Djavax.net.ssl.keyStorePassword=changeit
-Djavax.net.ssl.trustStore=truststore.jks
-Djavax.net.ssl.trustStorePassword=changeit
Set the system property debug-pack to enable debug logging on the server side:
-Ddebug-pack