feat: server, client 디렉토리 조정 및 client 헤더 파일 추가. 시나리오
혹은 대화형 클라이언트로 수정
This commit is contained in:
parent
b887f15662
commit
1d103d7f16
12 changed files with 281 additions and 112 deletions
|
|
@ -14,19 +14,20 @@ find_package(OpenSSL REQUIRED)
|
|||
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
|
||||
server/main.cpp
|
||||
server/NetworkService.cpp
|
||||
server/Session.cpp
|
||||
server/PacketHandler.cpp
|
||||
server/SessionManager.cpp
|
||||
server/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
|
||||
client/Client.cpp
|
||||
client/main.cpp
|
||||
)
|
||||
target_include_directories(Client PRIVATE include)
|
||||
target_link_libraries(Client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto)
|
||||
|
|
|
|||
170
client/Client.cpp
Normal file
170
client/Client.cpp
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#include "Client.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
BaseClient::BaseClient(boost::asio::io_context &io_context,
|
||||
const std::string &host, const std::string &port)
|
||||
: socket_(io_context) {
|
||||
tcp::resolver resolver(io_context);
|
||||
boost::asio::connect(socket_, resolver.resolve(host, port));
|
||||
}
|
||||
|
||||
void BaseClient::SendPacket(PacketID id, const void *payload,
|
||||
size_t payloadSize) {
|
||||
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));
|
||||
}
|
||||
|
||||
bool BaseClient::ReceiveHeader(PacketHeader &header) {
|
||||
try {
|
||||
boost::asio::read(socket_, boost::asio::buffer(&header, sizeof(header)));
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void BaseClient::Login(const std::string &nickname) {
|
||||
PKT_CS_Login loginPkt;
|
||||
std::memset(loginPkt.nickname, 0, sizeof(loginPkt.nickname));
|
||||
std::strncpy(loginPkt.nickname, nickname.c_str(),
|
||||
sizeof(loginPkt.nickname) - 1);
|
||||
SendPacket(PacketID::Login, &loginPkt, sizeof(loginPkt));
|
||||
std::cout << "로그인 요청 보냄: " << nickname << std::endl;
|
||||
}
|
||||
|
||||
void BaseClient::HandleUpgradeResult() {
|
||||
PKT_SC_UpgradeResult res;
|
||||
if (ReceivePayload(res)) {
|
||||
currentLevel_ = res.currentLevel;
|
||||
currentGold_ = res.currentGold;
|
||||
std::string resultStr =
|
||||
(res.result == 1) ? "성공" : (res.result == 0 ? "파괴!!!" : "실패");
|
||||
std::cout << ">> 강화 결과: " << resultStr
|
||||
<< " (현재레벨: " << currentLevel_
|
||||
<< ", 남은골드: " << currentGold_ << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void BaseClient::HandleSellResult() {
|
||||
PKT_SC_SellResult res;
|
||||
if (ReceivePayload(res)) {
|
||||
currentLevel_ = 0;
|
||||
currentGold_ = res.totalGold;
|
||||
std::cout << ">> 판매 결과: " << res.earnedGold
|
||||
<< " 골드 획득! (총 골드: " << currentGold_ << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void InteractiveClient::Run() {
|
||||
std::cout << "닉네임을 입력하세요: ";
|
||||
std::cin >> nickname_;
|
||||
Login(nickname_);
|
||||
|
||||
while (true) {
|
||||
if (currentLevel_ == 0) {
|
||||
std::cout << "\n[현재 검 없음] 1. 검 구매 시도, 2. 종료\n입력: ";
|
||||
} else {
|
||||
std::cout << "\n[현재 검 레벨: " << currentLevel_
|
||||
<< ", 골드: " << currentGold_
|
||||
<< "] 1. 강화 시도, 2. 판매, 3. 종료\n입력: ";
|
||||
}
|
||||
|
||||
int choice;
|
||||
if (!(std::cin >> choice))
|
||||
break;
|
||||
|
||||
if (currentLevel_ == 0) {
|
||||
if (choice == 1) {
|
||||
SendPacket(PacketID::CS_UpgradeSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_UpgradeResult)) {
|
||||
HandleUpgradeResult();
|
||||
}
|
||||
} else if (choice == 2) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (choice == 1) {
|
||||
SendPacket(PacketID::CS_UpgradeSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_UpgradeResult)) {
|
||||
HandleUpgradeResult();
|
||||
}
|
||||
} else if (choice == 2) {
|
||||
SendPacket(PacketID::CS_SellSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_SellResult)) {
|
||||
HandleSellResult();
|
||||
}
|
||||
} else if (choice == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#include <fstream>
|
||||
|
||||
void ScenarioClient::Run() {
|
||||
std::ifstream is(scenarioFile_);
|
||||
if (!is.is_open()) {
|
||||
std::cerr << "시나리오 파일을 열 수 없습니다: " << scenarioFile_
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(is >> nickname_))
|
||||
return;
|
||||
Login(nickname_);
|
||||
|
||||
std::string line;
|
||||
while (is >> line) {
|
||||
int choice = std::stoi(line);
|
||||
std::cout << "\n시나리오 입력 실행: " << choice << std::endl;
|
||||
|
||||
if (currentLevel_ == 0) {
|
||||
if (choice == 1) {
|
||||
SendPacket(PacketID::CS_UpgradeSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_UpgradeResult)) {
|
||||
HandleUpgradeResult();
|
||||
}
|
||||
} else if (choice == 2) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (choice == 1) {
|
||||
SendPacket(PacketID::CS_UpgradeSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_UpgradeResult)) {
|
||||
HandleUpgradeResult();
|
||||
}
|
||||
} else if (choice == 2) {
|
||||
SendPacket(PacketID::CS_SellSword);
|
||||
PacketHeader header;
|
||||
if (ReceiveHeader(header) &&
|
||||
header.id == static_cast<uint16_t>(PacketID::SC_SellResult)) {
|
||||
HandleSellResult();
|
||||
}
|
||||
} else if (choice == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
39
client/main.cpp
Normal file
39
client/main.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "Client.h"
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
bool isScenario = false;
|
||||
std::string nickname;
|
||||
std::string scenarioFile;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--scenario" && i + 1 < argc) {
|
||||
isScenario = true;
|
||||
scenarioFile = argv[++i];
|
||||
} else {
|
||||
nickname = arg;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
boost::asio::io_context io_context;
|
||||
if (isScenario) {
|
||||
if (scenarioFile.empty()) {
|
||||
std::cerr
|
||||
<< "시나리오 파일명이 필요합니다. 사용법: --scenario <파일경로>"
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
ScenarioClient client(io_context, "127.0.0.1", "30000", scenarioFile);
|
||||
client.Run();
|
||||
} else {
|
||||
InteractiveClient client(io_context, "127.0.0.1", "30000");
|
||||
client.Run();
|
||||
}
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Exception: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
59
include/Client.h
Normal file
59
include/Client.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include "Packet.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <string>
|
||||
|
||||
using boost::asio::ip::tcp;
|
||||
|
||||
class BaseClient {
|
||||
public:
|
||||
BaseClient(boost::asio::io_context &io_context, const std::string &host,
|
||||
const std::string &port);
|
||||
|
||||
virtual ~BaseClient() = default;
|
||||
|
||||
void SendPacket(PacketID id, const void *payload = nullptr,
|
||||
size_t payloadSize = 0);
|
||||
|
||||
bool ReceiveHeader(PacketHeader &header);
|
||||
|
||||
template <typename T> bool ReceivePayload(T &payload) {
|
||||
try {
|
||||
boost::asio::read(socket_, boost::asio::buffer(&payload, sizeof(T)));
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Login(const std::string &nickname);
|
||||
|
||||
virtual void Run() = 0;
|
||||
|
||||
protected:
|
||||
tcp::socket socket_;
|
||||
uint32_t currentLevel_ = 0;
|
||||
uint64_t currentGold_ = 0;
|
||||
std::string nickname_;
|
||||
|
||||
void HandleUpgradeResult();
|
||||
void HandleSellResult();
|
||||
};
|
||||
|
||||
class InteractiveClient : public BaseClient {
|
||||
public:
|
||||
using BaseClient::BaseClient;
|
||||
void Run() override;
|
||||
};
|
||||
|
||||
class ScenarioClient : public BaseClient {
|
||||
public:
|
||||
ScenarioClient(boost::asio::io_context &io_context, const std::string &host,
|
||||
const std::string &port, const std::string &scenarioFile)
|
||||
: BaseClient(io_context, host, port), scenarioFile_(scenarioFile) {}
|
||||
void Run() override;
|
||||
|
||||
private:
|
||||
std::string scenarioFile_;
|
||||
};
|
||||
|
|
@ -41,13 +41,11 @@ bool DatabaseManager::Init(boost::asio::io_context &io_context,
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +70,6 @@ void DatabaseManager::Unlock() {
|
|||
} else {
|
||||
auto next = waiting_queue_.front();
|
||||
waiting_queue_.pop();
|
||||
// 다음 대기자를 깨움
|
||||
next->cancel();
|
||||
}
|
||||
});
|
||||
|
|
@ -80,7 +77,6 @@ void DatabaseManager::Unlock() {
|
|||
|
||||
boost::asio::awaitable<DatabaseManager::UserData>
|
||||
DatabaseManager::LoadUser(std::string nickname) {
|
||||
// 줄 서기
|
||||
co_await Lock();
|
||||
|
||||
try {
|
||||
|
|
@ -126,7 +122,6 @@ DatabaseManager::LoadUser(std::string nickname) {
|
|||
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
|
||||
uint64_t gold,
|
||||
uint32_t swordLevel) {
|
||||
// 줄 서기
|
||||
co_await Lock();
|
||||
|
||||
try {
|
||||
|
|
@ -146,7 +141,6 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
|
|||
Logger::Log("SaveUser 에러: ", e.what());
|
||||
}
|
||||
|
||||
// 다음 사람 들어오세요
|
||||
Unlock();
|
||||
co_return;
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
|||
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);
|
||||
|
|
@ -63,7 +63,6 @@ void Session::DoReadBody() {
|
|||
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_;
|
||||
|
|
@ -25,7 +25,6 @@ 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());
|
||||
5
test_scenario.txt
Normal file
5
test_scenario.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
TestUser
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
#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