개요
코어 계층에 속하는 서버의 진입점이 되는 main과 실제 서버 로직을 진행하는 server, 클라이언트 관리를 위한 session에 대한 로직을 작성하였다.
하기에 코어 로직의 주요 기능에 대한 소스 파일을 설명하고자 한다.
main
// main.cpp
// 프로그램 진입점 및 서버 실행 파일
#include "core/server.h"
#include <boost/asio.hpp>
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <csignal>
#include <cstdlib>
#include <string>
// 시그널 핸들러용 전역 서버 변수
std::unique_ptr<game_server::Server> server;
// 시그널 핸들러 함수
void signal_handler(int signal)
{
spdlog::info("시그널 받음 {}, 서버 종료...", signal);
if (server) {
server->stop();
}
std::exit(0);
exit(signal);
}
int main(int argc, char* argv[])
{
try {
// 로거 초기화
auto console = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(console);
spdlog::set_level(spdlog::level::info);
// 시그널 핸들러 등록
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
// 기본 설정
short port = atoi(std::getenv("SERVER_PORT"));
std::string version = std::getenv("SERVER_VERSION");
std::string db_host = std::getenv("DB_HOST");
std::string db_port = std::getenv("DB_PORT");
std::string db_user = std::getenv("DB_USER");
std::string db_password = std::getenv("DB_PASSWORD");
std::string db_name = std::getenv("DB_NAME");
std::string db_connection_string =
"dbname=" + db_name + " user=" + db_user + " password=" + db_password + " host=" + db_host + " port=" + db_port +" client_encoding=UTF8";
spdlog::info("환경 변수 불러오기 완료! 매칭 서버 버전 : {}, 포트 번호 : {}", version, port);
// IO 컨텍스트 및 서버 생성
boost::asio::io_context io_context;
server = std::make_unique<game_server::Server>(
io_context, port, db_connection_string, version);
// 서버 실행
server->run();
// IO 컨텍스트 실행 (이벤트 루프)
spdlog::info("서버 시작, 포트 : {}", port);
io_context.run();
}
catch (std::exception& e) {
spdlog::error("서버 설정 중 예외 발생: {}", e.what());
return 1;
}
return 0;
}
프로그램의 진입점을 담당할 main.cpp파일이다. 서버가 run상태일 경우 시그널을 보내 인터럽트를 가능하게 설정하였다, 또한 spdlog에 대한 로거 초기화를 통해 서버 실행 시 발생하는 이벤트에 대한 로그를 작성할 준비를 해주었다.
포트번호, 클라이언트 버전, DB통신 관련 변수들을 환경변수 파일을 통해 관리할 것이다, 따라서 cstdlib라이브러리의 getenv메서드를 통해 환경변수를 관리하고 있으며 .env파일을 별도로 작성하여 docker compose 시 해당 파일에 기재된 환경 변수를 가져와 파싱할 수 있게끔 설계하였다. 이후 DB서버에 연결하고, 서버를 직접 실행하는 로직이 이어진다.
server.h
// core/server.h
#pragma once
#include <boost/asio.hpp>
#include <memory>
#include <string>
#include <map>
#include <unordered_map>
#include <mutex>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <nlohmann/json.hpp>
#include "../controller/controller.h"
#include "../util/db_pool.h"
namespace game_server {
using json = nlohmann::json;
class Session;
class Server {
public:
Server(boost::asio::io_context& io_context,
short port,
const std::string& db_connection_string,
const std::string& version);
~Server();
void run();
void stop();
// 세션 관리 메서드
std::string registerSession(std::shared_ptr<Session> session);
void registerMirrorSession(std::shared_ptr<Session> session, int port);
void removeSession(const std::string& token, int userId);
void removeMirrorSession(int port);
std::shared_ptr<Session> getSession(const std::string& token);
std::shared_ptr<Session> getMirrorSession(int port);
int getCCU();
int getRoomCapacity();
std::string generateSessionToken();
void setSessionTimeout(std::chrono::seconds timeout);
void startSessionTimeoutCheck();
void startBroadcastTimer();
bool checkAlreadyLogin(int userId);
std::string getServerVersion();
std::vector<std::shared_ptr<Session>> getWaitingSessions();
void broadcastCCU();
void broadcastLogin(const std::string& nickName);
void broadcastChat(const std::string& nickName, const std::string& message);
void broadcastActiveUser(const std::string& message, const std::vector<std::shared_ptr<Session>>& activeSessions);
void setSessionStatus(const json& users, bool flag);
private:
void do_accept();
void init_controllers();
void check_inactive_sessions();
void scheduleBroadcast();
boost::asio::io_context& io_context_;
boost::asio::ip::tcp::acceptor acceptor_;
std::unique_ptr<DbPool> db_pool_;
std::map<std::string, std::shared_ptr<Controller>> controllers_;
bool running_;
// 세션 관리 데이터
std::unordered_map<int, std::weak_ptr<Session>> mirrors_;
std::mutex mirrors_mutex_;
std::unordered_map<std::string, std::weak_ptr<Session>> sessions_;
std::mutex sessions_mutex_;
std::unordered_map<int, std::string> tokens_;
std::mutex tokens_mutex_;
boost::uuids::random_generator uuid_generator_;
std::chrono::seconds session_timeout_{ 12 }; // 기본 12초
boost::asio::steady_timer session_check_timer_;
bool timeout_check_running_{ false };
boost::asio::steady_timer broadcast_timer_;
bool broadcast_running_ = false;
const std::chrono::seconds broadcast_interval_ = std::chrono::seconds(3);
// 버전 관리 데이터
std::string version_;
};
} // namespace game_server
서버 관련 헤더파일이다. 주로 세션과 관리된 메서드가 작성되어 있으며, 세션의 포인터를 저장할 자료구조와 해당 자료구조를 순회할 시 동시성 문제를 해결하기 위한 뮤텍스, 암호화를 위한 라이브러리 기능, 주기적인 세션 활성화 여부 체크를 위한 STL등이 존재한다.
server.cpp
// core/server.cpp
#include "server.h"
#include "session.h"
#include "../controller/auth_controller.h"
#include "../controller/room_controller.h"
#include "../controller/game_controller.h"
#include "../service/auth_service.h"
#include "../service/room_service.h"
#include "../service/game_service.h"
#include "../repository/user_repository.h"
#include "../repository/room_repository.h"
#include "../repository/game_repository.h"
#include <iostream>
#include <spdlog/spdlog.h>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <nlohmann/json.hpp>
#include <vector>
namespace game_server {
using json = nlohmann::json;
Server::Server(boost::asio::io_context& io_context,
short port,
const std::string& db_connection_string,
const std::string& version)
: io_context_(io_context),
acceptor_(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)),
running_(false),
uuid_generator_(),
session_check_timer_(io_context),
broadcast_timer_(io_context),
version_(version)
{
// DB풀 생성
db_pool_ = std::make_unique<DbPool>(db_connection_string, 20);
// 컨트롤러 초기화
init_controllers();
spdlog::info("서버 초기화 완료! 포트 번호 : {}", port);
}
Server::~Server()
{
if (running_) {
stop();
}
}
void Server::setSessionStatus(const json& users, bool flag) {
std::vector<std::string> tokens;
{
std::lock_guard<std::mutex> tokens_lock(tokens_mutex_);
for (const auto& user : users["users"]) {
int u = user.get<int>();
if (!tokens_.count(u)) continue;
tokens.push_back(tokens_[u]);
}
}
{
std::lock_guard<std::mutex> sesions_lock(sessions_mutex_);
for (const std::string& token : tokens) {
if (!sessions_.count(token)) continue;
auto session = sessions_[token].lock();
if (flag) session->setStatus("게임중");
else session->setStatus("대기중");
}
}
}
std::string Server::getServerVersion() {
return version_;
}
bool Server::checkAlreadyLogin(int userId) {
std::lock_guard<std::mutex> lock(tokens_mutex_);
return tokens_.count(userId) > 0;
}
void Server::setSessionTimeout(std::chrono::seconds timeout) {
session_timeout_ = timeout;
spdlog::info("세션 타임아웃 발생 {} 초", timeout.count());
}
void Server::startSessionTimeoutCheck() {
if (timeout_check_running_) return;
timeout_check_running_ = true;
check_inactive_sessions();
}
void Server::check_inactive_sessions() {
if (!running_ || !timeout_check_running_) return;
spdlog::debug("유효하지 않은 세션 체크 중...");
std::vector<std::string> sessionsToRemove;
{
std::lock_guard<std::mutex> lock(sessions_mutex_);
for (const auto& [token, wsession] : sessions_) {
auto session = wsession.lock();
if (!session) {
// 세션이 이미 소멸됨
spdlog::info("세션 {}가 이미 소멸되었습니다.", token);
sessionsToRemove.push_back(token);
}
else if (!session->isActive(session_timeout_)) {
// 세션이 존재하지만 타임아웃됨
spdlog::info("세션 {}가 {}초간 연결이 없어 타임아웃 되었습니다.",
token, session_timeout_.count());
sessionsToRemove.push_back(token);
}
}
}
for (const auto& token : sessionsToRemove) {
std::shared_ptr<Session> session;
{
std::lock_guard<std::mutex> lock(sessions_mutex_);
auto it = sessions_.find(token);
if (it != sessions_.end()) {
session = it->second.lock();
sessions_.erase(it); // 컬렉션에서 세션 제거
spdlog::info("세션 {}가 서버로 부터 삭제되었습니다.", token);
}
}
if (session) {
session->handle_error("세션 타임 아웃 발생");
}
}
session_check_timer_.expires_after(std::chrono::seconds(10));
session_check_timer_.async_wait([this](const boost::system::error_code& ec) {
if (!ec) {
check_inactive_sessions();
}
});
}
void Server::startBroadcastTimer() {
if (broadcast_running_) return;
broadcast_running_ = true;
scheduleBroadcast();
}
void Server::scheduleBroadcast() {
if (!running_ || !broadcast_running_) return;
broadcast_timer_.expires_after(broadcast_interval_);
broadcast_timer_.async_wait([this](const boost::system::error_code & ec) {
if (!ec) {
broadcastCCU();
scheduleBroadcast();
}
else {
spdlog::error("동시 접속자 브로드캐스트 타이머 오류: {}", ec.message());
}
});
}
std::string Server::generateSessionToken() {
boost::uuids::uuid uuid = uuid_generator_();
return boost::uuids::to_string(uuid);
}
std::string Server::registerSession(std::shared_ptr<Session> session) {
std::lock_guard<std::mutex> lock(sessions_mutex_);
// 기존 세션이 존재하면 제거
for (auto it = sessions_.begin(); it != sessions_.end(); ++it) {
if (it->second.lock() == session) {
spdlog::info("이전에 할당된 토큰 확인, 삭제 후 새로운 토큰 할당: {}", it->first);
sessions_.erase(it);
break; // 한 개만 삭제하면 되므로 루프 종료
}
}
std::string token = generateSessionToken();
sessions_[token] = session;
int userId = session->getUserId();
if (userId) {
tokens_[userId] = token;
spdlog::info("유저ID : {}에게 토큰ID : {} 할당 완료", userId, token);
}
return token;
}
void Server::registerMirrorSession(std::shared_ptr<Session> session, int port) {
std::lock_guard<std::mutex> lock(mirrors_mutex_);
// 기존 세션이 존재하면 제거
for (auto it = mirrors_.begin(); it != mirrors_.end(); ++it) {
if (it->second.lock() == session) {
spdlog::info("이미 존재하는 미러 서버 세션이 확인되어 삭제 후 재할당 하였습니다, 포트 번호 : {}", it->first);
mirrors_.erase(it);
break; // 한 개만 삭제하면 되므로 루프 종료
}
}
mirrors_[port] = session;
return;
}
void Server::removeSession(const std::string& token, int userId) {
std::lock_guard<std::mutex> session_lock(sessions_mutex_);
std::lock_guard<std::mutex> token_lock(tokens_mutex_);
bool found = false;
auto it_session = sessions_.find(token);
if (it_session != sessions_.end()) {
sessions_.erase(it_session);
found = true;
}
auto it_token = tokens_.find(userId);
if (it_token != tokens_.end()) {
tokens_.erase(it_token);
found = true;
}
if (found) {
spdlog::info("유저 ID : {}의 토큰 삭제 완료, 토큰 ID : {}", userId, token);
}
}
void Server::removeMirrorSession(int port) {
std::lock_guard<std::mutex> lock(mirrors_mutex_);
auto it = mirrors_.find(port);
if (it != mirrors_.end()) {
mirrors_.erase(it);
spdlog::info("미러 서버 세션 삭제 완료, 포트 번호 : {}", port);
}
}
std::shared_ptr<Session> Server::getSession(const std::string& token) {
std::lock_guard<std::mutex> lock(sessions_mutex_);
auto it = sessions_.find(token);
if (it != sessions_.end()) {
auto session = it->second.lock();
if (session) {
return session;
}
else {
// 세션이 이미 소멸된 경우 맵에서 제거
sessions_.erase(it);
spdlog::info("토큰 ID : {}가 이미 삭제된 상태입니다, 세션 정보를 서버에서 제거하였습니다.", token);
}
}
return nullptr;
}
std::shared_ptr<Session> Server::getMirrorSession(int port) {
std::lock_guard<std::mutex> lock(mirrors_mutex_);
auto it = mirrors_.find(port);
if (it != mirrors_.end()) {
auto session = it->second.lock();
if (session) {
return session;
}
else {
// 세션이 이미 소멸된 경우 맵에서 제거
mirrors_.erase(it);
spdlog::info("포트 번호 : {}가 이미 삭제된 상태입니다, 미리 서버 세션 정보를 서버에서 제거하였습니다.", port);
}
}
return nullptr;
}
int Server::getCCU() {
std::lock_guard<std::mutex> lock(sessions_mutex_);
return sessions_.size();
}
int Server::getRoomCapacity() {
std::lock_guard<std::mutex> lock(mirrors_mutex_);
return mirrors_.size();
}
std::vector<std::shared_ptr<Session>> Server::getWaitingSessions() {
std::vector<std::shared_ptr<Session>> waitingSessions;
std::lock_guard<std::mutex> lock(sessions_mutex_);
for (const auto& obj : sessions_) {
auto session = obj.second.lock();
if (!session || !session->getUserId()) continue;
if (session->getStatus() == "대기중") {
waitingSessions.push_back(session);
}
}
return waitingSessions;
}
void Server::broadcastCCU() {
json broadcast = {
{"action", "CCUList"},
{"users", json::array()}
};
std::vector<std::shared_ptr<Session>> waitingSessions;
{
std::lock_guard<std::mutex> lock(sessions_mutex_);
for (const auto& obj : sessions_) {
auto session = obj.second.lock();
if (!session || !session->getUserId()) continue;
json userInfo;
userInfo["nickName"] = session->getUserNickName();
userInfo["status"] = session->getStatus();
broadcast["users"].push_back(userInfo);
if (session->getStatus() == "대기중") {
waitingSessions.push_back(session);
}
}
}
broadcastActiveUser(broadcast.dump(), waitingSessions);
}
void Server::broadcastLogin(const std::string& nickName) {
json broadcast = {
{"action", "newLogin"},
{"nickName", nickName}
};
auto waitingSessions = getWaitingSessions();
broadcastActiveUser(broadcast.dump(), waitingSessions);
}
void Server::broadcastChat(const std::string& nickName, const std::string& message) {
json broadcast = {
{"action", "chat"},
{"nickName", nickName},
{"message", message}
};
auto waitingSessions = getWaitingSessions();
broadcastActiveUser(broadcast.dump(), waitingSessions);
}
void Server::broadcastActiveUser(const std::string& message, const std::vector<std::shared_ptr<Session>>& sessions) {
for (const auto& session : sessions) {
session->write_broadcast(message);
}
}
void Server::init_controllers() {
// 레포지토리 생성
auto userRepo = UserRepository::create(db_pool_.get());
auto roomRepo = RoomRepository::create(db_pool_.get());
auto gameRepo = GameRepository::create(db_pool_.get());
std::shared_ptr<UserRepository> sharedUserRepo = std::move(userRepo);
std::shared_ptr<RoomRepository> sharedRoomRepo = std::move(roomRepo);
std::shared_ptr<GameRepository> sharedGameRepo = std::move(gameRepo);
spdlog::info("레포지토리 객체 생성 및 포인터화 완료");
// 서비스 생성
auto authService = AuthService::create(sharedUserRepo);
auto roomService = RoomService::create(sharedRoomRepo);
auto gameService = GameService::create(sharedGameRepo);
spdlog::info("레포지토리와 서비스 연동 및 서비스 객체 생성 완료");
// 컨트롤러 생성 및 등록
controllers_["auth"] = std::make_shared<AuthController>(std::move(authService));
controllers_["room"] = std::make_shared<RoomController>(std::move(roomService));
controllers_["game"] = std::make_shared<GameController>(std::move(gameService));
spdlog::info("서비스와 컨트롤러 연동 및 컨트롤러 객체 생성, 핸들러 할당 완료");
}
void Server::run()
{
running_ = true;
do_accept();
startSessionTimeoutCheck();
startBroadcastTimer();
spdlog::info("서버 실행 완료, 클라이언트 연결 요청을 기다리는 중...");
}
void Server::stop() {
if (!running_) return; // 이미 중지된 경우 중복 실행 방지
running_ = false;
timeout_check_running_ = false;
broadcast_running_ = false;
// 타이머 취소 및 대기
session_check_timer_.cancel();
broadcast_timer_.cancel();
// 모든 세션에 종료 알림
{
std::lock_guard<std::mutex> lock(sessions_mutex_);
for (auto& [token, wsession] : sessions_) {
auto session = wsession.lock();
try {
if (session) {
session->handle_error("서버 중단으로 인한 연결 종료");
}
}
catch (const std::exception& e) {
spdlog::error("세션을 정리하던 중 에러가 발생하였습니다. : {}", e.what());
}
}
sessions_.clear();
}
// acceptor 닫기
try {
if (acceptor_.is_open()) {
acceptor_.close();
}
}
catch (const std::exception& e) {
spdlog::error("서버 종료 중 에러가 발생하였습니다. : {}", e.what());
}
spdlog::info("서버 중단");
}
void Server::do_accept()
{
acceptor_.async_accept(
[this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) {
if (!ec) {
// 세션 생성 및 시작
auto session = std::make_shared<Session>(std::move(socket), controllers_, this);
session->start();
}
else {
spdlog::error("클라이언트 연결을 받아 들이던 중 에러가 발생하였습니다. : {}", ec.message());
}
// 계속해서 연결 수락 (서버가 여전히 실행 중인 경우)
if (running_) {
do_accept();
}
}
);
}
} // namespace game_server
이로 인해 필요 시 서버에서 클라이언트로의 브로드캐스팅 작업을 진행할 수 있는 기능이 내포되어 있어 클라이언트와 stateful한 통신을 할 수 있다. server.cpp의 주요 기능은 다음과 같다.
- 컨트롤러와 레포지토리 서비스 객체의 생성과 서버의 자료구조에 저장
- 세션 등록, 삭제, 세션 타임아웃 체크, 세션 상태 변경
- 중복 로그인 체크, 방 가용량 조회
- 동접자 조회, 로그인, 채팅 브로드캐스팅
채팅 기능의 경우 비속어 필터링 등의 추가 기능 구현이 필요하여 실제 서비스에 포함하지는 않았다. 하지만 구글링을 해보니 이미 비속어 필터링에 대한 OpenAPI가 많기에 클라이언트에 임베디드 하여 사용하면 문제는 없을 것 같다.
session.h
// core/session.h
#pragma once
#include "../controller/controller.h"
#include <boost/asio.hpp>
#include <memory>
#include <string>
#include <array>
#include <map>
#include <nlohmann/json.hpp>
namespace game_server {
using json = nlohmann::json;
class Server;
class Session : public std::enable_shared_from_this<Session> {
public:
Session(boost::asio::ip::tcp::socket socket,
std::map<std::string, std::shared_ptr<Controller>>& controllers,
Server* server);
~Session();
void start();
const std::string& getToken() const;
void initialize();
void handlePing();
bool isActive(std::chrono::seconds timeout) const;
void handle_error(const std::string& error_message);
void setToken(const std::string& token);
int getUserId();
std::string getUserNickName();
void setStatus(const std:: string& status);
std::string getStatus();
void write_broadcast(const std::string& response);
private:
void read_message();
void process_request(json& request);
void write_response(const std::string& response);
void init_current_user(const json& response);
void read_handshake();
void write_handshake_response(const std::string& response);
void write_mirror(const std::string& response, std::shared_ptr<Session> mirror);
boost::asio::ip::tcp::socket socket_;
std::map<std::string, std::shared_ptr<Controller>>& controllers_;
std::array<char, 8192> buffer_;
std::string message_;
int user_id_;
std::string user_name_;
std::string nick_name_;
std::string status_;
Server* server_;
std::chrono::steady_clock::time_point last_activity_time_;
std::string token_;
bool is_mirror_ = false;
int mirror_port_;
};
} // namespace game_server
세션, 즉 각 클라이언트의 상태 관리를 위한 클래스의 헤더파일이다.
session.cpp
// core/session.cpp
// 세션 관리 클래스 구현
// 클라이언트와의 통신 세션을 처리하는 핵심 파일
#include "session.h"
#include "server.h"
#include <iostream>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <string>
namespace game_server {
using json = nlohmann::json;
Session::Session(boost::asio::ip::tcp::socket socket,
std::map<std::string, std::shared_ptr<Controller>>& controllers,
Server* server)
: socket_(std::move(socket)),
controllers_(controllers),
user_id_(0),
server_(server),
last_activity_time_(std::chrono::steady_clock::now())
{
spdlog::info("새 세션이 생성되었습니다. 주소: {}:{}",
socket_.remote_endpoint().address().to_string(),
socket_.remote_endpoint().port());
}
Session::~Session() {
if (server_) {
if (is_mirror_) {
server_->removeMirrorSession(mirror_port_);
}
if (!token_.empty()) {
server_->removeSession(token_, user_id_);
}
}
}
void Session::initialize() {
// 서버에 세션 등록 및 토큰 받기
token_ = server_->registerSession(shared_from_this());
spdlog::info("세션이 초기화되었습니다. 토큰: {}", token_);
}
void Session::handlePing() {
last_activity_time_ = std::chrono::steady_clock::now();
spdlog::debug("유저 ID : {}로 부터 핑을 받음", user_id_);
json response = {
{"action", "refreshSession"},
{"status", "success"},
{"message", "pong"},
{"sessionToken", token_}
};
write_response(response.dump());
spdlog::debug("핑 수신, 세션 {} 갱신됨", token_);
}
bool Session::isActive(std::chrono::seconds timeout) const {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - last_activity_time_);
return elapsed < timeout;
}
const std::string& Session::getToken() const {
return token_;
}
// 세션 시작 - 핸드셰이크부터 시작
void Session::start() {
read_handshake(); // 먼저 핸드셰이크 처리
}
// 핸드셰이크 메시지 처리
void Session::read_handshake() {
auto self(shared_from_this());
socket_.async_read_some(
boost::asio::buffer(buffer_),
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
try {
std::string data(buffer_.data(), length);
json handshake = json::parse(data);
// 미러 서버 구분 로직
if (handshake.contains("connectionType") &&
handshake["connectionType"] == "mirror" &&
handshake.contains("port")) {
// 미러 서버 세션으로 설정
is_mirror_ = true;
mirror_port_ = handshake["port"];
spdlog::info("미러 서버 연결이 수립되었습니다. 포트: {}", mirror_port_);
// 미러 서버 전용 초기화
server_->registerMirrorSession(shared_from_this(), handshake["port"]);
user_id_ = handshake["port"];
// 확인 응답 전송
json response = {
{"status", "success"},
{"message", "미러 서버가 연결되었습니다"}
};
write_handshake_response(response.dump());
}
else {
// 일반 클라이언트 세션 초기화
std::string serverVersion = server_->getServerVersion();
if (!handshake.contains("version") || serverVersion != handshake["version"].get<std::string>()) {
handle_error("최신 버전이 아닌 클라이언트 접속 반려");
return;
}
initialize();
// 핸드셰이크가 실제 요청인 경우 처리
if (handshake.contains("action")) {
process_request(handshake);
}
else {
// 일반 클라이언트에게 연결 확인 메시지 전송
json response = {
{"status", "success"},
{"message", "서버에 연결되었습니다"}
};
write_handshake_response(response.dump());
}
}
}
catch (const std::exception& e) {
// 핸드셰이크 실패 처리
spdlog::error("핸드셰이크 오류: {}", e.what());
handle_error("잘못된 핸드셰이크 형식");
}
}
else {
handle_error("핸드셰이크 읽기 오류: " + ec.message());
}
});
}
// 핸드셰이크 응답 전송 (응답 후 일반 메시지 처리로 전환)
void Session::write_handshake_response(const std::string& response) {
auto self(shared_from_this());
boost::asio::async_write(
socket_,
boost::asio::buffer(response),
[this, self](boost::system::error_code ec, std::size_t /*length*/) {
if (!ec) {
// 핸드셰이크 완료 후 일반 메시지 처리 시작
read_message();
}
else {
handle_error("핸드셰이크 응답 쓰기 오류: " + ec.message());
}
});
}
void Session::read_message() {
auto self(shared_from_this());
// 비동기적으로 데이터 읽기
socket_.async_read_some(
boost::asio::buffer(buffer_),
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
try {
// 수신된 데이터를 문자열로 변환
std::string data(buffer_.data(), length);
// JSON 파싱
json request = json::parse(data);
// 요청 처리
process_request(request);
}
catch (const std::exception& e) {
// JSON 파싱 오류 등 예외 처리
spdlog::error("요청 데이터 처리 중 오류: {}", e.what());
json error_response = {
{"status", "error"},
{"message", "잘못된 요청 형식"}
};
write_response(error_response.dump());
}
}
else {
handle_error("메시지 읽기 오류: " + ec.message());
}
});
}
void Session::process_request(json& request) {
try {
spdlog::debug("요청 처리 중...");
// action 필드로 요청 유형 확인
std::string action = request["action"];
spdlog::debug("액션: {}", action);
std::string controller_type;
// 컨트롤러 유형 결정
if (action == "register" || action == "login" || action == "SSAFYlogin" || action == "updateNickName") {
if (user_id_) request["userId"] = user_id_;
controller_type = "auth";
}
else if (action == "createRoom" || action == "joinRoom" || action == "exitRoom" || action == "listRooms") {
if (user_id_ == 0) {
json error_response = {
{"status", "error"},
{"message", "인증이 필요합니다"}
};
write_response(error_response.dump());
return;
}
request["userId"] = user_id_;
controller_type = "room";
}
else if (action == "gameStart" || action == "gameEnd") {
if (user_id_ == 0 || !is_mirror_) {
json error_response = {
{"status", "error"},
{"message", "권한이 없습니다."}
};
write_response(error_response.dump());
return;
}
request["userId"] = user_id_;
controller_type = "game";
}
else if (action == "alivePing") {
handlePing();
return;
}
else if (action == "logout") {
std::string logMessage = user_name_ + " 님이 로그아웃하였습니다";
handle_error(logMessage);
return;
}
else if (action == "roomCapacity") {
json response;
response["action"] = "roomCapacity";
response["status"] = "success";
response["roomCapacity"] = server_->getRoomCapacity();
write_response(response.dump());
return;
}
else if (action == "CCU") {
json response;
response["action"] = "CCU";
response["status"] = "success";
response["roomCapacity"] = server_->getCCU();
write_response(response.dump());
return;
}
else {
// 알수 없는 액션 처리
spdlog::warn("알 수 없는 액션: {}", action);
json error_response = {
{"status", "error"},
{"message", "알 수 없는 액션"}
};
write_response(error_response.dump());
return;
}
auto controller_it = controllers_.find(controller_type);
if (controller_it != controllers_.end()) {
spdlog::debug("컨트롤러 찾음: {}", controller_type);
json response = controller_it->second->handleRequest(request);
spdlog::debug("컨트롤러 응답 수신됨");
if ((action == "login" || action == "SSAFYlogin") && response["status"] == "success") {
spdlog::debug("로그인 응답 처리 중");
if (server_->checkAlreadyLogin(response["userId"].get<int>())) {
spdlog::error("사용자 ID: {}는 이미 로그인되어 있습니다", response["userId"].get<int>());
json error_response = {
{"status", "error"},
{"message", "이미 로그인된 사용자입니다"}
};
write_response(error_response.dump());
return;
}
init_current_user(response);
std::string token = server_->registerSession(shared_from_this());
token_ = token;
response["sessionToken"] = token;
}
else if (action == "createRoom" && response["status"] == "success") {
spdlog::debug("방 생성 응답 처리 중");
// response 객체 디버깅 로그
spdlog::debug("응답 내용: {}", response.dump());
try {
json broad_response;
broad_response["action"] = "setRoom";
broad_response["roomId"] = response["roomId"];
broad_response["roomName"] = response["roomName"];
broad_response["maxPlayers"] = response["maxPlayers"];
auto mirror = server_->getMirrorSession(response["port"]);
if (!mirror) {
json error_response = {
{"status", "error"},
{"message", "미러 서버가 없습니다"}
};
spdlog::error("방 ID {}에 미러 서버가 없습니다", response["roomId"].get<int>());
write_response(error_response.dump());
return;
}
spdlog::debug("미러 서버 찾음, 메시지 브로드캐스팅");
status_ = std::to_string(broad_response["roomId"].get<int>()) + "번 방";
write_mirror(broad_response.dump(), mirror);
}
catch (const std::exception& e) {
spdlog::error("방 생성 응답 처리 중 오류: {}", e.what());
// 예외가 발생해도 원래 응답은 전송
}
}
else if (action == "joinRoom" && response["status"] == "success") {
status_ = std::to_string(response["roomId"].get<int>()) + "번 방";
}
else if (action == "exitRoom" && response["status"] == "success") {
status_ = "대기중";
}
else if (action == "gameStart" && response["status"] == "success") {
server_->setSessionStatus(response, true);
}
else if (action == "gameEnd" && response["status"] == "success") {
server_->setSessionStatus(response, false);
}
spdlog::debug("클라이언트에 응답 전송 중");
write_response(response.dump());
}
else {
spdlog::error("컨트롤러를 찾지 못함: {}", controller_type);
json error_response = {
{"status", "error"},
{"message", "내부 서버 오류"}
};
write_response(error_response.dump());
}
}
catch (const std::exception& e) {
spdlog::error("process_request 중 오류: {}", e.what());
if (request.is_object() && request.contains("action")) {
spdlog::error("실패한 액션: {}", request["action"].get<std::string>());
}
json error_response = {
{"status", "error"},
{"message", "잘못된 요청 형식"}
};
write_response(error_response.dump());
}
}
void Session::write_mirror(const std::string& response, std::shared_ptr<Session> mirror) {
boost::asio::async_write(
mirror->socket_,
boost::asio::buffer(response),
[mirror](boost::system::error_code ec, std::size_t /*length*/) {
if (!ec) {
// 다음 요청 대기
mirror->read_message();
}
else {
mirror->handle_error("응답 쓰기 오류: " + ec.message());
}
});
}
void Session::write_broadcast(const std::string& response) {
auto self = shared_from_this();
boost::asio::async_write(
socket_,
boost::asio::buffer(response),
[self](boost::system::error_code ec, std::size_t /*length*/) {
if (ec) {
self->handle_error("동접자 수 받기 에러: " + ec.message());
}
});
}
void Session::write_response(const std::string& response) {
auto self(shared_from_this());
// 클라이언트로 응답 데이터 전송
boost::asio::async_write(
socket_,
boost::asio::buffer(response),
[this, self](boost::system::error_code ec, std::size_t /*length*/) {
if (!ec) {
// 다음 요청 대기
read_message();
}
else {
handle_error("응답 쓰기 오류: " + ec.message());
}
});
}
void Session::handle_error(const std::string& error_message) {
// 오류 로깅
spdlog::info(error_message);
// 사용자가 방에 참여 중이라면 퇴장 처리
try {
auto controller_it = controllers_.find("room");
if (controller_it != controllers_.end() && user_id_ > 0) {
spdlog::debug("사용자 {}의 자동 방 퇴장 시도 중", user_id_);
json temp = {
{"action", "exitRoom"},
{"userId", user_id_}
};
json response = controller_it->second->handleRequest(temp);
if (response.contains("status") && response["status"] == "success") {
spdlog::info("사용자 {}가 세션 종료 시 자동으로 방에서 퇴장하였습니다", user_id_);
}
}
}
catch (const std::exception& e) {
spdlog::error("방 퇴장 중 에러가 발생하였습니다. : {}", e.what());
}
if (socket_.is_open()) {
boost::system::error_code ec;
socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
if (ec) {
spdlog::error("소켓 종료 중 에러가 발생하였습니다. : {}", ec.message());
}
socket_.close(ec);
if (ec) {
spdlog::error("소켓 종료 중 에러가 발생하였습니다. : {}", ec.message());
}
else {
spdlog::info("소켓을 정상적으로 종료하였습니다.");
}
}
}
int Session::getUserId() {
return user_id_;
}
std::string Session::getUserNickName() {
return nick_name_;
}
void Session::setStatus(const std::string& status) {
status_ = status;
}
std::string Session::getStatus() {
return status_;
}
void Session::setToken(const std::string& token) {
token_ = token;
}
void Session::init_current_user(const json& response) {
if (response.contains("userId")) user_id_ = response["userId"];
if (response.contains("userName")) user_name_ = response["userName"];
if (response.contains("nickName")) nick_name_ = response["nickName"];
status_ = "대기중";
spdlog::info("{}유저가 로그인 하였습니다. (ID: {}) 닉네임 : {}", user_name_, user_id_, nick_name_);
}
} // namespace game_server
각 클래이언트 세션의 처음부터 끝의 생명 주기에서 발생하는 요청에 대한 처리를 담당하는 소스코드이다.
주요 기능은 다음과 같다.
- 세션과 유저 정보 동기화
- 토큰 정보 가져오기, 변경하기
- 닉네임 변경하기
- 세션 연결 여부 갱신
- 닉네임 변경하기
- 미러 서버에 푸시 보내기
회고
첫 소켓 서버를 작성하다 보니 불필요한 코드가 많아져 길어지고 가독성이 떨어지는 느낌을 많이 받았다.
기존의 웹 프레임워크를 다루어 MVC패턴을 적용할 때 보다 제공해주는 기본 기능이 없기 때문에 직접 구현해야 하기에 이는 어쩔 수 없는 느낌이긴 하지만 코드를 더 보기 쉽게 리팩토링 하는 작업을 진행해야 할 것 같다.
추후엔 클라이언트 요청에 따라 세션에서 컨트롤러 핸들러로 요청을 보내고, 각 컨트롤러에서 해당하는 서비스 로직을 수행하기 위해 Service클래스로 요청을 보내는 내용을 담아보도록 하겠다.
'프로젝트 > 메타버스 게임' 카테고리의 다른 글
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 서비스 계층 (0) | 2025.04.09 |
---|---|
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 컨트롤러 계층 (0) | 2025.04.08 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 계층형 아키텍쳐 구현 (0) | 2025.04.01 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 Linux 빌드 환경 세팅(Makefile) (0) | 2025.03.31 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 Windows 빌드 환경 세팅(VisualStudio) (0) | 2025.03.31 |