init
This commit is contained in:
commit
b887f15662
21 changed files with 1038 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Build artifacts
|
||||
build/
|
||||
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
compile_commands.json
|
||||
|
||||
.cache/
|
||||
32
CMakeLists.txt
Normal file
32
CMakeLists.txt
Normal 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
51
README.md
Normal 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
12
compose.yml
Normal 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
42
include/DatabaseManager.h
Normal 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
15
include/Logger.h
Normal 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
25
include/NetworkService.h
Normal 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
83
include/Packet.h
Normal 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
15
include/PacketHandler.h
Normal 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
43
include/Session.h
Normal 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
20
include/SessionManager.h
Normal 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
63
include/SwordLogic.h
Normal 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
10
schema.sql
Normal 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
18
setup.sh
Executable 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
152
src/DatabaseManager.cpp
Normal 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
32
src/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();
|
||||
});
|
||||
}
|
||||
132
src/PacketHandler.cpp
Normal file
132
src/PacketHandler.cpp
Normal 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
101
src/Session.cpp
Normal 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
38
src/SessionManager.cpp
Normal 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
45
src/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;
|
||||
}
|
||||
96
tests/Client.cpp
Normal file
96
tests/Client.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue