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

[C++ 네트워크 프로그래밍] Day 25: 프로젝트: 서버 개발 (1)

by cogito21_cpp 2024. 8. 1.
반응형

실시간 채팅 애플리케이션 서버 개발 (계속)

이전 단계에서는 클라이언트 부분을 개발했습니다. 이번 단계에서는 서버 부분을 더 발전시키고, 사용자 이름과 메시지를 관리하는 기능을 추가하겠습니다.

서버 기능 요구사항

  1. 사용자 연결 관리: 여러 사용자가 동시에 서버에 연결할 수 있어야 합니다.
  2. 메시지 중계: 서버는 클라이언트로부터 메시지를 받아 다른 모든 클라이언트에게 중계합니다.
  3. 사용자 이름 관리: 각 사용자는 고유한 사용자 이름을 가지고 있어야 합니다.
  4. 메시지 형식 관리: JSON 형식의 메시지를 처리하고, 사용자 이름과 메시지를 구분합니다.

서버 클래스 다이어그램

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

서버 코드 구현

ChatServer.h

#ifndef CHATSERVER_H
#define CHATSERVER_H

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

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, const std::string& username);
    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_;
    std::unordered_map<std::string, std::shared_ptr<ClientSession>> user_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();
    void handle_message(const std::string& message);

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

#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, const std::string& username) {
    sessions_.insert(session);
    user_sessions_[username] = session;
}

void ChatServer::leave(std::shared_ptr<ClientSession> session) {
    sessions_.erase(session);
    for (auto it = user_sessions_.begin(); it != user_sessions_.end(); ++it) {
        if (it->second == session) {
            user_sessions_.erase(it);
            break;
        }
    }
}

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"
#include <nlohmann/json.hpp>

void ClientSession::start() {
    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());
            handle_message(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());
        }
    });
}

void ClientSession::handle_message(const std::string& message) {
    auto json_message = nlohmann::json::parse(message);
    if (json_message.contains("username")) {
        username_ = json_message["username"].get<std::string>();
        server_.join(shared_from_this(), username_);
    }
    if (json_message.contains("message")) {
        server_.broadcast(message);
    }
}

 

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};

        auto server = std::make_shared<ChatServer>(ioc, endpoint);

        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() 메서드는 클라이언트에게 메시지를 씁니다.
    • handle_message() 메서드는 수신된 메시지를 처리합니다.

이제 스물다섯 번째 날의 학습을 마쳤습니다. 실시간 채팅 애플리케이션의 서버에 사용자 이름 설정과 메시지 중계 기능을 추가하는 방법을 학습했습니다.

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

반응형