본문 바로가기
개인공부/IOCP 서버 제작 실습

IOCP 서버 제작 실습 7

by 하고싶은건많은놈 2023. 3. 25.

DB로 Redis를 연동하여 로그인 작업을 구현

 

PacketManager의 Run() 함수 실행시 RedisManager 객체를 생성하여 Init() 및 Run() 함수 실행

Connect() 함수에서 Redis DB와 연동한 후 DB 작업을 수행하는 쓰레드를 생성하여 ProcessTask() 함수 실행

ProcessTask() 함수에서는 deque에 RequestTask가 있다면 해당 Task에 맞는 작업을 수행

 

  • Login 로직
    1. [PacketManager] 패킷 처리 쓰레드의 ProcessPacket() 함수에서 수신된 패킷 데이터를 확인하여 패킷Id가 LOGIN_REQUEST일 경우 ProcessLogin() 함수 실행
    2. [PacketManager] ProcessLogin() 함수에서 서버의 최대 접속자 수 초과 여부, 중복 접속 요청 여부를 검사
      • 접속자 수 초과일경우 해당하는 LOGIN_USER_USED_ALL_OBJ 에러 코드를 LOGIN_RESPONSE_PACKET에 넣어 전송
      • 중복 접속일 경우 해당하는 LOGIN_USER_ALREADY 에러 코드를 LOGIN_RESPONSE_PACKET에 넣어 전송
      • 중복이 아닐 경우 DB에 유저ID와 PW 확인 요청을 해야하므로 TaskId로 REQUEST_LOGIN을 가진 Task를 생성하여 RedisManger의 PsuhRequest() 함수를 호출해 deque에 저장
    3. [RedisManager] Task 처리 쓰레드에서 수신된 Task 데이터를 확인하여 TaskId가 REQUEST_LOGIN일 경우 ProcessLogin() 함수 실행
    4. [RedisManager] CredisConn 클래스(c++에서 redis를 사용하기 위해 활용, 내부적으로 GET / SET 등의 redisCmd를 호출하여 Redis의 기능을 구현함)의 get() 함수를 호출하여 UserID에 대한 value값이 있는지 확인
      • value값이 존재한다면 입력받은 UserPW와 동일한지 판별, 아니라면 LOGIN_USER_INVALID_PW 에러 코드를 가진 Task를 생성하여 PushResponse() 함수로 deque에 저장
      • value값이 존재하지 않는다면 유저 생성으로 취급하여 TaskId로 RESPONSE_LOGIN을 가진 Task를 생ㅅ헝하여 PushResponse() 함수로 deque에 저장
    5. [PacketManager] 패킷 처리 쓰레드의 ProcessPacket() 함수에서 TakeResponseTask() 함수로 Task를 가져와 ProcessLoginDBResult() 함수를 실행
    6. [PacketManager] Task의 Result를 확인
      • 에러 코드가 설정되어있지 않다면 클라이언트로 로그인 성공 응답 패킷 전송, 추가로 UserManager에 등록되어있지 않은 ID라면 새롭게 등록
      • 에러 코드가 설정되어있다면 클라이언트로 에러 코드에 해당하는 응답 패킷 전송

 


 

  • RedisTaskDefine.h
    • 기존 패킷과 비슷한 구조를 가진 Task 패킷을 정의
    • RedisTask가 기존의 패킷 헤더에 해당
#pragma once

#include "ErrorCode.h"

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include "Packet.h"

enum class RedisTaskID : UINT16
{
	INVALID = 0,

	REQUEST_LOGIN = 1001,
	RESPONSE_LOGIN = 1002
};

struct RedisTask
{
	UINT32		UserIndex = 0;
	UINT16		TaskId = 0;
	UINT16		DataSize = 0;
	char		*pData = nullptr;

	void Release()
	{
		if (pData != nullptr)
		{
			delete[] pData;
		}
	}
};

#pragma pack(push, 1)

struct RedisLoginReq
{
	char UserID[MAX_USER_ID_LEN + 1];
	char UserPW[MAX_USER_PW_LEN + 1];
};

struct RedisLoginRes
{
	char UserID[MAX_USER_ID_LEN + 1];
	UINT16 Result = (UINT16)ERROR_CODE::NONE;
};

#pragma pack(pop)

 

 

  • RedisManager.h
    • 각 함수들의 구성과 작동방식은 PacketManager와 거의 동일함
#pragma once

#include "RedisTaskDefine.h"
#include "ErrorCode.h"
#include "./thirdparty/CRedisConn.h"

#include <unordered_map>
#include <vector>
#include <deque>
#include <thread>
#include <mutex>

class RedisManager
{
private:
	typedef void(RedisManager::* PROCESS_TASK_PACKET_FUNCTION)(UINT32, UINT16, char*);

	std::unordered_map<int, PROCESS_TASK_PACKET_FUNCTION> mTaskFunctionDictionary;

	RedisCpp::CRedisConn		mConn;

	bool						mIsRunProcessThread = false;
	std::vector<std::thread>	mTaskThreads;

	std::mutex					mReqLock;
	std::deque<RedisTask>		mRequestTask;

	std::mutex					mResLock;
	std::deque<RedisTask>		mResponseTask;

	bool Connect(std::string ip_, UINT16 port_)
	{
		if (mConn.connect(ip_, port_) == false)
		{
			std::cout << "Connect Error : " << mConn.getErrorStr() << std::endl;
			return false;
		}
		else
		{
			std::cout << "Connect Success\n";
		}

		return true;
	}

	void PushResponse(RedisTask task_)
	{
		std::lock_guard<std::mutex> guard(mResLock);
		mResponseTask.push_back(task_);
	}

	RedisTask DequeRequestTask()
	{
		std::lock_guard<std::mutex> guard(mReqLock);

		if (mRequestTask.empty())
		{
			return RedisTask();
		}

		auto task = mRequestTask.front();
		mRequestTask.pop_front();

		return task;
	}

	void ProcessTask()
	{
		std::cout << "Redis Thread start...\n";

		while (mIsRunProcessThread)
		{
			bool isIdle = true;

			auto task = DequeRequestTask();
			if (task.TaskId != 0)
			{
				isIdle = false;
				ProcessTaskPacket(task.UserIndex, task.TaskId, task.DataSize, task.pData);
			}

			if (isIdle)
			{
				std::this_thread::sleep_for(std::chrono::milliseconds(1));
			}
		}
	}
	void ProcessTaskPacket(const UINT32 userIndex_, const UINT16 taskId_, const UINT16 dataSize_, char* pData_)
	{
		auto iter = mTaskFunctionDictionary.find(taskId_);
		if (iter != mTaskFunctionDictionary.end())
		{
			(this->*(iter->second))(userIndex_, dataSize_, pData_);
		}
	}

	void ProcessLogin(UINT32 userIndex_, UINT16 dataSize_, char* pData_)
	{
		auto pRequest = (RedisLoginReq*)pData_;

		RedisLoginRes bodyData;
		bodyData.Result = (UINT16)ERROR_CODE::LOGIN_USER_INVALID_PW;
		CopyMemory(bodyData.UserID, pRequest->UserID, (MAX_USER_ID_LEN + 1));

		std::string value;
		if (mConn.get(pRequest->UserID, value))
		{
			if (value.compare(pRequest->UserPW) == 0)
			{
				bodyData.Result = (UINT16)ERROR_CODE::NONE;
			}
			else
			{
				std::cout << "Wrong PassWord\n";
			}
		}
		else
		{
			std::cout << "User " << pRequest->UserID << " Created\n";
			mConn.set(pRequest->UserID, pRequest->UserPW);
			bodyData.Result = (UINT16)ERROR_CODE::NONE;
		}

		RedisTask resTask;
		resTask.UserIndex = userIndex_;
		resTask.TaskId = (UINT16)RedisTaskID::RESPONSE_LOGIN;
		resTask.DataSize = sizeof(RedisLoginRes);
		resTask.pData = new char[resTask.DataSize];
		CopyMemory(resTask.pData, (char*)&bodyData, resTask.DataSize);

		PushResponse(resTask);
	}

public:
	RedisManager() = default;
	~RedisManager() = default;

	void Init()
	{
		mTaskFunctionDictionary[(int)RedisTaskID::REQUEST_LOGIN] = &RedisManager::ProcessLogin;
	}

	bool Run(std::string ip_, UINT16 port_, const UINT32 threadCount_)
	{
		if (Connect(ip_, port_) == false)
		{
			std::cout << "Redis Conncetion fail\n";
			return false;
		}

		mIsRunProcessThread = true;

		for (UINT32 i = 0; i < threadCount_; i++)
		{
			mTaskThreads.emplace_back([this]() { ProcessTask(); });
		}

		std::cout << "Redis is running...\n";
		return true;
	}

	void End()
	{
		mIsRunProcessThread = false;

		for (auto& thread : mTaskThreads)
		{
			if (thread.joinable())
			{
				thread.join();
			}
		}
	}

	void PushRequest(RedisTask task_)
	{
		std::lock_guard<std::mutex> guard(mReqLock);
		mRequestTask.push_back(task_);
	}

	RedisTask TakeResponseTask()
	{
		std::lock_guard<std::mutex> guard(mResLock);

		if (mResponseTask.empty())
		{
			return RedisTask();
		}

		auto task = mResponseTask.front();
		mResponseTask.pop_front();

		return task;
	}
};

 

 

  • PacketManager.h / cpp
    • Run() 함수에 RedisManager 관련 처리를 하는 동작들을 추가
    • ProcessPacket() 함수에 RedisManager의 Task를 확인하는 작업 추가, 패킷과 Task의 구조는 동일하기때문에 똑같이 ProcessRecvPacket() 함수로 처리 가능
    • ProcessLogin() 함수는 로그인시 발생할 수 있는 경우들에 대해 적절히 처리하도록 구성
    • ProcessLoginDBResult() 함수에서는 DB에 저장된 유저 정보를 확인
bool PacketManager::Run()
{
	mRedisMgr = new RedisManager;

	mRedisMgr->Init();

	if (mRedisMgr->Run("127.0.0.1", 6379, 1) == false)
	{
		return false;
	}

	mIsRunProcessThread = true;
	mProcessThread = std::thread([this]() { ProcessPacket(); });

	return true;
}

void PacketManager::ProcessPacket()
{
	while (mIsRunProcessThread)
	{
		bool isIdle = true;

		auto packetData = DequePacketData();
		if (packetData.PacketId != 0)
		{
			isIdle = false;
			ProcessRecvPacket(packetData.ClientIndex, packetData.PacketId, packetData.DataSize, packetData.pDataPtr);
		}

		auto task = mRedisMgr->TakeResponseTask();
		if (task.TaskId != (UINT16)RedisTaskID::INVALID)
		{
			isIdle = false;
			ProcessRecvPacket(task.UserIndex, task.TaskId, task.DataSize, task.pData);
			task.Release();
		}

		if (isIdle)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
		}
	}
}

void PacketManager::ProcessLogin(UINT32 clientIndex_, UINT16 packetSize_, char* pPacket_)
{
	if (LOGIN_REQUEST_PACKET_SIZE != packetSize_)
	{
		return;
	}

	auto pLoginReqPacket = reinterpret_cast<LOGIN_REQUEST_PACKET*>(pPacket_);

	auto pUserID = pLoginReqPacket->UserID;
	std::cout << "requested user id = " << pUserID << std::endl;

	LOGIN_RESPONSE_PACKET loginResPacket;
	loginResPacket.PacketId = (UINT16)PACKET_ID::LOGIN_RESPONSE;
	loginResPacket.PacketLength = sizeof(LOGIN_RESPONSE_PACKET);

	// Max 접속자 수 초과
	if (mUserManager->GetCurrentUserCnt() >= mUserManager->GetMaxUserCnt())
	{
		std::cout << "Server is Full\n";
		loginResPacket.Result = (UINT16)ERROR_CODE::LOGIN_USER_USED_ALL_OBJ;
		SendPacketFunc(clientIndex_, sizeof(LOGIN_RESPONSE_PACKET), (char*)&loginResPacket);
		return;
	}

	// 중복 접속
	if (mUserManager->FindUserIndexByID(pUserID) != -1)
	{
		std::cout << "Redundant Connection Fail\n";
		loginResPacket.Result = (UINT16)ERROR_CODE::LOGIN_USER_ALREADY;
		SendPacketFunc(clientIndex_, sizeof(LOGIN_RESPONSE_PACKET), (char*)&loginResPacket);
		return;
	}
	else
	{
		RedisLoginReq dbReq;
		CopyMemory(dbReq.UserID, pLoginReqPacket->UserID, (MAX_USER_ID_LEN + 1));
		CopyMemory(dbReq.UserPW, pLoginReqPacket->UserPW, (MAX_USER_PW_LEN + 1));

		RedisTask task;
		task.UserIndex = clientIndex_;
		task.TaskId = (UINT16)RedisTaskID::REQUEST_LOGIN;
		task.DataSize = sizeof(RedisLoginReq);
		task.pData = new char[task.DataSize];
	
		CopyMemory(task.pData, (char*)&dbReq, task.DataSize);
		mRedisMgr->PushRequest(task);

	}
}

void PacketManager::ProcessLoginDBResult(UINT32 clientIndex_, UINT16 packetSize_, char *pPacket_)
{
	std::cout << "ProcessLoginDBResult - UserIndex : " << clientIndex_ << std::endl;

	auto pBody = (RedisLoginRes*)pPacket_;
	
	if (pBody->Result == (UINT16)ERROR_CODE::NONE)
	{
		if (mUserManager->FindUserIndexByID(pBody->UserID) == -1)
			mUserManager->AddUser(pBody->UserID, clientIndex_);
		std::cout << "Login to Redis user id : " << pBody->UserID << std::endl;
	}

	LOGIN_RESPONSE_PACKET loginResPacket;
	loginResPacket.PacketId = (UINT16)PACKET_ID::LOGIN_RESPONSE;
	loginResPacket.PacketLength = sizeof(LOGIN_RESPONSE_PACKET);
	loginResPacket.Result = pBody->Result;
	SendPacketFunc(clientIndex_, sizeof(LOGIN_RESPONSE_PACKET), (char*)&loginResPacket);
}

'개인공부 > IOCP 서버 제작 실습' 카테고리의 다른 글

IOCP 서버 제작 실습 9  (0) 2023.03.27
IOCP 서버 제작 실습 8  (0) 2023.03.27
IOCP 서버 제작 실습 6  (0) 2023.03.23
IOCP 서버 제작 실습 5  (0) 2023.03.23
IOCP 서버 제작 실습 4  (0) 2023.03.22

댓글