Build a C++ Chat Server: Multi-Client Broadcast with
이 글의 핵심
Complete chat server guide: ChatRoom with strand-serialized operations, Session with async I/O and write queues, system messages, history management, data-race fixes, DoS protection, and production deployment patterns.
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
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Complete chat server guide: ChatRoom with strand-serialized join/leave/deliver, Session with async_read_until and write … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 채팅 서버 아키텍처 완벽 가이드 | Acceptor-Worker·방 관리·메시지 라우팅·커넥션 풀
- C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]
- C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]
이 글에서 다루는 키워드 (관련 검색어)
C++, Chat server, Asio, Broadcast, Session, Strand, Hands-on 등으로 검색하시면 이 글이 도움이 됩니다.