본문 바로가기
-----ETC-----/C++ 네트워크 프로그래밍 시리즈

[C++ 네트워크 프로그래밍] Day 22: 프로젝트 소개 및 설계 (실시간 채팅 애플리케이션)

by cogito21_cpp 2024. 8. 1.
반응형

실시간 채팅 애플리케이션

이번 프로젝트에서는 실시간 채팅 애플리케이션을 설계하고 구현할 것입니다. 이 애플리케이션은 클라이언트와 서버가 WebSocket을 통해 실시간으로 메시지를 주고받을 수 있게 합니다. 프로젝트는 클라이언트와 서버 두 부분으로 나뉘며, 사용자는 여러 클라이언트에서 메시지를 전송하고 수신할 수 있습니다.

프로젝트 설계

기능 요구사항

  1. 사용자 연결 관리: 여러 사용자가 동시에 서버에 연결할 수 있어야 합니다.
  2. 메시지 전송: 사용자가 메시지를 보내고, 서버는 이를 다른 모든 사용자에게 전달해야 합니다.
  3. 사용자 식별: 각 사용자는 고유한 사용자 이름으로 식별됩니다.
  4. 연결 유지: 서버는 사용자 연결 상태를 유지하고 관리해야 합니다.
  5. 오류 처리: 서버와 클라이언트 모두에서 오류를 처리해야 합니다.

시스템 구성 요소

  1. WebSocket 서버: 클라이언트 연결을 수락하고 메시지를 중계합니다.
  2. WebSocket 클라이언트: 서버에 연결하고 메시지를 주고받는 인터페이스를 제공합니다.

설계 다이어그램

+-------------------+    WebSocket    +-------------------+
|   Chat Client 1   | <-------------> |    Chat Server    |
+-------------------+                  +-------------------+
       |                                      |
       v                                      v
+-------------------+                  +-------------------+
|   Chat Client 2   | <-------------> |    Chat Server    |
+-------------------+                  +-------------------+

사용할 라이브러리

  • Boost.Asio: 네트워크 프로그래밍을 위한 비동기 I/O 라이브러리.
  • Boost.Beast: HTTP와 WebSocket 프로토콜을 지원하는 라이브러리.

서버 설계

서버는 클라이언트의 연결을 수락하고, 메시지를 중계합니다. 각 클라이언트는 WebSocket을 통해 서버에 연결되며, 서버는 연결된 모든 클라이언트에게 메시지를 전달합니다.

 

클래스 다이어그램

+-------------------+
|     ChatServer    |
+-------------------+
| +start()          |
| +stop()           |
| -accept()         |
| -read()           |
| -write()          |
+-------------------+
       |
       v
+-------------------+
|   ClientSession   |
+-------------------+
| +start()          |
| +send()           |
| -read()           |
| -write()          |
+-------------------+

 

클라이언트 설계

클라이언트는 사용자의 입력을 받아 서버에 메시지를 전송하고, 서버로부터 메시지를 수신하여 화면에 출력합니다.

 

클래스 다이어그램

+-------------------+
|   ChatClient      |
+-------------------+
| +connect()        |
| +disconnect()     |
| +sendMessage()    |
| -read()           |
| -write()          |
+-------------------+

 

코드 구현

ChatServer.h

#ifndef CHATSERVER_H
#define CHATSERVER_H

#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <unordered_set>

namespace net = boost::asio;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;

class ClientSession;

class ChatServer : public std::enable_shared_from_this<ChatServer> {
public:
    ChatServer(net::io_context& ioc, tcp::endpoint endpoint)
        : acceptor_(ioc), socket_(ioc) {
        acceptor_.open(endpoint.protocol());
        acceptor_.set_option(net::socket_base::reuse_address(true));
        acceptor_.bind(endpoint);
        acceptor_.listen();
        do_accept();
    }

    void broadcast(const std::string& message);
    void join(std::shared_ptr<ClientSession> session);
    void leave(std::shared_ptr<ClientSession> session);

private:
    void do_accept();

    tcp::acceptor acceptor_;
    tcp::socket socket_;
    std::unordered_set<std::shared_ptr<ClientSession>> sessions_;
};

#endif // CHATSERVER_H

 

ClientSession.h

#ifndef CLIENTSESSION_H
#define CLIENTSESSION_H

#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include "ChatServer.h"

namespace net = boost::asio;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;

class ClientSession : public std::enable_shared_from_this<ClientSession> {
public:
    ClientSession(tcp::socket socket, ChatServer& server)
        : ws_(std::move(socket)), server_(server) {}

    void start();
    void send(const std::string& message);

private:
    void do_read();
    void do_write();

    websocket::stream<tcp::socket> ws_;
    ChatServer& server_;
    boost::beast::flat_buffer buffer_;
    std::vector<std::string> write_msgs_;
};

#endif // CLIENTSESSION_H

 

ChatServer.cpp

#include "ChatServer.h"
#include "ClientSession.h"

void ChatServer::broadcast(const std::string& message) {
    for (auto& session : sessions_) {
        session->send(message);
    }
}

void ChatServer::join(std::shared_ptr<ClientSession> session) {
    sessions_.insert(session);
}

void ChatServer::leave(std::shared_ptr<ClientSession> session) {
    sessions_.erase(session);
}

void ChatServer::do_accept() {
    acceptor_.async_accept(socket_, [this](boost::system::error_code ec) {
        if (!ec) {
            auto session = std::make_shared<ClientSession>(std::move(socket_), *this);
            session->start();
        }
        do_accept();
    });
}

 

ClientSession.cpp

#include "ClientSession.h"

void ClientSession::start() {
    server_.join(shared_from_this());
    do_read();
}

void ClientSession::send(const std::string& message) {
    auto self(shared_from_this());
    net::post(ws_.get_executor(), [this, self, message]() {
        bool write_in_progress = !write_msgs_.empty();
        write_msgs_.push_back(message);
        if (!write_in_progress) {
            do_write();
        }
    });
}

void ClientSession::do_read() {
    auto self(shared_from_this());
    ws_.async_read(buffer_, [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
            auto message = boost::beast::buffers_to_string(buffer_.data());
            server_.broadcast(message);
            buffer_.consume(length);
            do_read();
        } else {
            server_.leave(shared_from_this());
        }
    });
}

void ClientSession::do_write() {
    auto self(shared_from_this());
    ws_.async_write(net::buffer(write_msgs_.front()), [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
            write_msgs_.erase(write_msgs_.begin());
            if (!write_msgs_.empty()) {
                do_write();
            }
        } else {
            server_.leave(shared_from_this());
        }
    });
}

 

main.cpp

#include <boost/asio.hpp>
#include "ChatServer.h"

int main() {
    try {
        net::io_context ioc{1};
        tcp::endpoint endpoint{net::ip::make_address("0.0.0.0"), 12345};

        std::make_shared<ChatServer>(ioc, endpoint)->run();
        ioc.run();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

 

설명

위의 코드는 간단한 실시간 채팅 애플리케이션의 서버를 구현한 예제입니다.

  • ChatServer 클래스:
    • 클라이언트 세션을 관리하고, 메시지를 브로드캐스트합니다.
    • broadcast(), join(), leave() 메서드를 통해 세션 관리와 메시지 브로드캐스트를 수행합니다.
    • do_accept() 메서드는 클라이언트 연결을 수락합니다.
  • ClientSession 클래스:
    • 각 클라이언트와의 세션을 관리합니다.
    • start() 메서드는 세션을 시작하고, send() 메서드는 메시지를 전송합니다.
    • do_read() 메서드는 클라이언트로부터 메시지를 읽고, do_write() 메서드는 메시지를 클라이언트에 씁니다.

 

이제 스물두 번째 날의 학습을 마쳤습니다. 실시간 채팅 애플리케이션의 기본 설계와 Boost.Asio와 Boost.Beast를 사용하여 서버를 구현하는 방법을 학습했습니다.

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 클라이언트 개발(1)"에 대해 학습하겠습니다.

반응형