Project Docs: https://deepwiki.com/mohitvdx/Asynchronous-Concurrent-Chat/1-system-overview
ChatRoom is a robust and scalable chat room application developed in C++ leveraging the power of Boost.Asio for asynchronous network operations and the standard C++ threading library for concurrent client management. This application enables real-time, multi-user communication within a single chat room environment. The design emphasizes non-blocking I/O to maximize server efficiency and responsiveness, allowing a single server instance to handle a significant number of concurrent client connections.
This project demonstrates key software engineering principles, including modular design, separation of concerns, and efficient resource utilization through asynchronous programming. It serves as a practical example of building network-based applications with a focus on performance and scalability.
- High-Performance Asynchronous Communication: Implements non-blocking I/O using Boost.Asio, enabling the server to efficiently handle numerous concurrent client connections without the overhead of traditional blocking I/O. This architecture allows for optimal resource utilization and low-latency communication.
- Concurrent Client Handling via Threading: Utilizes the standard C++ threading library to dedicate a thread to each client session. This ensures that each client's interactions are handled independently, preventing any single client from blocking the operations of others and improving the overall responsiveness of the application.
- Scalable Room Management: Provides a well-defined
Room
abstraction to manage connected clients. The room is responsible for maintaining a collection of active participants and efficiently broadcasting messages to all connected clients, facilitating group communication. - Robust Message Handling: Encapsulates message handling logic within the
Message
class, which includes encoding and decoding message headers to manage message length and ensure proper delivery. This layer provides a clear interface for message construction and interpretation across the client and server. - Clear Separation of Concerns: The project adheres to the principle of separation of concerns, with distinct classes responsible for specific functionalities:
Session
for individual client connections,Room
for managing the chat environment, andMessage
for data transfer. This design promotes maintainability and testability.
The ChatRoom application follows a client-server architecture with the following key components:
Server
: The central component responsible for listening for incoming client connections on a specified port. Upon accepting a connection, it creates a newSession
object to handle communication with that client. TheServer
also manages the centralRoom
instance.Client
: The application that users interact with. It establishes a connection to theServer
and allows users to send and receive messages. TheClient
uses Boost.Asio for its network operations and a separate thread for handling user input concurrently with receiving messages from the server.Session
: Represents an individual connection between theServer
and aClient
. EachSession
object is responsible for:- Managing the socket associated with the client.
- Asynchronously reading incoming messages from the client using Boost.Asio's
async_read_until
. - Decoding the message using the
Message
class. - Delivering the received message to the
Room
for broadcast. - Receiving messages from the
Room
and asynchronously writing them back to the client using Boost.Asio'sasync_write
. - Maintaining a message queue for outgoing messages to ensure ordered delivery.
Room
: Manages a collection ofSession
objects (representing connected clients). It provides the following functionalities:- Adding new
Session
objects when clients connect (join
). - Removing
Session
objects when clients disconnect (leave
). - Receiving messages from individual
Session
objects and broadcasting them to all other connectedSession
objects (deliver
). It maintains a message queue for internal processing.
- Adding new
Message
: A utility class responsible for encapsulating the data exchanged between theClient
and theServer
. It handles:- Encoding the message body with a fixed-size header indicating the body length. This ensures proper message framing during network transmission.
- Decoding the header to determine the message body length upon reception.
- Providing methods to access the raw message data and the message body.
The core of the server's scalability lies in its use of Boost.Asio for asynchronous I/O operations. Instead of blocking the execution thread while waiting for network events (like data being received or sent), Boost.Asio allows the application to initiate an operation and then continue with other tasks. When the operation completes, a completion handler (a callback function) is executed.
async_read_until
: This method in theSession
class is used to asynchronously read data from the client socket until a specific delimiter (\n
in this case) is encountered. This simplifies message parsing as it ensures that a complete message is read before processing. The completion handler forasync_read_until
processes the received data, creates aMessage
object, and delivers it to theRoom
. It then initiates another asynchronous read operation to listen for the next message.async_write
: When theSession
needs to send a message to a client, it usesasync_write
. This method asynchronously writes the message data to the client's socket. A completion handler is provided to handle potential errors during the write operation.
This asynchronous approach allows the server's main thread (the io_context
's thread) to efficiently manage multiple client connections by switching between different tasks as I/O operations complete, rather than being blocked by any single connection.
To ensure that the server remains responsive even under heavy load, each incoming client connection is handled by a dedicated thread. When the server accepts a new connection, a Session
object is created, and its start()
method is called. This method typically initiates the asynchronous read operations for that client.
While Boost.Asio itself is asynchronous, using a separate thread for each session allows for better isolation and can simplify the logic for managing individual client states. The io_context.run()
call in the main thread of the server is responsible for dispatching the completion handlers for all asynchronous operations across all connected clients.
The Session
class is crucial for managing the lifecycle and communication of a single client connection. Key aspects of session management include:
- Initialization: When a new client connects, a
Session
object is created with the client's socket and a reference to the sharedRoom
object. - Joining the Room: Upon creation, the
Session
registers itself with theRoom
using thejoin()
method, making it a participant in the chat. - Message Handling: The
Session
is responsible for receiving messages from the client, decoding them, and delivering them to theRoom
. It also receives messages broadcast by theRoom
and sends them to its associated client. - Disconnection Handling: When a client disconnects (either gracefully or due to an error), the
async_read
operation will likely encounter an error (e.g.,boost::asio::error::eof
). The completion handler forasync_read
handles this scenario by removing theSession
from theRoom
using theleave()
method, ensuring that the disconnected client no longer receives messages. - Outgoing Message Queue: The
Session
maintains a queue (messageQueue
) for outgoing messages. This ensures that messages are written to the socket in the order they are received from theRoom
, even though the write operations are asynchronous.
The Room
class provides a central point for managing all connected clients and facilitating message broadcasting. Its key responsibilities include:
- Participant Tracking: The
Room
maintains astd::set
ofParticipantPointer
(which arestd::shared_ptr
toSession
objects in this case). Using astd::set
ensures that each participant is stored only once and allows for efficient addition and removal. - Joining and Leaving: The
join()
method adds a new participant to the set, and theleave()
method removes a participant. - Message Delivery: The
deliver()
method is responsible for taking a message and broadcasting it to all participants in the room (except the sender). It iterates through the set of participants and calls thewrite()
method of eachSession
to enqueue the message for sending. The use of a queue in bothRoom
andSession
ensures that messages are processed and sent in the order they are received.
The class diagram provides a visual representation of the project's structure and the relationships between its core components. Let's break down each class and its responsibilities:
-
Participant
(Interface): Defines the contract for any object that can participate in the chat room. It specifies two pure virtual methods:deliver(Message& message)
: An abstract method that participants must implement to handle receiving a message.write(Message& message)
: An abstract method that participants must implement to send a message. This interface allows for potential future extensions where different types of participants (e.g., bots) could be added to the room.
-
Session
(Class): Implements theParticipant
interface and represents a single client connection.-tcp::socket clientSocket
: The Boost.Asio socket object used for communication with the client.-boost::asio::streambuf buffer
: A buffer used for asynchronous read operations.-Room& room
: A reference to theRoom
object that this session belongs to. This allows the session to interact with the room (e.g., to deliver messages).-std::deque<Message> messageQueue
: A queue to hold outgoing messages waiting to be written to the client socket.+Session(tcp::socket s, Room& room)
: Constructor to initialize a newSession
object.+start()
: Initiates the session by joining the room and starting asynchronous read operations.+deliver(Message& message)
: Implements theParticipant
interface'sdeliver
method. It enqueues the message to be sent to the client.+write(Message& message)
: Implements theParticipant
interface'swrite
method. It enqueues the message for asynchronous writing to the socket.+async_read()
: Initiates an asynchronous read operation on the client socket.+async_write(std::string messageBody, size_t messageLength)
: Initiates an asynchronous write operation to the client socket.
-
Room
(Class): Manages the chat room environment.-std::deque<Message> messageQueue
: A queue for messages being processed within the room.-std::set<ParticipantPointer> participants
: A set to store pointers to all participants (currentlySession
objects) in the room. Usingstd::set
ensures uniqueness and efficient lookups.+join(ParticipantPointer participant)
: Adds a participant to the room.+leave(ParticipantPointer participant)
: Removes a participant from the room.+deliver(ParticipantPointer participant, Message& message)
: Receives a message from a participant and broadcasts it to all other participants in the room.
-
Message
(Class): Represents a message exchanged between clients and the server.-char data[header+maxBytes]
: A fixed-size buffer to store the message header and body.-size_t bodyLength_
: Stores the length of the message body.+Message()
: Default constructor.+Message(std::string message)
: Constructor to create a message from a string.+getData() std::string
: Returns the raw message data (header + body).+getBody() std::string
: Returns the message body.+getNewBodyLength(size_t newLength) size_t
: Validates and returns the new body length.+encodeHeader()
: Encodes the message body length into the header.+decodeHeader() bool
: Decodes the header to retrieve the message body length and returnsfalse
if the length exceeds the maximum allowed.+getBodyLength() size_t
: Returns the current body length.+printMessage()
: A utility method to print the message content.
-
Server
(Class): Contains the main entry point for the server application.-boost::asio::io_context io_context
: The core Boost.Asio object that provides I/O services.-tcp::acceptor acceptor
: A Boost.Asio acceptor object used to listen for incoming connections.-Room room
: The centralRoom
object managed by the server.+main(int argc, char* argv)
: The main function of the server application.+accept_connection(io_context, port, acceptor, room, endpoint)
: Asynchronously accepts incoming client connections and creates newSession
objects.
-
Client
(Class): Contains the main entry point for the client application.-boost::asio::io_context io_context
: The core Boost.Asio object for the client's I/O operations.-tcp::socket socket
: The Boost.Asio socket object used to connect to the server.-std::thread input_thread
: A separate thread to handle user input.+main(int argc, char* argv)
: The main function of the client application.+async_read(tcp::socket& socket)
: Asynchronously reads messages from the server.
-
boost_asio_io_context
(Class - Conceptual): Represents the Boost.Asio I/O context, highlighting its key methods used in the project.+run()
: Runs the I/O event loop, dispatching completion handlers.+post(function)
: Posts a function to be executed on theio_context
's thread.
-
Thread_Model
(Concept): Describes the threading model used in the application.IO Thread: Runs io_context.run()
: The main thread of the server and the thread runningio_context.run()
in the client are responsible for handling asynchronous operations.Input Thread: Handles user input
: The client uses a separate thread to read user input from the console, preventing blocking of the main I/O thread.Async Callbacks: Run on IO Thread
: Completion handlers for asynchronous operations are executed on the thread whereio_context.run()
is called.
-
MessageFlow
(Concept): Illustrates the flow of messages within the application.Client Input → Socket Write
: When a user types a message in the client, it is written to the client's socket.Socket Read → Message Parse → Room Broadcast
: The server reads the message from the client's socket, parses it, and theRoom
broadcasts it to all participants.Room Broadcast → Session Queue → Socket Write
: TheRoom
delivers the message to eachSession
, which enqueues it. TheSession
then asynchronously writes the message to the respective client's socket.
-
AsyncPattern
(Concept): Highlights the asynchronous programming patterns used with Boost.Asio.async_read_until(delimiter)
: Used for reading complete messages from the socket.async_write(buffer)
: Used for non-blocking writing of data to the socket.post(function)
: Used to safely perform operations on theio_context
from other threads (e.g., writing to the socket from the input thread in the client).Completion Handlers
: Callback functions that are executed when asynchronous operations complete.
Note for macOS Users: Boost libraries might not be readily available or easily installable on macOS. To address this, the project was developed within a Docker container running Ubuntu. The development workflow involved SSHing into this container to write and run the code.
To build and run the project, follow these steps:
- Clone the Repository:
git clone <repository_url> cd ChatRoom
- (For macOS Users): Set up Docker Container:
- Ensure you have Docker installed on your macOS system.
- Create a Dockerfile (if you haven't already) with an Ubuntu base image and instructions to install necessary dependencies, including Boost.Asio and build tools.
- Build the Docker image:
docker build -t chatroom_dev .
- Run a container from the image, mapping a port for the chat application and potentially a port for SSH:
docker run -d -p <host_port>:<container_port> -p <host_ssh_port>:22 --name chatroom_container chatroom_dev
- Find the IP address of your Docker container.
- SSH into the container:
ssh <user>@<container_ip> -p <host_ssh_port>
- Navigate to the project directory within the container.
- (Within the Ubuntu Docker Container or on a system with Boost): Build the Project:
Run the
make
command in the project root directory. This will compile the server and client applications.This command utilizes the providedmake
Makefile
to compilechatRoom.cpp
into the server executablechatApp
andclient.cpp
into the client executableclientApp
. TheMakefile
usesg++
with C++20 standard, includes necessary Boost libraries for linking, and enables debugging symbols. - (Within the Docker Container or on a system with Boost): Run the Server:
Execute the server application, providing the port number as a command-line argument. For example, to run the server on port 12345:
The server will start listening for incoming connections on the specified port (within the container). Ensure that the container's port is mapped to a port on your host machine if you are running the client outside the container.
./chatApp 12345
- (Within the Docker Container or on a system with Boost): Run the Client(s):
Open one or more terminal windows (either within the Docker container or on your host machine if the port is correctly mapped) and execute the client application, providing the same port number as the server:
Each client application will connect to the server, and users can start exchanging messages. Type your message in the client terminal and press Enter to send it to the chat room.
./clientApp 12345
One of the planned future enhancements for this project is the integration of post-quantum cryptography (PQC). As quantum computing technology advances, traditional public-key cryptography algorithms like RSA and ECC will become vulnerable to attacks. Implementing post-quantum encryption will ensure the long-term security and confidentiality of communications within the chat room, protecting against potential future threats. This will involve exploring and integrating suitable PQC libraries and algorithms into the existing network communication framework.
Boost.Asio is a powerful cross-platform C++ library for network and low-level I/O programming. It provides an asynchronous model that allows applications to handle multiple concurrent connections efficiently. The core concept in Asio is the I/O context (io_context
), which acts as a central point for all I/O operations. You create I/O objects (like sockets and acceptors) and associate them with an io_context
. Then, you initiate asynchronous operations on these objects, providing completion handlers that will be executed when the operation finishes.
In this project:
- The
Server
uses anio_context
to manage theacceptor
and the sockets of connected clients. - The
Client
also uses anio_context
for its socket connection to the server. - Asynchronous operations like
async_read_until
andasync_write
are initiated on the sockets, and their completion handlers are executed by theio_context
when the operations complete. - The
io_context.run()
method blocks until all pending asynchronous operations have completed or theio_context
has been stopped.
The server utilizes the standard C++ threading library (std::thread
) to handle each client session in a separate thread. This approach provides the following benefits:
- Isolation: Each client's session runs independently, preventing issues in one session from affecting others.
- Responsiveness: Even if one client is performing a long operation (though unlikely in this simple chat application), it won't block the handling of other clients.
- Simplified Logic: While Boost.Asio is asynchronous, using a thread per session can sometimes simplify the state management for individual connections.
However, it's important to note that for very high concurrency, a purely event-driven approach with Boost.Asio (without a thread per connection) can be more scalable as it reduces the overhead of managing a large number of threads. The current implementation provides a good balance for moderate concurrency and clear separation.
The Message
class implements a simple encoding scheme where each message is prefixed with a fixed-size header (4 bytes in this case) that indicates the length of the message body. This is a common technique for framing messages in network communication to ensure that the receiver knows how many bytes to read for a complete message.
- Encoding: When a
Message
object is created with a string, the constructor calculates the length of the string, encodes it into the 4-byte header (as an ASCII representation of the integer length), and then copies the string into the data buffer after the header. - Decoding: When the server receives data, it reads until the delimiter (
\n
). TheMessage
object'sdecodeHeader()
method extracts the first 4 bytes as the header, converts it back to an integer, and sets thebodyLength_
accordingly. It also performs a check to ensure that the body length doesn't exceed the maximum allowed size. This ensures that the server can correctly interpret the incoming message.