Chat Example - nichnet/netplay GitHub Wiki
Chat Example
The chat example demonstrates a complete client-server application using the Netplay library. It showcases real-time messaging, user authentication, connection management, and system notifications.
Overview
The chat example consists of:
- Server: Manages client connections, validates usernames, and broadcasts messages
- Client: Connects to server, sends messages, and displays received messages
- Shared Messages: Common message types used by both client and server
Running the Example
Building and Running
# Build the example JARs
./gradlew chatServerJar chatClientJar
# Start the server
java -jar build/libs/chat-server-1.0.0.jar
# Start client(s) in separate terminals
java -jar build/libs/chat-client-1.0.0.jar
Using the Chat
- Start the server - Choose host, port, and connection limit
- Start client(s) - Enter username and server details
- Chat - Type messages and press Enter
- Exit - Type
/quit
to disconnect
Architecture
Project Structure
src/com/netplay/example/
├── shared/
│ ├── Constants.java # Message type constants
│ └── messages/
│ ├── NetworkMessageChat.java # Chat message
│ ├── NetworkMessageLogin.java # Login request
│ └── NetworkMessageNotification.java # System notifications
├── server/
│ ├── ChatServerApp.java # Server application entry point
│ ├── MyServer.java # Server implementation
│ └── actions/
│ ├── ServerActions.java # Action registry
│ ├── NetworkActionChat.java # Handle chat messages
│ └── NetworkActionLogin.java # Handle login requests
└── client/
├── ChatClientApp.java # Client application entry point
├── MyClient.java # Client implementation
└── actions/
├── ClientActions.java # Action registry
├── NetworkActionChat.java # Handle received chats
└── NetworkActionNotification.java # Handle notifications
Message Types
Constants
public class Constants {
public static final int NETWORK_MESSAGE_LOGIN = 0;
public static final int NETWORK_MESSAGE_NOTIFICATION = 1;
public static final int NETWORK_MESSAGE_CHAT = 2;
}
Login Message
@NetworkMessageHandler(Constants.NETWORK_MESSAGE_LOGIN)
public class NetworkMessageLogin extends NetworkSerializable {
@NetworkSerializableProperty(0)
public String getUsername() { return username; }
}
- Purpose: Client sends username to server for authentication
- Direction: Client → Server
- Compression: No
Chat Message
@NetworkMessageHandler(value = Constants.NETWORK_MESSAGE_CHAT, compressed = true)
public class NetworkMessageChat extends NetworkSerializable {
@NetworkSerializableProperty(0)
public String getSender() { return sender; }
@NetworkSerializableProperty(1)
public String getMessage() { return message; }
}
- Purpose: Carries chat messages between users
- Direction: Client → Server → Other Clients
- Compression: Yes (chat messages can be long)
Notification Message
@NetworkMessageHandler(Constants.NETWORK_MESSAGE_NOTIFICATION)
public class NetworkMessageNotification extends NetworkSerializable {
@NetworkSerializableProperty(0)
public String getMessage() { return message; }
}
- Purpose: System notifications (user join/leave, errors)
- Direction: Server → Clients
- Compression: No
Server Implementation
MyServer Class
public class MyServer extends Server {
private HashMap<String, String> users = new HashMap<>();
@Override
public void onUserConnected(NetworkConnection connection) {
users.put(connection.getId(), null); // No username yet
System.out.println("User connected: " + connection + " (waiting for login)");
}
@Override
public void onUserDisconnected(NetworkConnection connection) {
String username = users.get(connection.getId());
if (username != null) {
// Notify others that user left
String leaveMessage = username + " left the chat";
NetworkMessageNotification notification = new NetworkMessageNotification(leaveMessage);
NetworkMessage networkMessage = new NetworkMessage(Constants.NETWORK_MESSAGE_NOTIFICATION, notification);
sendMessageBroadcast(networkMessage);
}
users.remove(connection.getId());
}
}
Key Server Features
- User Management: Tracks usernames per connection
- Authentication: Validates unique usernames
- Message Broadcasting: Sends messages to all connected clients
- Security: Server validates sender identity, not trusting client claims
Login Action Handler
public class NetworkActionLogin extends NetworkAction {
@Override
public void perform(NetworkMessage message) throws Exception {
NetworkMessageLogin loginMessage = NetworkMessageRegistry.getInstance().create(message);
String username = loginMessage.getUsername();
String connectionId = message.getSenderConnectionId();
if (MyServer.getInstance().isUserLoggedIn(username)) {
// Username taken - kick user
NetworkMessageNotification notification = new NetworkMessageNotification(username + " is already logged in. Kicking...");
MyServer.getInstance().sendMessageUnicast(connectionId, new NetworkMessage(Constants.NETWORK_MESSAGE_NOTIFICATION, notification));
// Delayed kick to ensure message delivery
new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
MyServer.getInstance().kick(connectionId);
}).start();
return;
}
// Accept login
MyServer.getInstance().setConnectionUsername(connectionId, username);
// Notify others
NetworkMessageNotification notification = new NetworkMessageNotification(username + " joined the chat");
MyServer.getInstance().sendMessageBroadcastExcept(new String[]{connectionId},
new NetworkMessage(Constants.NETWORK_MESSAGE_NOTIFICATION, notification));
}
}
Chat Action Handler
public class NetworkActionChat extends NetworkAction {
@Override
public void perform(NetworkMessage message) throws Exception {
NetworkMessageChat chatMessage = NetworkMessageRegistry.getInstance().create(message);
String senderUsername = MyServer.getInstance().getConnectionUsername(message.getSenderConnectionId());
// Security: Don't trust client's sender name
if (senderUsername == null) {
System.out.println("Chat message from unauthenticated user, ignoring");
return;
}
// Create verified message with server-side username
NetworkMessageChat verifiedMessage = new NetworkMessageChat(senderUsername, chatMessage.getMessage());
NetworkMessage broadcastMessage = new NetworkMessage(Constants.NETWORK_MESSAGE_CHAT, verifiedMessage);
// Broadcast to all except sender
MyServer.getInstance().sendMessageBroadcastExcept(new String[]{message.getSenderConnectionId()}, broadcastMessage);
}
}
Client Implementation
MyClient Class
public class MyClient extends Client {
private String username;
@Override
public void onConnected() {
System.out.println("Connected to Server.");
if (username != null) {
sendLoginMessage(username);
}
}
private void sendLoginMessage(String username) {
NetworkMessageLogin loginMessage = new NetworkMessageLogin(username);
NetworkMessage networkMessage = new NetworkMessage(Constants.NETWORK_MESSAGE_LOGIN, loginMessage);
sendNetworkMessage(networkMessage);
}
@Override
public void onDisconnected() {
System.out.println("Disconnected from Server.");
}
}
Client Action Handlers
Chat Handler:
public class NetworkActionChat extends NetworkAction {
@Override
public void perform(NetworkMessage message) throws Exception {
NetworkMessageChat chatMessage = NetworkMessageRegistry.getInstance().create(message);
System.out.println(chatMessage.getSender() + ": " + chatMessage.getMessage());
}
}
Notification Handler:
public class NetworkActionNotification extends NetworkAction {
@Override
public void perform(NetworkMessage message) throws Exception {
NetworkMessageNotification notification = NetworkMessageRegistry.getInstance().create(message);
System.out.println("*** " + notification.getMessage() + " ***");
}
}
Communication Flow
User Joins
- Client connects to server
- Server calls
onUserConnected()
- Client automatically sends login message
- Server validates username uniqueness
- Server stores username for connection
- Server broadcasts join notification to other users
Sending Messages
- User types message in client
- Client creates
NetworkMessageChat
with username and message - Client sends to server
- Server validates sender is authenticated
- Server creates new message with verified username
- Server broadcasts to all other connected clients
- Receiving clients display the message
User Leaves
- Client disconnects (gracefully or forcefully)
- Server calls
onUserDisconnected()
- Server broadcasts leave notification
- Server removes user from tracking
Security Features
- Server Authority: Server validates all usernames, never trusts client claims
- Authentication Required: Unauthenticated users cannot send chat messages
- Username Uniqueness: Duplicate usernames are rejected with automatic kick
- Input Validation: Empty usernames and messages are handled gracefully
Error Handling
- Connection Failures: Graceful handling of network interruptions
- Duplicate Usernames: Clear error message and automatic disconnection
- Invalid Input: Empty usernames and messages are rejected
- Server Full: Connection refused when max capacity reached
Key Learning Points
- Message Registration: Using reflection to automatically register message types
- Action Patterns: Separating message handling logic into discrete action classes
- State Management: Server maintains authoritative state, clients are presentation layer
- Security: Never trust client data, always validate server-side
- Broadcasting: Efficient message distribution to multiple clients
- Graceful Degradation: Handling edge cases and error conditions