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

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

by cogito21_cpp 2024. 8. 1.
반응형

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

이전 단계에서는 기본적인 메시지 중계와 사용자 이름 관리를 구현했습니다. 이번 단계에서는 서버에 더 많은 기능을 추가하고, 개선하겠습니다.

추가 기능 요구사항

  1. 사용자 목록 관리: 현재 접속해 있는 사용자 목록을 관리하고 클라이언트에게 제공해야 합니다.
  2. 연결 및 연결 해제 알림: 사용자가 연결되거나 연결이 해제될 때 모든 사용자에게 알림을 보냅니다.
  3. 메시지 포맷 관리: JSON 형식의 메시지를 사용하여 메시지를 구조화합니다.

서버 클래스 다이어그램 (업데이트)

+-------------------+
|     ChatServer    |
+-------------------+
| +start()          |
| +stop()           |
| -accept()         |
| -read()           |
| -write()          |
| -broadcast()      |
| -updateUserList() |
+-------------------+
       |
       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>
#include <nlohmann/json.hpp>

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 nlohmann::json& message);
    void join(std::shared_ptr<ClientSession> session, const std::string& username);
    void leave(std::shared_ptr<ClientSession> session);
    void updateUserList();

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 nlohmann::json& 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 nlohmann::json& 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;

    // 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
    nlohmann::json notification;
    notification["type"] = "join";
    notification["username"] = username;
    broadcast(notification);

    updateUserList();
}

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) {
            // 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
            nlohmann::json notification;
            notification["type"] = "leave";
            notification["username"] = it->first;
            broadcast(notification);

            user_sessions_.erase(it);
            break;
        }
    }

    updateUserList();
}

void ChatServer::updateUserList() {
    nlohmann::json user_list_message;
    user_list_message["type"] = "user_list";
    for (const auto& user_session : user_sessions_) {
        user_list_message["users"].push_back(user_session.first);
    }
    broadcast(user_list_message);
}

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 nlohmann::json& 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.dump());
        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")) {
        nlohmann::json broadcast_message;
        broadcast_message["type"] = "message";
        broadcast_message["username"] = username_;
        broadcast_message["message"] = json_message["message"];
        server_.broadcast(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() 메서드는 클라이언트가 서버에서 연결을 종료할 때 호출되며, 연결 해제 알림을 브로드캐스트합니다.
    • updateUserList() 메서드는 현재 연결된 사용자 목록을 업데이트하고, 모든 사용자에게 전송합니다.
  • ClientSession 클래스:
    • start() 메서드는 세션을 시작합니다.
    • send() 메서드는 서버에서 클라이언트로 메시지를 전송합니다.
    • do_read() 메서드는 클라이언트로부터 메시지를 읽습니다.
    • do_write() 메서드는 클라이언트에게 메시지를 씁니다.
    • handle_message() 메서드는 수신된 메시지를 처리합니다. 사용자 이름과 메시지를 구분하여 처리합니다.

이제 스물여섯 번째 날의 학습을 마쳤습니다. 실시간 채팅 애플리케이션의 서버에 사용자 목록 관리, 연결 및 연결 해제 알림 기능을 추가하는 방법을 학습했습니다.

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 실시간 채팅 기능 구현"에 대해 학습하겠습니다.

반응형