본문으로 건너뛰기
Previous
Next
Build a C++ Chat Server: Multi-Client Broadcast with

Build a C++ Chat Server: Multi-Client Broadcast with

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

  1. Architecture overview
  2. ChatRoom implementation
  3. Session implementation
  4. Protocol design
  5. Real-world examples
  6. Performance benchmarks
  7. Common mistakes
  8. Debugging tips
  9. Best practices
  10. 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++, Chat server, Asio, Broadcast, Session, Strand, Hands-on 등으로 검색하시면 이 글이 도움이 됩니다.