feat: server, client 디렉토리 조정 및 client 헤더 파일 추가. 시나리오

혹은 대화형 클라이언트로 수정
This commit is contained in:
bumpsoo 2026-02-04 13:16:36 +00:00
parent b887f15662
commit 1d103d7f16
12 changed files with 281 additions and 112 deletions

View file

@ -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)

170
client/Client.cpp Normal file
View file

@ -0,0 +1,170 @@
#include "Client.h"
#include <iostream>
#include <thread>
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<uint16_t>(id);
header.size = static_cast<uint16_t>(payloadSize);
std::vector<uint8_t> 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<uint16_t>(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<uint16_t>(PacketID::SC_UpgradeResult)) {
HandleUpgradeResult();
}
} else if (choice == 2) {
SendPacket(PacketID::CS_SellSword);
PacketHeader header;
if (ReceiveHeader(header) &&
header.id == static_cast<uint16_t>(PacketID::SC_SellResult)) {
HandleSellResult();
}
} else if (choice == 3) {
break;
}
}
}
}
#include <fstream>
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<uint16_t>(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<uint16_t>(PacketID::SC_UpgradeResult)) {
HandleUpgradeResult();
}
} else if (choice == 2) {
SendPacket(PacketID::CS_SellSword);
PacketHeader header;
if (ReceiveHeader(header) &&
header.id == static_cast<uint16_t>(PacketID::SC_SellResult)) {
HandleSellResult();
}
} else if (choice == 3) {
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

39
client/main.cpp Normal file
View file

@ -0,0 +1,39 @@
#include "Client.h"
#include <iostream>
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;
}

59
include/Client.h Normal file
View file

@ -0,0 +1,59 @@
#pragma once
#include "Packet.h"
#include <boost/asio.hpp>
#include <string>
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 <typename T> 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_;
};

View file

@ -41,13 +41,11 @@ bool DatabaseManager::Init(boost::asio::io_context &io_context,
boost::asio::awaitable<void> 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::UserData>
DatabaseManager::LoadUser(std::string nickname) {
// 줄 서기
co_await Lock();
try {
@ -126,7 +122,6 @@ DatabaseManager::LoadUser(std::string nickname) {
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
uint64_t gold,
uint32_t swordLevel) {
// 줄 서기
co_await Lock();
try {
@ -146,7 +141,6 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
Logger::Log("SaveUser 에러: ", e.what());
}
// 다음 사람 들어오세요
Unlock();
co_return;
}

View file

@ -28,7 +28,6 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
reinterpret_cast<const PKT_CS_Login *>(packet.payload.data());
std::string nickname(loginPkt->nickname);
// DB에서 유저 정보 로드 (co_await 활용!)
auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname);
session->SetNickname(userData.nickname);

View file

@ -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_;

View file

@ -25,7 +25,6 @@ void SessionManager::Broadcast(PacketHeader header,
std::span<const uint8_t> body) {
std::lock_guard<std::mutex> lock(mutex_);
// 패킷 직렬화 수행 (헤더 + 바디)
std::vector<uint8_t> buffer(sizeof(PacketHeader) + body.size());
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
std::memcpy(buffer.data() + sizeof(PacketHeader), body.data(), body.size());

5
test_scenario.txt Normal file
View file

@ -0,0 +1,5 @@
TestUser
1
1
2
3

View file

@ -1,96 +0,0 @@
#include "Packet.h"
#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <vector>
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<uint16_t>(id);
header.size = static_cast<uint16_t>(payloadSize);
std::vector<uint8_t> 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<uint16_t>(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<uint16_t>(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;
}