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 run
orjust run
-
Connect a client:
cargo run --bin client
orjust client
Run 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
main
function insrc/bin/server.rs
is executed. - It calls
init()
to set up logging and read the server address from environment variables. - A new
Server
instance is created and itsrun()
method is called. - Inside
run()
:- A
TcpListener
is bound to the specified address. - Several channels are created for inter-component communication.
- Three main components are initialized as separate Tokio tasks:
UserProcessor
RoomProcessor
ServerProcessor
- The server enters a loop, accepting new client connections.
- A
When a new client connects:
- A
ClientHandler
is initialized for the new connection. - The
ClientHandler
performs authentication by exchanging aHandshake
message. - 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
ProcessMessage
enum. - 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
RoomProcessor
maintains a HashMap of room names toRoomManager
instances. - When a new room is created:
- A new
RoomManager
is instantiated - A new Tokio task is spawned to run this
RoomManager
- The
RoomManager
is added to the HashMap
- A new
- Room operations (join, leave, message) are handled by sending messages to the appropriate
RoomManager
task. - Each
RoomManager
maintains 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
UserProcessor
maintains aUserManager
instance. - User operations (add, remove, list) are processed by the
UserProcessor
. - Unlike rooms, individual users don't have their own tasks. Instead, the
UserProcessor
handles 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 toClientMessage
variants. - 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.