This commit is contained in:
bumpsoo 2026-02-03 09:44:08 +00:00
commit b887f15662
21 changed files with 1038 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Build artifacts
build/
.vscode/
.idea/
*.swp
*.swo
.DS_Store
compile_commands.json
.cache/

32
CMakeLists.txt Normal file
View file

@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.10)
project(SocketServer VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_compile_options(-Wall -Wextra -Wpedantic)
# Boost 및 의존성 찾기
find_package(Boost REQUIRED COMPONENTS system)
find_package(OpenSSL REQUIRED)
# 컴파일 명령 추출 활성화 (LSP/IDE 용)
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
)
target_include_directories(Server PRIVATE include)
target_link_libraries(Server PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto)
add_executable(Client
tests/Client.cpp
)
target_include_directories(Client PRIVATE include)
target_link_libraries(Client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto)

51
README.md Normal file
View file

@ -0,0 +1,51 @@
# C++20 게임 서버 (SocketServer)
## 개요
이 프로젝트는 Modern C++ (C++20)와 Boost.Asio를 사용한 게임 서버 코어를 보여줍니다.
비동기 I/O, 멀티스레딩, 패킷 직렬화 및 글로벌 세션 관리 시스템을 특징으로 합니다.
## 주요 기능
- C++20: std::span, std::jthread, std::format (준비됨) 및 Concepts 활용.
- Boost.Asio: 확장 가능한 네트워킹을 위한 Proactor 패턴 적용.
- 패킷 처리: 효율적인 헤더/바디 분리 및 디스패칭.
- 스레드 안전성: 뮤텍스(mutex)를 사용한 공유 자원 보호.
- 글로벌 세션 관리: 모든 접속자를 한곳에서 관리하고 브로드캐스팅하는 시스템.
- 검 키우기 컨텐츠: MySQL 연동을 통한 유저 데이터 영속성 및 강화 로직
## 사전 요구 사항 (테스트 환경, debian 13)
- g++ 14.2.0 이상
- CMake 3.31.6 이상
- Boost Libraries 1.83.0 이상
- OpenSSL 3.5.4 이상
- MariaDB Client Library 11.8.3 이상
## 환경 설정
```bash
./setup.sh
```
## 빌드
```bash
cmake --build build
```
## 실행 방법
1. 서버 시작:
```bash
./Server
```
2. 클라이언트 실행:
```bash
./Client
```
## 아키텍처
- NetworkService: io_context 및 스레드 풀 관리.
- Session: 개별 클라이언트 연결 및 비동기 읽기/쓰기 루프 처리.
- PacketHandler: 패킷 ID에 따른 로직 디스패칭.
- SessionManager: 연결된 모든 클라이언트를 글로벌하게 관리하고 메시지를 브로드캐스팅하는 싱글톤 관리자.
- DatabaseManager: Boost.MySQL을 사용한 비동기 데이터베이스 연동 및 데이터 영속화 관리.
- Logger: 멀티스레드 환경에서 안전한 로그 출력을 위한 스레드 세이프 로거.
- Packet: #pragma pack(1)을 사용한 효율적인 바이너리 프로토콜 정의.

12
compose.yml Normal file
View file

@ -0,0 +1,12 @@
services:
db:
image: mysql:8.0
container_name: socket_server_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: socket_server
ports:
- "33306:3306"
volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql

42
include/DatabaseManager.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/mysql.hpp>
#include <memory>
#include <queue>
#include <string>
class DatabaseManager {
public:
static DatabaseManager &GetInstance();
bool 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);
struct UserData {
std::string nickname;
uint64_t gold;
uint32_t swordLevel;
};
boost::asio::awaitable<UserData> LoadUser(std::string nickname);
boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold,
uint32_t swordLevel);
private:
DatabaseManager() = default;
// 비동기 Mutex 역할을 할 함수들
boost::asio::awaitable<void> Lock();
void Unlock();
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn_;
std::unique_ptr<boost::asio::strand<boost::asio::io_context::executor_type>>
strand_;
// 비동기 락 시스템을 위한 상태 변수
bool is_locked_ = false;
std::queue<std::shared_ptr<boost::asio::steady_timer>> waiting_queue_;
};

15
include/Logger.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include <iostream>
#include <mutex>
class Logger {
public:
// 가변 인자 템플릿과 Fold Expression을 사용하여 여러 인자를 안전하게 출력
template <typename... Args> static void Log(Args &&...args) {
std::lock_guard<std::mutex> lock(mutex_);
(std::cout << ... << std::forward<Args>(args)) << std::endl;
}
private:
static inline std::mutex mutex_;
};

25
include/NetworkService.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <boost/asio.hpp>
#include <thread>
#include <vector>
using boost::asio::ip::tcp;
class NetworkService {
public:
NetworkService(uint16_t port, int threadCount);
~NetworkService();
void Run();
void Stop();
private:
void DoAccept();
boost::asio::io_context io_context_;
std::vector<std::jthread> threads_;
tcp::acceptor acceptor_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type>
work_guard_;
};

83
include/Packet.h Normal file
View file

@ -0,0 +1,83 @@
#pragma once
#include <cstdint>
#include <vector>
// 표준 헤더 크기: 2바이트 크기 + 2바이트 ID
#pragma pack(push, 1)
struct PacketHeader {
uint16_t size;
uint16_t id;
};
#pragma pack(pop)
// 처리를 쉽게 하기 위한 단순 패킷 래퍼
struct Packet {
PacketHeader header;
// Payload 데이터 (벡터 또는 문자열)
std::vector<uint8_t> payload;
};
enum class PacketID : uint16_t {
Ping = 1,
// 로그인 요청 (PKT_CS_Login)
Login = 10,
Chat = 20,
// 검 키우기 관련 패킷
// 강화 요청
CS_UpgradeSword = 30,
// 강화 결과 응답
SC_UpgradeResult = 31,
// 판매 요청
CS_SellSword = 35,
// 판매 결과 응답
SC_SellResult = 36,
// 랭킹 조회 요청
CS_RankingRequest = 40,
// 랭킹 리스트 응답
SC_RankingList = 41,
};
// 게임 규칙 관련 상수
namespace GameConfig {
const uint32_t MAX_SWORD_LEVEL = 20;
const uint64_t INITIAL_GOLD = 10000;
} // namespace GameConfig
// 고정 크기 데이터 전송을 위한 구조체 정의
#pragma pack(push, 1)
struct PKT_CS_Login {
// 유저 닉네임 (최대 32자)
char nickname[32];
};
struct PKT_SC_UpgradeResult {
// 0: 파괴, 1: 성공, 2: 실패
uint8_t result;
// 현재 강화 레벨
uint32_t currentLevel;
// 현재 보유 골드
uint64_t currentGold;
};
struct PKT_SC_SellResult {
// 판매 후 획득 골드
uint64_t earnedGold;
// 현재 총 골드
uint64_t totalGold;
};
struct RankingEntry {
// 유저 이름 (최대 32자)
char username[32];
// 검 레벨
uint32_t swordLevel;
};
struct PKT_SC_RankingList {
// 랭킹 리스트 개수
uint32_t count;
// 이후 RankingEntry[count] 만큼 데이터가 따라옴
};
#pragma pack(pop)

15
include/PacketHandler.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include "Packet.h"
#include "Session.h"
#include <boost/asio/awaitable.hpp>
#include <memory>
class Session;
class PacketHandler {
public:
// 패킷 종류에 따라 비동기 로직 디스패칭 (코루틴 방식)
static boost::asio::awaitable<void>
HandlePacket(std::shared_ptr<Session> session, const Packet packet);
};

43
include/Session.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "Packet.h"
#include <boost/asio.hpp>
#include <deque>
#include <memory>
#include <span>
using boost::asio::ip::tcp;
class Session : public std::enable_shared_from_this<Session> {
public:
Session(tcp::socket socket);
~Session();
void Start();
void Send(std::span<const uint8_t> data);
void SetNickname(const std::string &nickname);
const std::string &GetNickname() const;
void SetGold(uint64_t gold);
uint64_t GetGold() const;
void SetSwordLevel(uint32_t level);
uint32_t GetSwordLevel() const;
private:
void DoReadHeader();
void DoReadBody();
void DoWrite();
tcp::socket socket_;
PacketHeader packetHeader_;
std::vector<uint8_t> packetBody_;
// 스레드 안전성과 순서 보장을 위한 출력 패킷 큐
std::deque<std::vector<uint8_t>> writeQueue_;
std::string nickname_;
uint64_t gold_ = 0;
uint32_t swordLevel_ = 0;
};

20
include/SessionManager.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "Session.h"
#include <memory>
#include <mutex>
#include <set>
#include <span>
class SessionManager {
public:
static SessionManager &GetInstance();
void Join(std::shared_ptr<Session> session);
void Leave(std::shared_ptr<Session> session);
void Broadcast(PacketHeader header, std::span<const uint8_t> body);
private:
std::mutex mutex_;
std::set<std::shared_ptr<Session>> sessions_;
};

63
include/SwordLogic.h Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include "Packet.h"
#include <cstdint>
#include <random>
class SwordLogic {
public:
// 강화 비용 계산
static uint64_t GetUpgradeCost(uint32_t currentLevel) {
// 0강은 무료
if (currentLevel == 0)
return 0;
// 레벨이 오를수록 비용 증가
return (currentLevel * 1000) + 500;
}
// 판매 가격 계산
static uint64_t GetSellPrice(uint32_t currentLevel) {
if (currentLevel == 0)
return 100;
// 성공 횟수에 비례하여 상승
return (currentLevel * 2000) + 1000;
}
// 강화 시도 결과 계산 (0: 파괴, 1: 성공, 2: 실패)
static uint8_t TryUpgrade(uint32_t currentLevel) {
// 최대 레벨 도달 시 실패 처리
if (currentLevel >= GameConfig::MAX_SWORD_LEVEL)
return 2;
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<int> dis(1, 100);
int randVal = dis(gen);
// 확률 설정
int successProb = 0;
int destroyProb = 0;
if (currentLevel < 10) {
// 0~9강: 성공 확률 높음, 파괴 없음
// 90% ~ 45%
successProb = 90 - (currentLevel * 5);
destroyProb = 0;
} else {
// 10~19강: 성공 확률 낮음, 파괴 존재
// 40% ~ 4%
successProb = 40 - ((currentLevel - 10) * 4);
// 5% ~ 23%
destroyProb = 5 + ((currentLevel - 10) * 2);
}
// 성공
if (randVal <= successProb)
return 1;
// 파괴
if (randVal <= (successProb + destroyProb))
return 0;
// 실패
return 2;
}
};

10
schema.sql Normal file
View file

@ -0,0 +1,10 @@
CREATE DATABASE IF NOT EXISTS socket_server;
USE socket_server;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) UNIQUE NOT NULL,
gold BIGINT UNSIGNED DEFAULT 10000,
sword_level INT DEFAULT 0,
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

18
setup.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "Starting project setup..."
if [ ! -d "build" ]; then
mkdir build
echo "Created build directory."
fi
cd build
cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
echo "CMake configuration complete."
cd ..
ln -sf build/compile_commands.json compile_commands.json

152
src/DatabaseManager.cpp Normal file
View file

@ -0,0 +1,152 @@
#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;
// 이 체크 로직 자체가 strand 위에서 안전하게 돌아야 함
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
src/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();
});
}

132
src/PacketHandler.cpp Normal file
View file

@ -0,0 +1,132 @@
#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);
// DB에서 유저 정보 로드 (co_await 활용!)
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;
}

101
src/Session.cpp Normal file
View file

@ -0,0 +1,101 @@
#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);
}
});
}

38
src/SessionManager.cpp Normal file
View file

@ -0,0 +1,38 @@
#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
src/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;
}

96
tests/Client.cpp Normal file
View file

@ -0,0 +1,96 @@
#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;
}