Understanding rust, tokio, threads, channels & async better by implementing a multi-client chat server from scratch using Rust and Tokio.
- Multi-client server chat
- Channels-based architecture for shared state management (avoiding
Arc<Mutex>to use channels instead (simply for learning purposes)) - Global chat messaging
- Private/Direct Messaging
- Chat Rooms (Create, Join, Leave, List)
- Room-specific messaging
- Global Notifications
- User authentication (basic implementation)
- Task-based architecture:
- Client connection handling (send and receive)
- Core message processing
- User management (new users, user channels, user removal)
- Room management (create, join, leave, message routing)
- Ping functionality for testing connection
The project is organized into several modules:
connection: Handles the low-level connection details and frame encoding/decodingserver: Implements the server-side logic, including client handling and message processingclient: Implements the client-side logic and user interfacecommon: Contains shared data structures and message types
- Send and receive frames (encode and decode data over the network as bytes)
- Implement basic echo server
- Create chat server handling multiple clients
- Implement broadcasting messages to all connected clients
- Implement private/direct messaging
- Add support for multiple chat rooms
- Better handling of user input
- Some terminal UI for the client, ratatui?
- Implement more robust authentication and user management
- Implement end-to-end encryption for messages
- Save chat history to a database
-
Start the server:
cargo runorjust run -
Connect a client:
cargo run --bin clientorjust clientRun this command in multiple terminal windows to simulate multiple clients.
:quit- Disconnect from the server:ping- Send a ping to the server:pm <username> <message>- Send a private message to a specific user:users- List all connected users:cr <room_name>- Create a new chat room:jr <room_name>- Join a chat room:lr <room_name>- Leave a chat room:lrs- List all available rooms:lru <room_name>- List users in a specific room:rm <room_name> <message>- Send a message to a specific room
When you run the server, the following sequence of events occurs:
- The
mainfunction insrc/bin/server.rsis executed. - It calls
init()to set up logging and read the server address from environment variables. - A new
Serverinstance is created and itsrun()method is called. - Inside
run():- A
TcpListeneris bound to the specified address. - Several channels are created for inter-component communication.
- Three main components are initialized as separate Tokio tasks:
UserProcessorRoomProcessorServerProcessor
- The server enters a loop, accepting new client connections.
- A
When a new client connects:
- A
ClientHandleris initialized for the new connection. - The
ClientHandlerperforms authentication by exchanging aHandshakemessage. - If successful, a new Tokio task is spawned to handle this client's messages.
The ServerProcessor is the central component for routing messages:
- It receives messages from clients via the
ProcessMessageenum. - Based on the message type, it routes the message to the appropriate handler:
- User-related messages go to the
UserProcessor - Room-related messages go to the
RoomProcessor - Global messages are broadcast to all clients
- User-related messages go to the
Rooms are managed by the RoomProcessor and individual RoomManager instances:
- The
RoomProcessormaintains a HashMap of room names toRoomManagerinstances. - When a new room is created:
- A new
RoomManageris instantiated - A new Tokio task is spawned to run this
RoomManager - The
RoomManageris added to the HashMap
- A new
- Room operations (join, leave, message) are handled by sending messages to the appropriate
RoomManagertask. - Each
RoomManagermaintains its own set of users and handles room-specific messaging.
This approach allows each room to operate independently and concurrently.
User management is similar to room management but simpler:
- The
UserProcessormaintains aUserManagerinstance. - User operations (add, remove, list) are processed by the
UserProcessor. - Unlike rooms, individual users don't have their own tasks. Instead, the
UserProcessorhandles all user-related operations.
User input is handled in the Client struct (src/client/mod.rs):
- The
run()method sets up a channel for user input. - A separate Tokio task is spawned to read from stdin continuously.
- User input is parsed in the
parse_user_input()function, which converts text commands toClientMessagevariants. - These messages are sent through the channel and processed in the main client loop.
- Depending on the message type, it's either handled locally (e.g.,
:ping) or sent to the server.
The entire system is built on Tokio's asynchronous runtime:
- Each major component (Server, UserProcessor, RoomProcessor, ClientHandler) runs in its own Tokio task.
- Communication between components is primarily done through channels (mpsc and broadcast).
- This design allows the system to handle many concurrent connections and operations efficiently.
- Oneshot channels are used to provide a clean end efficient way to handle one-time request-response interactions.
For the initial implementation, I followed the tutorial Lily Mara - Creating a Chat Server with async Rust and Tokio.