feat: server, client 디렉토리 조정 및 client 헤더 파일 추가. 시나리오
혹은 대화형 클라이언트로 수정
This commit is contained in:
parent
b887f15662
commit
1d103d7f16
12 changed files with 281 additions and 112 deletions
146
server/DatabaseManager.cpp
Normal file
146
server/DatabaseManager.cpp
Normal 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
32
server/NetworkService.cpp
Normal 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
131
server/PacketHandler.cpp
Normal 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
100
server/Session.cpp
Normal 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
37
server/SessionManager.cpp
Normal 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
45
server/main.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue