I'm FanJae.

[Two Faced Poker] Day 5. Server 리팩토링 I. 대공사 본문

Toy Project/Two Faced Poker

[Two Faced Poker] Day 5. Server 리팩토링 I. 대공사

FanJae 2024. 10. 17. 23:04

1. Server 리팩토링을 결심한 이유

① Client_Event_Handler와 Game_Manager간의 독립적인 클래스라고 하기에 다소 모호한 설계 구조 개선

- 기존 내가 만들었던 Client_Event_Handler는 거의 대부분의 Event를 처리하고 있다.

- Message 수신 자체는 Client_Event_Handler가 담당하더라도 다른 Manager들이 이를 처리하도록 개선하였다.

② 서버가 상태 값을 가지고 있지 않음으로써 발생하는 문제 해소.

- 준비 / 완료등을 Client 가 보는 기준에서 처리하는 것은 옳지 않다고 생각해서.. 바꿔보고자 한다.

- Client에서 보낸 메시지를 검증을 하더라도 이 부분은 서버가 반드시 가지고 있어야 한다.

Packet의 Header 정보 부재

- TCP/IP에서는 원래 n바이트를 보냈다고 이를 받는 쪽에서 n바이트를 받는다는 보장이 없다.

- 이 부분에 대해서 너무 생각없이 처리했다. 이 부분을 제대로 처리해보도록 하겠다.


2. Server 구조 개선

- 기존 Client_Handler가 하는 역할이 단순한 메시지 핸들뿐 아니라 실질적인 로직도 모두 처리하고 있었다.

- 따라서 이 부분을 개선하고, 각 Manager를 생성하고, 각각의 이벤트를 처리한다.

 

2-1. Map Key값 개선.

static std::map<int, std::shared_ptr<Game_Manager>> gameManagers;
static std::map<int, std::shared_ptr<Room_Manager>> roomManagers;

- 초기 설계 의도는 제목으로 중복된 방을 만들지 않게할 의도였다.

- 하지만 Key값이 String인건 그렇게 좋은 방법도 아닐뿐만 아니라, int (4byte) 이상 방이 만들어질 가능성도 적다.

 

2-2. Room Manager 추가 

#include <string>
#include <WinSock2.h>
#include <set>
#include <map>
#include <memory>
#include "foundation.h"

const std::string EXIT_ROOM_COMPLETE = "/Exit_Room_Complete";
const std::string ROOM_CLIENT_EVENT = "/Room_Event ";
const std::string UPDATE_ID = "Update_ID ";
const std::string UPDATE_READY_STATE = "Update_Ready_State ";
const std::string READY = "READY";
const std::string DONE = "DONE";

enum class TargetType
{
	SELF,        
	OTHERS,      
	ALL          
};

class Room_Manager
{
private:
	std::string roomName;
	std::set<SOCKET> sockets;
	std::map<std::string, std::shared_ptr<User>> users;
	std::map<SOCKET, std::string> socketUserID;
	int roomNumber;
	int roomCount;
public:
	Room_Manager(const int roomNumber, const std::string& roomName);
	static std::shared_ptr<Room_Manager> createRoom(const int roomNumber, const std::string& roomName);
	void broadcast_Message(const std::string& message, SOCKET sender_socket, TargetType target_type);
	std::string getUserIDFromSocket(const SOCKET& socket) const;
	std::pair<std::string, bool> getOtherUserInfo(const std::string& userID);
	std::pair<std::string, bool> getThisUserInfo(const std::string& userID);
	

	void addUser(const std::string& userID, SOCKET ID);
	void removeUser(const std::string& userID, SOCKET ID);
	void userUpdate(SOCKET ID);
	bool isroomEmpty() const;
	int getroomCount() const;
	std::string getroomName() const;
	void roomCountSet(const std::string& type);
};

- Room Manager라는 것을 추가했다.

- Client_Handle에서 Room Manager를 통해서 해당 로직을 처리하는 방식으로 수정했다.

 

① Before Handle_Create_Chatting_Room()

void ClientEventHandler::Handle_Create_Chatting_Room(const std::string& message)
{
	this->roomName = message;
	std::string send_message = "";
	{
		const std::lock_guard<std::mutex> lock(chatRoom_mutex);
		if (chatRooms.find(roomName) != chatRooms.end())
		{
			send_message = EXIST_ROOM;
		}
		else
		{
			std::cout << "[Log] : Client Create Room : " << message << std::endl;

			chatRooms[message].insert(socket);
			Room_count[message] = 0;

			std::shared_ptr<Game_Manager> roomGameManager = std::make_shared<Game_Manager>(roomName);
			gameManagers[roomName] = roomGameManager;

			send_message = roomName;

		}
	}
	send(socket, send_message.c_str(), send_message.length(), 0);
}

 

② After Handle_Create_Chatting_Room()

void ClientEventHandler::Handle_Create_Chatting_Room(const std::string& roomName)
{
	const std::lock_guard<std::mutex> lock(chatRoom_mutex);
	std::string send_message = std::to_string(roomcountNumber);

	std::shared_ptr<Room_Manager> roomManager = Room_Manager::createRoom(roomcountNumber, roomName);
	roomManagers[roomcountNumber] = roomManager;

	std::shared_ptr<Game_Manager> gameManager = std::make_shared<Game_Manager>(roomcountNumber, roomManager);
	gameManagers[roomcountNumber] = gameManager;
	roomcountNumber++;

	std::cout << "[System] : Room_Number : " << send_message << "Room_Name : " << roomName << std::endl;
	SendPacket(this->socket, send_message);
}

- 객체를 생성할 때 Smart Pointer를 사용했다.

- 방을 생성할 때 만들어지는 Room_Manager와 Game_Manager는 방이 삭제되는 시점(erase) 시점에 반환된다.

 

Handle_Exit_Room()

void ClientEventHandler::Handle_Exit_Room()
{
	std::cout << "[System] : " << "Handle_exit_Room" << std::endl;
	std::map<int, std::shared_ptr<Room_Manager>>::iterator it;

	it = roomManagers.find(this->roomNumber);
	if (it != roomManagers.end())
	{
		std::cout << "[System] : Room Find.";
		it->second->removeUser(this->ID, this->socket);
		if (it->second->isroomEmpty() == true)
		{
			roomManagers.erase(this->roomNumber);
			gameManagers.erase(this->roomNumber);
			this->roomNumber = 0;
		}
	}
	else
	{
		std::cout << "[System] : Room Not Found";
		return ;
	}
}

- 객체를 삭제하는 시점에서 자원이 자동으로 반환하도록 처리했다.

 

기존 Client_Handler에서 하던 작업의 대부분은 옮겼다. 

 

2-3. Room Manager 내부 처리

void Room_Manager::addUser(const std::string& userID, SOCKET socket)
{
	users[userID] = std::make_shared<User>(userID);
	sockets.insert(socket);
	socketUserID[socket] = userID;
	roomCountSet("PLUS");
}
void Room_Manager::removeUser(const std::string& userID, SOCKET socket)
{
	std::string exit_message = EXIT_ROOM_COMPLETE;
	broadcast_Message(exit_message, socket,TargetType::SELF);

	users.erase(userID);
	sockets.erase(socket);
	socketUserID.erase(socket);
	roomCountSet("MINUS");

	exit_message = userID + " has exited.";
	std::cout << "System : " << exit_message << std::endl;
	broadcast_Message(exit_message,socket,TargetType::OTHERS);
}

 

void Room_Manager::userUpdate(SOCKET clientSocket)
{
	std::string update_message;
	if (roomCount > 1)
	{
		for (SOCKET target_socket : sockets)
		{
			if (target_socket == clientSocket)
			{
				std::string ID = getUserIDFromSocket(clientSocket);
				std::pair<std::string, bool> userData = getThisUserInfo(ID);

				update_message = ROOM_CLIENT_EVENT + UPDATE_ID + userData.first;
				std::cout << "Update_message : " << update_message << std::endl;
				broadcast_Message(update_message, clientSocket, TargetType::OTHERS);

				if (userData.second == false)
				{
					update_message = ROOM_CLIENT_EVENT + UPDATE_READY_STATE + READY;
				}
				else
				{
					update_message = ROOM_CLIENT_EVENT + UPDATE_READY_STATE + DONE;
				}
				std::cout << "Update_message : " << update_message << std::endl;
				broadcast_Message(update_message, clientSocket, TargetType::OTHERS);
			}
			else
			{
				std::string ID = getUserIDFromSocket(target_socket);
				std::pair<std::string, bool> userData = getThisUserInfo(ID);

				update_message = ROOM_CLIENT_EVENT +  UPDATE_ID + userData.first;
				std::cout << "Update_message : " << update_message << std::endl;
				broadcast_Message(update_message, clientSocket, TargetType::SELF);

				if (userData.second == false)
				{
					update_message = ROOM_CLIENT_EVENT + UPDATE_READY_STATE + READY;
				}
				else
				{
					update_message = ROOM_CLIENT_EVENT + UPDATE_READY_STATE + DONE;
				}
				std::cout << "Update_message : " << update_message << std::endl;
				broadcast_Message(update_message, clientSocket, TargetType::SELF);
			}
		}
	}
}

- Client_Handle에서 해야만 하는 작업이 아니라면, 가급적 Manager에서 처리하도록 다 갈아 엎었다..

- 이전에 다소 불필요하게 만들어놨던 이벤트(상수값)도 정리해서 가급적 비슷한 이벤트는 묶었다.

- 이를 적당히 묶어서, 불필요한 이벤트는 정리하고 반드시 구분이 필요한 이벤트만 남겼다.

 

2-4. Packet

#include "Packet.h"
#include <iostream>
#include <WinSock2.h>
std::string ReceivePacket(SOCKET clientSocket)
{
	char buffer[1025];
	uint32_t Packet_length = 0;
	int byte_length = recv(clientSocket, reinterpret_cast<char*>(&Packet_length), sizeof(Packet_length), 0);
	int errorCode = errno;
	char errorMessage[256];

	if (byte_length == 0)
	{
		std::cerr << "Recive : Connection closed." << strerror_s(errorMessage,sizeof(errorMessage),errorCode) << std::endl;
		return "";
	}
	if (byte_length < 0)
	{
		std::cerr << "Recive : Error" << strerror_s(errorMessage, sizeof(errorMessage), errorCode) << std::endl;
		return "";
	}

	Packet_length = ntohl(Packet_length);
	if (Packet_length > 1024)
	{
		std::cerr << "Packet too large. Packet_length value changed." << std::endl;
		Packet_length = 1024;
	}

	byte_length = recv(clientSocket, buffer, Packet_length, 0);
	if (byte_length <= 0)
	{
		std::cerr << "Received Error." << std::endl;
		return "";
	}
	buffer[Packet_length] = '\0';

	std::string test = buffer;
	std::cout << "[System] Receive " << Packet_length << " " << test << std::endl;
	
	return test;
}

void SendPacket(SOCKET clientSocket, const std::string& message)
{
	uint32_t Packet_length = htonl(message.size());

	send(clientSocket, reinterpret_cast<char*>(&Packet_length), sizeof(Packet_length), 0);

	std::cout << "[System] : Send message_size : " << message.size() << "message : " << message << std::endl;
	
	send(clientSocket, message.c_str(), message.size(), 0);
}

- 대체 무슨 생각으로 내가 이걸 안 만들었을까..

- 그것보다 더 이상했던건 이전 Multi Room Chatting Server는 어떻게 문제가 없었을까...

- 이 리팩토링의 가장 큰 핵심은 여기라고 생각한다.

- Server와 Client에서 송수신시 길이 정보를 우선 읽어들인 이후, 길이 정보를 먼저 보낸 이후에 메시지를 처리한다.

 

Comments