From 1d103d7f16b1fbb46dcf7d3c5c9ab130f1a848fe Mon Sep 17 00:00:00 2001 From: bumpsoo Date: Wed, 4 Feb 2026 13:16:36 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20server,=20client=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=A1=B0=EC=A0=95=20=EB=B0=8F=20client=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80.?= =?UTF-8?q?=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20=ED=98=B9=EC=9D=80=20?= =?UTF-8?q?=EB=8C=80=ED=99=94=ED=98=95=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 15 +-- client/Client.cpp | 170 ++++++++++++++++++++++++++++ client/main.cpp | 39 +++++++ include/Client.h | 59 ++++++++++ {src => server}/DatabaseManager.cpp | 6 - {src => server}/NetworkService.cpp | 0 {src => server}/PacketHandler.cpp | 1 - {src => server}/Session.cpp | 1 - {src => server}/SessionManager.cpp | 1 - {src => server}/main.cpp | 0 test_scenario.txt | 5 + tests/Client.cpp | 96 ---------------- 12 files changed, 281 insertions(+), 112 deletions(-) create mode 100644 client/Client.cpp create mode 100644 client/main.cpp create mode 100644 include/Client.h rename {src => server}/DatabaseManager.cpp (95%) rename {src => server}/NetworkService.cpp (100%) rename {src => server}/PacketHandler.cpp (98%) rename {src => server}/Session.cpp (98%) rename {src => server}/SessionManager.cpp (96%) rename {src => server}/main.cpp (100%) create mode 100644 test_scenario.txt delete mode 100644 tests/Client.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e4d571b..26f9ed8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,19 +14,20 @@ find_package(OpenSSL REQUIRED) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_executable(Server - src/main.cpp - src/NetworkService.cpp - src/Session.cpp - src/PacketHandler.cpp - src/SessionManager.cpp - src/DatabaseManager.cpp + server/main.cpp + server/NetworkService.cpp + server/Session.cpp + server/PacketHandler.cpp + server/SessionManager.cpp + server/DatabaseManager.cpp ) target_include_directories(Server PRIVATE include) target_link_libraries(Server PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto) add_executable(Client - tests/Client.cpp + client/Client.cpp + client/main.cpp ) target_include_directories(Client PRIVATE include) target_link_libraries(Client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto) diff --git a/client/Client.cpp b/client/Client.cpp new file mode 100644 index 0000000..3ee0b2c --- /dev/null +++ b/client/Client.cpp @@ -0,0 +1,170 @@ +#include "Client.h" +#include +#include + +BaseClient::BaseClient(boost::asio::io_context &io_context, + const std::string &host, const std::string &port) + : socket_(io_context) { + tcp::resolver resolver(io_context); + boost::asio::connect(socket_, resolver.resolve(host, port)); +} + +void BaseClient::SendPacket(PacketID id, const void *payload, + size_t payloadSize) { + PacketHeader header; + header.id = static_cast(id); + header.size = static_cast(payloadSize); + + std::vector buffer(sizeof(PacketHeader) + payloadSize); + std::memcpy(buffer.data(), &header, sizeof(PacketHeader)); + if (payload && payloadSize > 0) { + std::memcpy(buffer.data() + sizeof(PacketHeader), payload, payloadSize); + } + + boost::asio::write(socket_, boost::asio::buffer(buffer)); +} + +bool BaseClient::ReceiveHeader(PacketHeader &header) { + try { + boost::asio::read(socket_, boost::asio::buffer(&header, sizeof(header))); + return true; + } catch (...) { + return false; + } +} + +void BaseClient::Login(const std::string &nickname) { + PKT_CS_Login loginPkt; + std::memset(loginPkt.nickname, 0, sizeof(loginPkt.nickname)); + std::strncpy(loginPkt.nickname, nickname.c_str(), + sizeof(loginPkt.nickname) - 1); + SendPacket(PacketID::Login, &loginPkt, sizeof(loginPkt)); + std::cout << "로그인 요청 보냄: " << nickname << std::endl; +} + +void BaseClient::HandleUpgradeResult() { + PKT_SC_UpgradeResult res; + if (ReceivePayload(res)) { + currentLevel_ = res.currentLevel; + currentGold_ = res.currentGold; + std::string resultStr = + (res.result == 1) ? "성공" : (res.result == 0 ? "파괴!!!" : "실패"); + std::cout << ">> 강화 결과: " << resultStr + << " (현재레벨: " << currentLevel_ + << ", 남은골드: " << currentGold_ << ")" << std::endl; + } +} + +void BaseClient::HandleSellResult() { + PKT_SC_SellResult res; + if (ReceivePayload(res)) { + currentLevel_ = 0; + currentGold_ = res.totalGold; + std::cout << ">> 판매 결과: " << res.earnedGold + << " 골드 획득! (총 골드: " << currentGold_ << ")" << std::endl; + } +} + +void InteractiveClient::Run() { + std::cout << "닉네임을 입력하세요: "; + std::cin >> nickname_; + Login(nickname_); + + while (true) { + if (currentLevel_ == 0) { + std::cout << "\n[현재 검 없음] 1. 검 구매 시도, 2. 종료\n입력: "; + } else { + std::cout << "\n[현재 검 레벨: " << currentLevel_ + << ", 골드: " << currentGold_ + << "] 1. 강화 시도, 2. 판매, 3. 종료\n입력: "; + } + + int choice; + if (!(std::cin >> choice)) + break; + + if (currentLevel_ == 0) { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_UpgradeResult)) { + HandleUpgradeResult(); + } + } else if (choice == 2) { + break; + } + } else { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_UpgradeResult)) { + HandleUpgradeResult(); + } + } else if (choice == 2) { + SendPacket(PacketID::CS_SellSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_SellResult)) { + HandleSellResult(); + } + } else if (choice == 3) { + break; + } + } + } +} + +#include + +void ScenarioClient::Run() { + std::ifstream is(scenarioFile_); + if (!is.is_open()) { + std::cerr << "시나리오 파일을 열 수 없습니다: " << scenarioFile_ + << std::endl; + return; + } + + if (!(is >> nickname_)) + return; + Login(nickname_); + + std::string line; + while (is >> line) { + int choice = std::stoi(line); + std::cout << "\n시나리오 입력 실행: " << choice << std::endl; + + if (currentLevel_ == 0) { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_UpgradeResult)) { + HandleUpgradeResult(); + } + } else if (choice == 2) { + break; + } + } else { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_UpgradeResult)) { + HandleUpgradeResult(); + } + } else if (choice == 2) { + SendPacket(PacketID::CS_SellSword); + PacketHeader header; + if (ReceiveHeader(header) && + header.id == static_cast(PacketID::SC_SellResult)) { + HandleSellResult(); + } + } else if (choice == 3) { + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..e94f85b --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,39 @@ +#include "Client.h" +#include + +int main(int argc, char *argv[]) { + bool isScenario = false; + std::string nickname; + std::string scenarioFile; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--scenario" && i + 1 < argc) { + isScenario = true; + scenarioFile = argv[++i]; + } else { + nickname = arg; + } + } + + try { + boost::asio::io_context io_context; + if (isScenario) { + if (scenarioFile.empty()) { + std::cerr + << "시나리오 파일명이 필요합니다. 사용법: --scenario <파일경로>" + << std::endl; + return 1; + } + ScenarioClient client(io_context, "127.0.0.1", "30000", scenarioFile); + client.Run(); + } else { + InteractiveClient client(io_context, "127.0.0.1", "30000"); + client.Run(); + } + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/include/Client.h b/include/Client.h new file mode 100644 index 0000000..34e2325 --- /dev/null +++ b/include/Client.h @@ -0,0 +1,59 @@ +#pragma once + +#include "Packet.h" +#include +#include + +using boost::asio::ip::tcp; + +class BaseClient { +public: + BaseClient(boost::asio::io_context &io_context, const std::string &host, + const std::string &port); + + virtual ~BaseClient() = default; + + void SendPacket(PacketID id, const void *payload = nullptr, + size_t payloadSize = 0); + + bool ReceiveHeader(PacketHeader &header); + + template bool ReceivePayload(T &payload) { + try { + boost::asio::read(socket_, boost::asio::buffer(&payload, sizeof(T))); + return true; + } catch (...) { + return false; + } + } + + void Login(const std::string &nickname); + + virtual void Run() = 0; + +protected: + tcp::socket socket_; + uint32_t currentLevel_ = 0; + uint64_t currentGold_ = 0; + std::string nickname_; + + void HandleUpgradeResult(); + void HandleSellResult(); +}; + +class InteractiveClient : public BaseClient { +public: + using BaseClient::BaseClient; + void Run() override; +}; + +class ScenarioClient : public BaseClient { +public: + ScenarioClient(boost::asio::io_context &io_context, const std::string &host, + const std::string &port, const std::string &scenarioFile) + : BaseClient(io_context, host, port), scenarioFile_(scenarioFile) {} + void Run() override; + +private: + std::string scenarioFile_; +}; diff --git a/src/DatabaseManager.cpp b/server/DatabaseManager.cpp similarity index 95% rename from src/DatabaseManager.cpp rename to server/DatabaseManager.cpp index dff989c..4374a32 100644 --- a/src/DatabaseManager.cpp +++ b/server/DatabaseManager.cpp @@ -41,13 +41,11 @@ bool DatabaseManager::Init(boost::asio::io_context &io_context, boost::asio::awaitable DatabaseManager::Lock() { auto executor = co_await boost::asio::this_coro::executor; - // 이 체크 로직 자체가 strand 위에서 안전하게 돌아야 함 co_await boost::asio::post( boost::asio::bind_executor(*strand_, boost::asio::use_awaitable)); if (!is_locked_) { is_locked_ = true; - // 즉시 획득 co_return; } @@ -72,7 +70,6 @@ void DatabaseManager::Unlock() { } else { auto next = waiting_queue_.front(); waiting_queue_.pop(); - // 다음 대기자를 깨움 next->cancel(); } }); @@ -80,7 +77,6 @@ void DatabaseManager::Unlock() { boost::asio::awaitable DatabaseManager::LoadUser(std::string nickname) { - // 줄 서기 co_await Lock(); try { @@ -126,7 +122,6 @@ DatabaseManager::LoadUser(std::string nickname) { boost::asio::awaitable DatabaseManager::SaveUser(std::string nickname, uint64_t gold, uint32_t swordLevel) { - // 줄 서기 co_await Lock(); try { @@ -146,7 +141,6 @@ boost::asio::awaitable DatabaseManager::SaveUser(std::string nickname, Logger::Log("SaveUser 에러: ", e.what()); } - // 다음 사람 들어오세요 Unlock(); co_return; } diff --git a/src/NetworkService.cpp b/server/NetworkService.cpp similarity index 100% rename from src/NetworkService.cpp rename to server/NetworkService.cpp diff --git a/src/PacketHandler.cpp b/server/PacketHandler.cpp similarity index 98% rename from src/PacketHandler.cpp rename to server/PacketHandler.cpp index 6354698..74515f1 100644 --- a/src/PacketHandler.cpp +++ b/server/PacketHandler.cpp @@ -28,7 +28,6 @@ PacketHandler::HandlePacket(std::shared_ptr session, reinterpret_cast(packet.payload.data()); std::string nickname(loginPkt->nickname); - // DB에서 유저 정보 로드 (co_await 활용!) auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname); session->SetNickname(userData.nickname); diff --git a/src/Session.cpp b/server/Session.cpp similarity index 98% rename from src/Session.cpp rename to server/Session.cpp index 44bd80c..eaf67b3 100644 --- a/src/Session.cpp +++ b/server/Session.cpp @@ -63,7 +63,6 @@ void Session::DoReadBody() { socket_, boost::asio::buffer(packetBody_.data(), packetBody_.size()), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { - // 논리 패킷 구성 Packet packet; packet.header = packetHeader_; packet.payload = packetBody_; diff --git a/src/SessionManager.cpp b/server/SessionManager.cpp similarity index 96% rename from src/SessionManager.cpp rename to server/SessionManager.cpp index be45c19..8a89d84 100644 --- a/src/SessionManager.cpp +++ b/server/SessionManager.cpp @@ -25,7 +25,6 @@ void SessionManager::Broadcast(PacketHeader header, std::span body) { std::lock_guard lock(mutex_); - // 패킷 직렬화 수행 (헤더 + 바디) std::vector buffer(sizeof(PacketHeader) + body.size()); std::memcpy(buffer.data(), &header, sizeof(PacketHeader)); std::memcpy(buffer.data() + sizeof(PacketHeader), body.data(), body.size()); diff --git a/src/main.cpp b/server/main.cpp similarity index 100% rename from src/main.cpp rename to server/main.cpp diff --git a/test_scenario.txt b/test_scenario.txt new file mode 100644 index 0000000..1291a8d --- /dev/null +++ b/test_scenario.txt @@ -0,0 +1,5 @@ +TestUser +1 +1 +2 +3 diff --git a/tests/Client.cpp b/tests/Client.cpp deleted file mode 100644 index 7029dbd..0000000 --- a/tests/Client.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "Packet.h" -#include -#include -#include -#include - -using boost::asio::ip::tcp; - -void SendPacket(tcp::socket &socket, PacketID id, const void *payload = nullptr, - size_t payloadSize = 0) { - PacketHeader header; - header.id = static_cast(id); - header.size = static_cast(payloadSize); - - std::vector buffer(sizeof(PacketHeader) + payloadSize); - std::memcpy(buffer.data(), &header, sizeof(PacketHeader)); - if (payload && payloadSize > 0) { - std::memcpy(buffer.data() + sizeof(PacketHeader), payload, payloadSize); - } - - boost::asio::write(socket, boost::asio::buffer(buffer)); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - std::cout << "사용법: " << argv[0] << " <닉네임>" << std::endl; - return 1; - } - - std::string nickname = argv[1]; - - try { - boost::asio::io_context io_context; - tcp::socket socket(io_context); - tcp::resolver resolver(io_context); - boost::asio::connect(socket, resolver.resolve("127.0.0.1", "30000")); - - std::cout << "서버에 연결되었습니다! (닉네임: " << nickname << ")" - << std::endl; - - // 1. 로그인 패킷 전송 - PKT_CS_Login loginPkt; - std::memset(loginPkt.nickname, 0, sizeof(loginPkt.nickname)); - std::strncpy(loginPkt.nickname, nickname.c_str(), - sizeof(loginPkt.nickname) - 1); - SendPacket(socket, PacketID::Login, &loginPkt, sizeof(loginPkt)); - std::cout << "로그인 요청 보냄" << std::endl; - - // 2. 강화 테스트 (5번 시도) - for (int i = 0; i < 5; ++i) { - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - SendPacket(socket, PacketID::CS_UpgradeSword); - std::cout << i + 1 << "번째 강화 시도..." << std::endl; - - // 결과 수신 - PacketHeader resHeader; - boost::asio::read(socket, - boost::asio::buffer(&resHeader, sizeof(resHeader))); - - if (resHeader.id == static_cast(PacketID::SC_UpgradeResult)) { - PKT_SC_UpgradeResult res; - boost::asio::read(socket, boost::asio::buffer(&res, sizeof(res))); - - std::string resultStr = - (res.result == 1) ? "성공" : (res.result == 0 ? "파괴!!!" : "실패"); - std::cout << ">> 강화 결과: " << resultStr - << " (현재레벨: " << res.currentLevel - << ", 남은골드: " << res.currentGold << ")" << std::endl; - } - } - - // 3. 판매 테스트 - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - SendPacket(socket, PacketID::CS_SellSword); - std::cout << "검 판매 시도..." << std::endl; - - // 결과 수신 - PacketHeader sellResHeader; - boost::asio::read( - socket, boost::asio::buffer(&sellResHeader, sizeof(sellResHeader))); - if (sellResHeader.id == static_cast(PacketID::SC_SellResult)) { - PKT_SC_SellResult res; - boost::asio::read(socket, boost::asio::buffer(&res, sizeof(res))); - std::cout << ">> 판매 결과: " << res.earnedGold - << " 골드 획득! (총 골드: " << res.totalGold << ")" - << std::endl; - } - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - } catch (std::exception &e) { - std::cerr << "Exception: " << e.what() << "\n"; - } - - return 0; -}