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

  1. Start the server - Choose host, port, and connection limit
  2. Start client(s) - Enter username and server details
  3. Chat - Type messages and press Enter
  4. 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

  1. Client connects to server
  2. Server calls onUserConnected()
  3. Client automatically sends login message
  4. Server validates username uniqueness
  5. Server stores username for connection
  6. Server broadcasts join notification to other users

Sending Messages

  1. User types message in client
  2. Client creates NetworkMessageChat with username and message
  3. Client sends to server
  4. Server validates sender is authenticated
  5. Server creates new message with verified username
  6. Server broadcasts to all other connected clients
  7. Receiving clients display the message

User Leaves

  1. Client disconnects (gracefully or forcefully)
  2. Server calls onUserDisconnected()
  3. Server broadcasts leave notification
  4. 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

  1. Message Registration: Using reflection to automatically register message types
  2. Action Patterns: Separating message handling logic into discrete action classes
  3. State Management: Server maintains authoritative state, clients are presentation layer
  4. Security: Never trust client data, always validate server-side
  5. Broadcasting: Efficient message distribution to multiple clients
  6. Graceful Degradation: Handling edge cases and error conditions