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

[C++ 네트워크 프로그래밍] Day 27: 프로젝트: 실시간 채팅 기능 구현

by cogito21_cpp 2024. 8. 1.
반응형

실시간 채팅 애플리케이션 기능 구현

이전 단계에서 사용자 목록 관리 및 연결/해제 알림 기능을 서버에 추가했습니다. 이번 단계에서는 실시간 채팅 기능을 구현하여, 사용자들이 메시지를 주고받을 수 있도록 하겠습니다.

기능 요구사항

  1. 메시지 전송: 사용자가 메시지를 입력하면 서버를 통해 다른 모든 사용자에게 전송됩니다.
  2. 메시지 수신: 서버로부터 메시지를 수신하고 이를 사용자에게 표시합니다.
  3. JSON 메시지 형식: 메시지는 JSON 형식으로 전송되며, 사용자 이름과 메시지를 포함합니다.

서버 코드 업데이트

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

 

클라이언트 코드 업데이트

ChatClient.h

#ifndef CHATCLIENT_H
#define CHATCLIENT_H

#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <string>
#include <queue>
#include <iostream>

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

class ChatClient : public std::enable_shared_from_this<ChatClient> {
public:
    ChatClient(net::io_context& ioc, const tcp::resolver::results_type& endpoints)
        : resolver_(ioc), ws_(ioc), endpoints_(endpoints) {}

    void connect();
    void disconnect();
    void sendMessage(const std::string& message);
    void setUsername(const std::string& username);

private:
    void doRead();
    void doWrite();
    std::string formatMessage(const std::string& message);

    tcp::resolver resolver_;
    websocket::stream<tcp::socket> ws_;
    tcp::resolver::results_type endpoints_;
    boost::beast::flat_buffer buffer_;
    std::queue<std::string> writeMessages_;
    std::string username_;
};

#endif // CHATCLIENT_H

 

ChatClient.cpp

#include "ChatClient.h"
#include <nlohmann/json.hpp>

void ChatClient::connect() {
    auto self(shared_from_this());
    net::async_connect(ws_.next_layer(), endpoints_,
        [this, self](boost::system::error_code ec, tcp::endpoint) {
            if (!ec) {
                ws_.async_handshake("localhost", "/",
                    [this, self](boost::system::error_code ec) {
                        if (!ec) {
                            doRead();
                        } else {
                            std::cerr << "Handshake failed: " << ec.message() << std::endl;
                        }
                    });
            } else {
                std::cerr << "Connect failed: " << ec.message() << std::endl;
            }
        });
}

void ChatClient::disconnect() {
    auto self(shared_from_this());
    ws_.async_close(websocket::close_code::normal,
        [this, self](boost::system::error_code ec) {
            if (ec) {
                std::cerr << "Close failed: " << ec.message() << std::endl;
            }
        });
}

void ChatClient::sendMessage(const std::string& message) {
    auto self(shared_from_this());
    net::post(ws_.get_executor(), [

this, self, message]() {
        bool writeInProgress = !writeMessages_.empty();
        writeMessages_.push(formatMessage(message));
        if (!writeInProgress) {
            doWrite();
        }
    });
}

void ChatClient::setUsername(const std::string& username) {
    username_ = username;
}

std::string ChatClient::formatMessage(const std::string& message) {
    nlohmann::json jsonMessage;
    jsonMessage["username"] = username_;
    jsonMessage["message"] = message;
    return jsonMessage.dump();
}

void ChatClient::doRead() {
    auto self(shared_from_this());
    ws_.async_read(buffer_,
        [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                std::cout << "Received: " << boost::beast::buffers_to_string(buffer_.data()) << std::endl;
                buffer_.consume(length);
                doRead();
            } else {
                std::cerr << "Read failed: " << ec.message() << std::endl;
                disconnect();
            }
        });
}

void ChatClient::doWrite() {
    auto self(shared_from_this());
    ws_.async_write(net::buffer(writeMessages_.front()),
        [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                writeMessages_.pop();
                if (!writeMessages_.empty()) {
                    doWrite();
                }
            } else {
                std::cerr << "Write failed: " << ec.message() << std::endl;
                disconnect();
            }
        });
}

 

main.cpp

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

int main() {
    try {
        net::io_context ioc;
        tcp::resolver resolver(ioc);
        auto endpoints = resolver.resolve("localhost", "12345");

        auto client = std::make_shared<ChatClient>(ioc, endpoints);

        std::cout << "Enter your username: ";
        std::string username;
        std::getline(std::cin, username);
        client->setUsername(username);

        client->connect();

        std::thread t([&ioc]() { ioc.run(); });

        std::string message;
        while (std::getline(std::cin, message)) {
            client->sendMessage(message);
        }

        client->disconnect();
        t.join();
    } 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() 메서드는 수신된 메시지를 처리합니다. 사용자 이름과 메시지를 구분하여 처리합니다.
  • ChatClient 클래스:
    • connect() 메서드는 서버에 연결을 설정합니다.
    • disconnect() 메서드는 서버와의 연결을 종료합니다.
    • sendMessage() 메서드는 사용자의 메시지를 서버로 전송합니다.
    • setUsername() 메서드는 사용자의 이름을 설정합니다.
    • formatMessage() 메서드는 메시지를 JSON 형식으로 변환합니다.
    • doRead() 메서드는 서버로부터 메시지를 읽습니다.
    • doWrite() 메서드는 서버로 메시지를 씁니다.

이제 스물일곱 번째 날의 학습을 마쳤습니다. 실시간 채팅 애플리케이션의 서버와 클라이언트를 완성하는 방법을 학습했습니다.

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 보안 기능 추가"에 대해 학습하겠습니다.

반응형