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