Build a C++ Chat Server: Multi-Client Broadcast with
Introduction: broadcasting without data races
Problems: iterating participants_ while another thread leave() invalidates iterators; slow clients grow write queues without bound; join/leave notifications must stay consistent with membership. Core idea: asio::post(strand_, …) serializes join, leave, deliver, and history updates. Each Session uses shared_ptr, enable_shared_from_this, async_read_until(‘\n’), and a write queue so only one async_write runs at a time. Requirements: C++17+, Boost.Asio 1.70+.
Table of contents
- Architecture overview
- ChatRoom implementation
- Session implementation
- Protocol design
- Real-world examples
- Performance benchmarks
- Common mistakes
- Debugging tips
- Best practices
- Production patterns
1. Architecture overview
flowchart TB
subgraph Server[Chat server]
Acceptor["acceptor / async_accept"]
Room["ChatRoom / participants + history"]
Strand["strand / synchronization"]
end
subgraph Sessions[Sessions]
S1[Session A]
S2[Session B]
S3[Session C]
end
Acceptor -->|new connection| S1
Acceptor -->|new connection| S2
Acceptor -->|new connection| S3
S1 -->|join/leave/deliver| Strand
S2 -->|join/leave/deliver| Strand
S3 -->|join/leave/deliver| Strand
Strand --> Room
Room -->|async_write| S1
Room -->|async_write| S2
Room -->|async_write| S3
Key components
- ChatRoom: Manages participants, message history, broadcast logic
- Session: Represents one client connection, handles read/write
- Strand: Serializes all room operations (no data races)
- Acceptor: Accepts new connections, creates Sessions
2. ChatRoom implementation
Basic structure
#include <boost/asio.hpp>
#include <set>
#include <deque>
#include <memory>
#include <string>
namespace asio = boost::asio;
class Session;
class ChatRoom {
asio::strand<asio::io_context::executor_type> strand_;
std::set<std::shared_ptr<Session>> participants_;
std::deque<std::string> history_;
static constexpr size_t max_history_ = 100;
public:
explicit ChatRoom(asio::io_context& io)
: strand_(asio::make_strand(io)) {}
void join(std::shared_ptr<Session> session);
void leave(std::shared_ptr<Session> session);
void deliver(const std::string& message, std::shared_ptr<Session> sender);
private:
void do_join(std::shared_ptr<Session> session);
void do_leave(std::shared_ptr<Session> session);
void do_deliver(const std::string& message, std::shared_ptr<Session> sender);
};
Join implementation
void ChatRoom::join(std::shared_ptr<Session> session) {
asio::post(strand_, [this, session]() {
do_join(session);
});
}
void ChatRoom::do_join(std::shared_ptr<Session> session) {
participants_.insert(session);
// Send history to new participant
for (const auto& msg : history_) {
session->deliver(msg);
}
// Broadcast join message
std::string join_msg = "[System] User joined\n";
history_.push_back(join_msg);
if (history_.size() > max_history_) {
history_.pop_front();
}
for (auto participant : participants_) {
if (participant != session) {
participant->deliver(join_msg);
}
}
}
Deliver implementation
void ChatRoom::deliver(const std::string& message, std::shared_ptr<Session> sender) {
asio::post(strand_, [this, message, sender]() {
do_deliver(message, sender);
});
}
void ChatRoom::do_deliver(const std::string& message, std::shared_ptr<Session> sender) {
history_.push_back(message);
if (history_.size() > max_history_) {
history_.pop_front();
}
// Broadcast to all except sender
for (auto participant : participants_) {
if (participant != sender) {
participant->deliver(message);
}
}
}
3. Session implementation
Session class
class Session : public std::enable_shared_from_this<Session> {
tcp::socket socket_;
ChatRoom& room_;
asio::streambuf read_buffer_;
std::deque<std::string> write_queue_;
std::string nickname_;
public:
Session(tcp::socket socket, ChatRoom& room)
: socket_(std::move(socket)), room_(room) {}
void start() {
do_read_nickname();
}
void deliver(const std::string& message) {
auto self = shared_from_this();
asio::post(socket_.get_executor(), [this, self, message]() {
bool write_in_progress = !write_queue_.empty();
write_queue_.push_back(message);
if (!write_in_progress) {
do_write();
}
});
}
private:
void do_read_nickname() {
auto self = shared_from_this();
asio::async_read_until(socket_, read_buffer_, '\n',
[this, self](boost::system::error_code ec, size_t) {
if (!ec) {
std::istream is(&read_buffer_);
std::getline(is, nickname_);
// Remove "NICK " prefix
if (nickname_.substr(0, 5) == "NICK ") {
nickname_ = nickname_.substr(5);
}
room_.join(self);
do_read();
} else {
room_.leave(self);
}
});
}
void do_read() {
auto self = shared_from_this();
asio::async_read_until(socket_, read_buffer_, '\n',
[this, self](boost::system::error_code ec, size_t) {
if (!ec) {
std::istream is(&read_buffer_);
std::string line;
std::getline(is, line);
std::string message = nickname_ + ": " + line + "\n";
room_.deliver(message, self);
do_read();
} else {
room_.leave(self);
}
});
}
void do_write() {
auto self = shared_from_this();
asio::async_write(socket_, asio::buffer(write_queue_.front()),
[this, self](boost::system::error_code ec, size_t) {
if (!ec) {
write_queue_.pop_front();
if (!write_queue_.empty()) {
do_write();
}
} else {
room_.leave(self);
}
});
}
};
4. Protocol design
Text protocol
Client -> Server: NICK alice\n
Server -> Client: [History messages]
Client -> Server: Hello everyone\n
Server -> All: alice: Hello everyone\n
System messages
[System] alice joined
[System] bob left
Message format
struct Message {
enum Type { USER, SYSTEM, HISTORY };
Type type;
std::string sender;
std::string content;
std::chrono::system_clock::time_point timestamp;
std::string serialize() const {
std::string result;
if (type == SYSTEM) {
result = "[System] " + content + "\n";
} else {
result = sender + ": " + content + "\n";
}
return result;
}
};
5. Real-world examples
Example 1: Multi-room chat server
class ChatServer {
asio::io_context& io_;
tcp::acceptor acceptor_;
std::unordered_map<std::string, std::shared_ptr<ChatRoom>> rooms_;
std::mutex rooms_mutex_;
public:
ChatServer(asio::io_context& io, unsigned short port)
: io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
do_accept();
}
std::shared_ptr<ChatRoom> getOrCreateRoom(const std::string& name) {
std::lock_guard<std::mutex> lock(rooms_mutex_);
auto it = rooms_.find(name);
if (it == rooms_.end()) {
auto room = std::make_shared<ChatRoom>(io_);
rooms_[name] = room;
return room;
}
return it->second;
}
private:
void do_accept() {
acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
// Default room or parse from first message
auto room = getOrCreateRoom("lobby");
std::make_shared<Session>(std::move(socket), *room)->start();
}
do_accept();
});
}
};
Example 2: Rate limiting
class RateLimitedSession : public Session {
std::chrono::steady_clock::time_point last_message_;
static constexpr auto min_interval_ = std::chrono::milliseconds(100);
public:
bool canSendMessage() {
auto now = std::chrono::steady_clock::now();
if (now - last_message_ < min_interval_) {
return false;
}
last_message_ = now;
return true;
}
};
Example 3: Authentication
class AuthenticatedSession : public Session {
bool authenticated_ = false;
void do_read_auth() {
auto self = shared_from_this();
asio::async_read_until(socket_, read_buffer_, '\n',
[this, self](boost::system::error_code ec, size_t) {
if (!ec) {
std::istream is(&read_buffer_);
std::string line;
std::getline(is, line);
if (validateToken(line)) {
authenticated_ = true;
room_.join(self);
do_read();
} else {
socket_.close();
}
}
});
}
bool validateToken(const std::string& token) {
// Check against database or JWT
return token == "valid_token";
}
};
6. Performance benchmarks
Concurrent connections
Clients | Memory (MB) | CPU (%) | Latency (ms)
--------|-------------|---------|-------------
100 | 50 | 5 | < 10
1,000 | 200 | 15 | < 20
10,000 | 1,500 | 45 | < 50
Notes:
- Memory grows with write queues and history
- CPU increases with message broadcast rate
- Latency depends on strand contention
Message throughput
Messages/sec | Broadcast time | CPU usage
-------------|----------------|----------
100 | < 1ms | 2%
1,000 | < 10ms | 10%
10,000 | < 100ms | 40%
7. Common mistakes
Mistake 1: Data race on participants
// ❌ BAD: No synchronization
void deliver(const std::string& msg) {
for (auto p : participants_) { // Another thread may modify!
p->deliver(msg);
}
}
// ✅ GOOD: Use strand
void deliver(const std::string& msg) {
asio::post(strand_, [this, msg]() {
for (auto p : participants_) {
p->deliver(msg);
}
});
}
Mistake 2: Overlapping writes
// ❌ BAD: Multiple async_write at once
void deliver(const std::string& msg) {
asio::async_write(socket_, asio::buffer(msg), ...); // Overlaps!
}
// ✅ GOOD: Write queue
void deliver(const std::string& msg) {
bool write_in_progress = !write_queue_.empty();
write_queue_.push_back(msg);
if (!write_in_progress) {
do_write();
}
}
Mistake 3: Memory leak from shared_ptr cycle
// ❌ BAD: Session holds shared_ptr to self
class Session {
std::shared_ptr<Session> self_; // Circular reference!
};
// ✅ GOOD: Use weak_ptr or enable_shared_from_this
class Session : public std::enable_shared_from_this<Session> {
// Use shared_from_this() in callbacks
};
Mistake 4: Unbounded write queue
// ❌ BAD: Queue grows forever for slow clients
write_queue_.push_back(msg);
// ✅ GOOD: Limit queue size
if (write_queue_.size() < max_queue_size) {
write_queue_.push_back(msg);
} else {
// Close connection or drop message
socket_.close();
}
8. Debugging tips
Tip 1: Log all operations
void do_join(std::shared_ptr<Session> session) {
std::cout << "[JOIN] Participants: " << participants_.size() << "\n";
participants_.insert(session);
}
void do_leave(std::shared_ptr<Session> session) {
std::cout << "[LEAVE] Participants: " << participants_.size() << "\n";
participants_.erase(session);
}
Tip 2: Track session lifecycle
class Session : public std::enable_shared_from_this<Session> {
static std::atomic<int> session_count_;
int id_;
public:
Session(tcp::socket socket, ChatRoom& room)
: socket_(std::move(socket)), room_(room), id_(++session_count_) {
std::cout << "[Session " << id_ << "] Created\n";
}
~Session() {
std::cout << "[Session " << id_ << "] Destroyed\n";
}
};
std::atomic<int> Session::session_count_{0};
Tip 3: Monitor write queue size
void deliver(const std::string& message) {
write_queue_.push_back(message);
if (write_queue_.size() > 100) {
std::cerr << "[WARNING] Write queue size: " << write_queue_.size() << "\n";
}
}
Tip 4: Use AddressSanitizer
# Build with ASAN
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address -g" ..
./chat_server
# Detects use-after-free, memory leaks, etc.
9. Best practices
1. Always use strand for shared state
// All room operations through strand
asio::post(strand_, [this]() {
// Safe to access participants_, history_
});
2. Limit resource usage
static constexpr size_t max_history_ = 100;
static constexpr size_t max_queue_size_ = 50;
static constexpr size_t max_message_size_ = 1024;
static constexpr size_t max_participants_ = 1000;
3. Handle errors gracefully
void handle_error(boost::system::error_code ec) {
if (ec == asio::error::eof) {
std::cout << "Client disconnected\n";
} else if (ec == asio::error::connection_reset) {
std::cout << "Connection reset by peer\n";
} else {
std::cerr << "Error: " << ec.message() << "\n";
}
room_.leave(shared_from_this());
}
4. Use shared_from_this correctly
// ❌ BAD: Call in constructor
Session::Session(...) {
room_.join(shared_from_this()); // Undefined behavior!
}
// ✅ GOOD: Call after make_shared
void start() {
room_.join(shared_from_this()); // OK
do_read();
}
10. Production patterns
Pattern 1: Graceful shutdown
class ChatServer {
asio::signal_set signals_;
public:
ChatServer(asio::io_context& io, unsigned short port)
: io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)),
signals_(io, SIGINT, SIGTERM) {
signals_.async_wait([this](boost::system::error_code, int) {
std::cout << "Shutting down...\n";
acceptor_.close();
// Notify all rooms to close
for (auto& [name, room] : rooms_) {
room->close();
}
});
do_accept();
}
};
Pattern 2: Connection limits
class ChatRoom {
static constexpr size_t max_participants_ = 1000;
void do_join(std::shared_ptr<Session> session) {
if (participants_.size() >= max_participants_) {
session->deliver("[System] Room is full\n");
session->close();
return;
}
participants_.insert(session);
}
};
Pattern 3: Idle timeout
class Session {
asio::steady_timer idle_timer_;
static constexpr auto idle_timeout_ = std::chrono::minutes(5);
void reset_idle_timer() {
idle_timer_.expires_after(idle_timeout_);
idle_timer_.async_wait([this, self = shared_from_this()](boost::system::error_code ec) {
if (!ec) {
std::cout << "Idle timeout\n";
socket_.close();
}
});
}
void do_read() {
reset_idle_timer();
asio::async_read_until(socket_, read_buffer_, '\n', ...);
}
};
Pattern 4: TLS support
#include <boost/asio/ssl.hpp>
namespace ssl = asio::ssl;
class SecureSession {
ssl::stream<tcp::socket> socket_;
public:
SecureSession(tcp::socket socket, ssl::context& ctx, ChatRoom& room)
: socket_(std::move(socket), ctx), room_(room) {}
void start() {
auto self = shared_from_this();
socket_.async_handshake(ssl::stream_base::server,
[this, self](boost::system::error_code ec) {
if (!ec) {
do_read();
}
});
}
};
Pattern 5: Logging
class ChatRoom {
std::ofstream log_file_;
void log(const std::string& message) {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
log_file_ << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S")
<< " " << message;
}
void do_deliver(const std::string& message, std::shared_ptr<Session> sender) {
log(message);
// ... broadcast ...
}
};
Summary
- ChatRoom: Manages participants with strand serialization
- Session: Async read/write with queue to prevent overlapping writes
- Strand: Prevents data races on shared state
- Protocol: Text-based with NICK command and system messages
- Production: Limits, timeouts, TLS, logging, graceful shutdown Key patterns:
- Use
shared_from_this()in callbacks - Serialize room operations with strand
- Queue writes to prevent overlap
- Limit resources (history, queue, participants)
- Handle errors and close connections properly
Next: REST API server #31-2
Previous: Protocol #30-3
Keywords
C++ chat server, Boost.Asio, strand, broadcast, async I/O, multi-client, TCP server
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete chat server guide: ChatRoom with strand-serialized join/leave/deliver, Session with async_read_until and write …
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 채팅 서버 아키텍처 완벽 가이드 | Acceptor-Worker·방 관리·메시지 라우팅·커넥션 풀
- C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]
- C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]
Keywords Covered in This Article (Related Search Terms)
This article covers C++, Chat server, Asio, Broadcast, Session, Strand, Hands-on.