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

146
server/DatabaseManager.cpp Normal file
View file

@ -0,0 +1,146 @@
#include "DatabaseManager.h"
#include "Logger.h"
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/mysql.hpp>
DatabaseManager &DatabaseManager::GetInstance() {
static DatabaseManager instance;
return instance;
}
bool DatabaseManager::Init(boost::asio::io_context &io_context,
const std::string &host, uint16_t port,
const std::string &user, const std::string &password,
const std::string &db) {
try {
strand_ = std::make_unique<
boost::asio::strand<boost::asio::io_context::executor_type>>(
io_context.get_executor());
boost::asio::ip::tcp::resolver resolver(io_context.get_executor());
auto endpoints = resolver.resolve(host, std::to_string(port));
boost::mysql::handshake_params params(user, password, db);
static boost::asio::ssl::context ssl_ctx(
boost::asio::ssl::context::tlsv12_client);
conn_ = std::make_unique<boost::mysql::tcp_ssl_connection>(
io_context.get_executor(), ssl_ctx);
conn_->connect(*endpoints.begin(), params);
Logger::Log("Database 연결 성공 (AsyncLock 적용): ", host, ":", port);
return true;
} catch (const std::exception &e) {
Logger::Log("Database 연결 실패: ", e.what());
return false;
}
}
// 비동기 락 획득 (이미 락이 걸려있으면 대기열에 들어감)
boost::asio::awaitable<void> DatabaseManager::Lock() {
auto executor = co_await boost::asio::this_coro::executor;
co_await boost::asio::post(
boost::asio::bind_executor(*strand_, boost::asio::use_awaitable));
if (!is_locked_) {
is_locked_ = true;
co_return;
}
// 이미 잠겨있으면 타이머를 만들어서 대기열에 추가 (비동기 대기)
auto timer = std::make_shared<boost::asio::steady_timer>(
executor, std::chrono::steady_clock::time_point::max());
waiting_queue_.push(timer);
try {
co_await timer->async_wait(boost::asio::use_awaitable);
} catch (...) {
// 타이머 취소(Unlock에서 호출함)되면 성공적으로 락을 획득한 것으로 간주
}
co_return;
}
// 비동기 락 해제 (대기열에 다음 사람이 있으면 깨워줌)
void DatabaseManager::Unlock() {
boost::asio::dispatch(*strand_, [this]() {
if (waiting_queue_.empty()) {
is_locked_ = false;
} else {
auto next = waiting_queue_.front();
waiting_queue_.pop();
next->cancel();
}
});
}
boost::asio::awaitable<DatabaseManager::UserData>
DatabaseManager::LoadUser(std::string nickname) {
co_await Lock();
try {
if (!conn_) {
Unlock();
co_return UserData{nickname, 10000, 0};
}
boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
"SELECT username, gold, sword_level FROM users WHERE username = ?",
boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), result,
boost::asio::use_awaitable);
if (result.rows().empty()) {
boost::mysql::statement ins_stmt =
co_await conn_->async_prepare_statement(
"INSERT INTO users (username, gold, sword_level) VALUES (?, "
"10000, 0)",
boost::asio::use_awaitable);
co_await conn_->async_execute(ins_stmt.bind(nickname), result,
boost::asio::use_awaitable);
Logger::Log("새로운 유저 탄생 (DB): ", nickname);
Unlock();
co_return UserData{nickname, 10000, 0};
} else {
auto row = result.rows().at(0);
UserData data;
data.nickname = row.at(0).as_string();
data.gold = row.at(1).as_uint64();
data.swordLevel = (uint32_t)row.at(2).as_int64();
Unlock();
co_return data;
}
} catch (const std::exception &e) {
Logger::Log("LoadUser 에러: ", e.what());
Unlock();
co_return UserData{nickname, 10000, 0};
}
}
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
uint64_t gold,
uint32_t swordLevel) {
co_await Lock();
try {
if (!conn_) {
Unlock();
co_return;
}
boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
"UPDATE users SET gold = ?, sword_level = ? WHERE username = ?",
boost::asio::use_awaitable);
co_await conn_->async_execute(
stmt.bind(gold, (int64_t)swordLevel, nickname), result,
boost::asio::use_awaitable);
} catch (const std::exception &e) {
Logger::Log("SaveUser 에러: ", e.what());
}
Unlock();
co_return;
}

32
server/NetworkService.cpp Normal file
View file

@ -0,0 +1,32 @@
#include "NetworkService.h"
#include "Logger.h"
#include "Session.h"
NetworkService::NetworkService(uint16_t port, int threadCount)
: acceptor_(io_context_, tcp::endpoint(tcp::v4(), port)),
work_guard_(boost::asio::make_work_guard(io_context_)) {
threads_.reserve(threadCount);
for (int i = 0; i < threadCount; ++i) {
threads_.emplace_back([this]() { io_context_.run(); });
}
}
NetworkService::~NetworkService() { Stop(); }
void NetworkService::Run() {
Logger::Log("서버가 포트 ", acceptor_.local_endpoint().port(),
"에서 리스닝을 시작했습니다.");
DoAccept();
}
void NetworkService::Stop() { io_context_.stop(); }
void NetworkService::DoAccept() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket))->Start();
}
DoAccept();
});
}

131
server/PacketHandler.cpp Normal file
View file

@ -0,0 +1,131 @@
#include "PacketHandler.h"
#include "DatabaseManager.h"
#include "Logger.h"
#include "SessionManager.h"
#include "SwordLogic.h"
#include <boost/asio/use_awaitable.hpp>
#include <cstring>
#include <string>
boost::asio::awaitable<void>
PacketHandler::HandlePacket(std::shared_ptr<Session> session,
const Packet packet) {
switch (static_cast<PacketID>(packet.header.id)) {
case PacketID::Ping: {
Logger::Log("Ping 수신됨");
session->Send(std::span<const uint8_t>(
reinterpret_cast<const uint8_t *>(&packet.header),
sizeof(PacketHeader)));
} break;
case PacketID::Login: {
if (packet.payload.size() < sizeof(PKT_CS_Login)) {
Logger::Log("로그인 패킷 크기가 올바르지 않습니다.");
co_return;
}
const PKT_CS_Login *loginPkt =
reinterpret_cast<const PKT_CS_Login *>(packet.payload.data());
std::string nickname(loginPkt->nickname);
auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname);
session->SetNickname(userData.nickname);
session->SetGold(userData.gold);
session->SetSwordLevel(userData.swordLevel);
Logger::Log("클라이언트 로그인: ", nickname, " (Gold: ", session->GetGold(),
", Level: ", session->GetSwordLevel(), ")");
SessionManager::GetInstance().Join(session);
} break;
case PacketID::CS_UpgradeSword: {
uint32_t currentLevel = session->GetSwordLevel();
uint64_t cost = SwordLogic::GetUpgradeCost(currentLevel);
uint64_t currentGold = session->GetGold();
PKT_SC_UpgradeResult res;
if (currentGold < cost) {
// 골드 부족
res.result = 2;
} else {
session->SetGold(currentGold - cost);
uint8_t result = SwordLogic::TryUpgrade(currentLevel);
res.result = result;
// 성공
if (result == 1) {
session->SetSwordLevel(currentLevel + 1);
// 파괴
} else if (result == 0) {
session->SetSwordLevel(0);
}
co_await DatabaseManager::GetInstance().SaveUser(
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
}
res.currentLevel = session->GetSwordLevel();
res.currentGold = session->GetGold();
PacketHeader header;
header.id = static_cast<uint16_t>(PacketID::SC_UpgradeResult);
header.size = sizeof(PKT_SC_UpgradeResult);
std::vector<uint8_t> buffer(sizeof(PacketHeader) +
sizeof(PKT_SC_UpgradeResult));
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
std::memcpy(buffer.data() + sizeof(PacketHeader), &res,
sizeof(PKT_SC_UpgradeResult));
session->Send(buffer);
Logger::Log("강화 시도 [", session->GetNickname(), "]: ", (int)res.result,
" (레벨: ", currentLevel, "->", res.currentLevel, ")");
} break;
case PacketID::CS_SellSword: {
uint32_t currentLevel = session->GetSwordLevel();
uint64_t price = SwordLogic::GetSellPrice(currentLevel);
uint64_t newGold = session->GetGold() + price;
session->SetGold(newGold);
session->SetSwordLevel(0);
co_await DatabaseManager::GetInstance().SaveUser(
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
PKT_SC_SellResult res;
res.earnedGold = price;
res.totalGold = newGold;
PacketHeader header;
header.id = static_cast<uint16_t>(PacketID::SC_SellResult);
header.size = sizeof(PKT_SC_SellResult);
std::vector<uint8_t> buffer(sizeof(PacketHeader) +
sizeof(PKT_SC_SellResult));
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
std::memcpy(buffer.data() + sizeof(PacketHeader), &res,
sizeof(PKT_SC_SellResult));
session->Send(buffer);
Logger::Log("검 판매 [", session->GetNickname(), "]: ", price,
" 골드 획득");
} break;
case PacketID::Chat: {
std::string msg(packet.payload.begin(), packet.payload.end());
Logger::Log("채팅 [", session->GetNickname(), "]: ", msg);
SessionManager::GetInstance().Broadcast(packet.header, packet.payload);
} break;
default:
Logger::Log("알 수 없는 패킷 ID: ", packet.header.id);
break;
}
co_return;
}

100
server/Session.cpp Normal file
View file

@ -0,0 +1,100 @@
#include "Session.h"
#include "PacketHandler.h"
#include "SessionManager.h"
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
Session::Session(tcp::socket socket) : socket_(std::move(socket)) {}
Session::~Session() {
// 필요한 경우 정리 작업 수행
}
void Session::SetNickname(const std::string &nickname) { nickname_ = nickname; }
const std::string &Session::GetNickname() const { return nickname_; }
void Session::SetGold(uint64_t gold) { gold_ = gold; }
uint64_t Session::GetGold() const { return gold_; }
void Session::SetSwordLevel(uint32_t level) { swordLevel_ = level; }
uint32_t Session::GetSwordLevel() const { return swordLevel_; }
void Session::Start() { DoReadHeader(); }
void Session::Send(std::span<const uint8_t> data) {
auto self(shared_from_this());
boost::asio::post(
socket_.get_executor(),
[this, self,
msg = std::vector<uint8_t>(data.begin(), data.end())]() mutable {
bool writeInProgress = !writeQueue_.empty();
writeQueue_.push_back(std::move(msg));
if (!writeInProgress) {
DoWrite();
}
});
}
void Session::DoReadHeader() {
auto self(shared_from_this());
boost::asio::async_read(
socket_, boost::asio::buffer(&packetHeader_, sizeof(PacketHeader)),
[this, self](boost::system::error_code ec, std::size_t /*length*/) {
if (!ec) {
// 바디 크기 결정
// 안전 확인: 메모리 고갈을 방지하기 위해 최대 패킷 크기 제한
if (packetHeader_.size > 1024 * 10) {
socket_.close();
return;
}
packetBody_.resize(packetHeader_.size);
DoReadBody();
} else {
// 연결 끊김 또는 에러 발생 시 매니저에서 제거
SessionManager::GetInstance().Leave(self);
}
});
}
void Session::DoReadBody() {
auto self(shared_from_this());
boost::asio::async_read(
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_;
// 패킷 핸들러 코루틴 실행
boost::asio::co_spawn(
socket_.get_executor(),
PacketHandler::HandlePacket(self, std::move(packet)),
boost::asio::detached);
// 계속해서 읽기
DoReadHeader();
} else {
// 에러 발생 시 매니저에서 제거
SessionManager::GetInstance().Leave(self);
}
});
}
void Session::DoWrite() {
auto self(shared_from_this());
boost::asio::async_write(
socket_, boost::asio::buffer(writeQueue_.front()),
[this, self](boost::system::error_code ec, std::size_t /*length*/) {
if (!ec) {
writeQueue_.pop_front();
if (!writeQueue_.empty()) {
DoWrite();
}
} else {
// 에러 발생 시 매니저에서 제거
SessionManager::GetInstance().Leave(self);
}
});
}

37
server/SessionManager.cpp Normal file
View file

@ -0,0 +1,37 @@
#include "SessionManager.h"
#include "Logger.h"
SessionManager &SessionManager::GetInstance() {
static SessionManager instance;
return instance;
}
void SessionManager::Join(std::shared_ptr<Session> session) {
std::lock_guard<std::mutex> lock(mutex_);
sessions_.insert(session);
Logger::Log("클라이언트[", session->GetNickname(),
"]가 입장했습니다. 총 세션 수: ", sessions_.size());
}
void SessionManager::Leave(std::shared_ptr<Session> session) {
std::lock_guard<std::mutex> lock(mutex_);
if (sessions_.erase(session) > 0) {
Logger::Log("클라이언트[", session->GetNickname(),
"]가 퇴장했습니다. 총 세션 수: ", sessions_.size());
}
}
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());
for (auto &session : sessions_) {
// session::send에 span을 전달해야 함. 벡터 자체를 전달할 수도 있지만 우리
// Send는 span을 인자로 받음. Session::Send는 span을 받아 복사본을 생성함.
session->Send(buffer);
}
}

45
server/main.cpp Normal file
View file

@ -0,0 +1,45 @@
#include "DatabaseManager.h"
#include "Logger.h"
#include "NetworkService.h"
#include <csignal>
int main() {
try {
uint16_t port = 30000;
uint32_t threadCount = std::thread::hardware_concurrency();
if (threadCount == 0)
threadCount = 1;
boost::asio::io_context main_context;
if (!DatabaseManager::GetInstance().Init(main_context, "127.0.0.1", 33306,
"root", "root_password",
"socket_server")) {
Logger::Log("경고: 초기 DB 연결에 실패했습니다. (서버는 계속 실행됨)");
}
NetworkService server(port, threadCount);
server.Run();
boost::asio::signal_set signals(main_context, SIGINT, SIGTERM);
Logger::Log("서버가 실행 중입니다. (종료: Ctrl+C)");
signals.async_wait(
[&server, &main_context](const boost::system::error_code &error,
int signal_number) {
if (!error) {
Logger::Log("\n종료 시그널 수신 (", signal_number,
"). 서버를 중지합니다...");
server.Stop();
main_context.stop();
}
});
main_context.run();
} catch (std::exception &e) {
Logger::Log("Exception: ", e.what());
}
return 0;
}