SimpleNet is a simplistic, client-server framework written in Java. One or more Client
objects can connect to a Server
and send data back-and-forth via TCP. Most methods that read data from the network are non-blocking and are invoked asynchronously when the requested data arrives. Not having to block a thread and wait for data is what makes SimpleNet scalable for different types of applications such as chat servers, multiplayer game servers, and so much more!
- Add SimpleNet as a dependency using either Maven or Gradle:
Maven:
<dependency>
<groupId>com.github.antideveloppeur</groupId>
<artifactId>SimpleNet</artifactId>
<version>1.6.6-j8</version>
</dependency>
Gradle:
implementation 'com.github.antideveloppeur:SimpleNet:1.6.6-j8'
- As stated above, SimpleNet is mostly callback-based. This means that you can request a specific amount of bytes,
n
, and specify what should be done with thosen
bytes, as if theClient
orServer
has already received them.
// Request a single byte and print it out when it arrives.
client.readByte(System.out::println);
// Continuously request a single byte and print it out when it arrives.
//
// After the callback completes, another byte will be requested automatically,
// and the same callback will be invoked. This continues until the connection is closed.
client.readByteAlways(System.out::println);
- Packets are sent from a
Client
to aServer
(and vice versa) via thePacket
class.
// Create a packet that contains two bytes (with the value 42) and send it to a client.
Packet.builder().putByte(42).putByte(42).queueAndFlush(client);
// Create two packets with the specified data and queue them (but don't flush) to a client.
Packet.builder().putInt(123456).queue(client);
Packet.builder().putString("Hello World!").queue(client);
// Flush the queued packets to the client (these packets will be transformed into a single,
// big packet to improve throughput.
//
// This method only needs to be called when using Packet#queue and not Packet#queueAndFlush.
client.flush();
// Instantiate a new client.
Client client = new Client();
// Register a connection listener.
client.onConnect(() -> {
System.out.println(client + " has connected to the server!");
// Builds a packet and sends it to the server immediately.
Packet.builder().putByte(1).putInt(42).queueAndFlush(client);
});
// Register a pre-disconnection listener.
client.preDisconnect(() -> System.out.println(client + " is about to disconnect from the server!"));
// Register a post-disconnection listener.
client.postDisconnect(() -> System.out.println(client + " successfully disconnected from the server!"));
// Attempt to connect to a server AFTER registering listeners.
client.connect("localhost", 43594);
// Instantiate a new server.
Server server = new Server();
// Register one connection listener.
server.onConnect(client -> {
System.out.println(client + " has connected!");
/*
* When one byte arrives from the client, switch on it.
* If the byte equals 1, then request an int and print it
* when it arrives.
*
* Because `readByteAlways` is used, this will loop when
* the callback completes, but does not hang the executing thread.
*/
client.readByteAlways(opcode -> {
switch (opcode) {
case 1:
client.readInt(System.out::println);
}
});
// Register a pre-disconnection listener.
client.preDisconnect(() -> System.out.println(client + " is about to disconnect!"));
// Register a post-disconnection listener.
client.postDisconnect(() -> System.out.println(client + " has successfully disconnected!"));
});
// Bind the server to an address and port AFTER registering listeners.
server.bind("localhost", 43594);
To emphasize how easy it is to use SimpleNet, I have written an implementation of a scalable chat server below. Note how only two classes are required, ChatServer
and ChatClient
. Ideally, a chat server should use a GUI and not the console, as this leads to message cut-off in the window, but this is only to serve as a proof-of-concept.
The full ChatServer
implementation is as follows:
public class ChatServer {
public static void main(String[] args) {
// Initialize a new server.
Server server = new Server();
// Create a map that will keep track of nicknames on our chat server.
ConcurrentHashMap<Client, String> nicknameMap = new ConcurrentHashMap<>();
// This callback is invoked when a client connects to this server.
server.onConnect(client -> {
// When a client disconnects, remove them from the nickname map.
client.postDisconnect(() -> nicknameMap.remove(client));
// Repeatedly read a single byte.
client.readByteAlways(opcode -> {
switch (opcode) {
case 1: // Change nickname
client.readString(nickname -> nicknameMap.put(client, nickname));
break;
case 2: // Send message to connected clients.
client.readString(message -> {
message = nicknameMap.get(client) + ": " + message;
server.queueAndFlushToAllExcept(Packet.builder().putString(message), client);
});
break;
}
});
});
// Bind the server to our local network AFTER registering listeners.
server.bind("localhost", 43594);
}
}
The full ChatClient
implementation is as follows:
public class ChatClient {
public static void main(String[] args) {
// Initialize a new client.
Client client = new Client();
// This callback is invoked when this client connects to a server.
client.onConnect(() -> {
Scanner scanner = new Scanner(System.in);
// If messages arrive from other clients, print them to the console.
client.readStringAlways(System.out::println);
System.out.print("Enter your nickname: ");
Packet.builder().putByte(1).putString(scanner.nextLine()).queueAndFlush(client);
// Infinite loop to accept user-input for the chat server.
while (true) {
System.out.print("> ");
// Read the client's message from the console.
String message = scanner.nextLine();
// If this client types "/leave", close their connection to the server.
if ("/leave".equals(message)) {
client.close();
break;
}
// Otherwise, send a packet to the server containing the client's message.
Packet.builder().putByte(2).putString(message).queueAndFlush(client);
}
});
// Attempt to connect to a server AFTER registering listeners.
client.connect("localhost", 43594);
}
}
- Can data be sent and received outside of the
onConnect
callback?- Yes, but only if the client is already connected to the server. However, this is not recommended unless you're well-versed in concurrency, as the
onConnect
callback is performed asynchronously.
- Yes, but only if the client is already connected to the server. However, this is not recommended unless you're well-versed in concurrency, as the
- Can a SimpleNet
Client
be used with some unrelated server and vice-versa?- Absolutely!
- I have large packets that exceed the default buffer size; what can I do to avoid an exception?
- Ideally, the best option would be to split your single, large packet into multiple, small packets.
- If splitting the packet is not possible for any reason, both
Client
andServer
have an overloaded constructor that accepts a buffer size in bytes. You can simply specify a size larger than 8192 (the default size).
- What's next for SimpleNet?
- Once Project Loom is complete and integrated into the mainline JDK, SimpleNet will be rewritten entirely; blocking I/O will be using fibers at that point, which will be much more scalable than my current implementation that uses a fixed thread pool.