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

IOCP 서버 제작 실습 6

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

서버의 종류를 에코 서버에서 채팅 서버로 변경

 

패킷과 유저를 관리하기 위한 PacketManager와 UserManager 및 User 클래스를 추가

네트워크 라이브러리(IOCPServer.h / ClientInfo.h / Define.h) 및 main.cpp는 기존과 동일(단, EchoServer 클래스의 명칭이 ChatServer로 바뀜)

 

  • 데이터 패킷 수신 로직
    1. [ClientInfo] 클라이언트 등록시 + 비동기 I/O Recv 작업 완료시 BindRecv()에서 WSARecv()
    2. [IOCPServer] Worker 쓰레드에서 비동기 Recv 작업 완료 감지
    3. [ChatServer] OnRecieve()
    4. [PacketManager] ReceivePacketdata()
    5. [User] SetPacketData()에서 User의 mPacketDataBuffer에 저장
    6. [PacketManager] EnqueuePacketData()에서 mPacketQueue에 저장
  • 데이터 패킷 처리 로직
    1. [ChatServer] Run()
    2. [PacketManager] Run()
    3. [PacketManager] 새로운 쓰레드에서 ProceessPacket()
    4. [PacketManager] ProcessRecvPacket()
    5. [PacketManager] ProcessUserConnect() 또는 ProcessUserDisConnect() 또는 ProcessLogin()
  • 데이터 패킷 송신 로직
    1. [PacketManager] ProcessLogin()에서 SendPackeFunc()
    2. [IOCPServer] SendMsg
    3. [ClientInfo] SendMsg
    4. [ClientInfo] SendIO()에서 WSASend()
    5. [IOCPServer] Worker 쓰레드에서 비동기 Send 작업 완료 감지
    6. [ClientInfo] SendCompleted()
  • 클라이언트 연결 로직
    1. [ChatServer] Run()
    2. [IOCPServer] StartServer()
    3. [IOCPServer] CreateAccepterThread()
    4. [IOCPServer] 새로운 쓰레드에서 AccepterThread()
    5. [ClientInfo] PostAccept()에서 AcceptEx()
    6. [IOCPServer] Worker 쓰레드에서 비동기 Accept 작업 완료 감지
    7. [ClientInfo] ACceptCompletion()
    8. [ClientInfo] OnConnect()
    9. [ChatServer] OnConnect
    10. [PacketManager] PushSystemPacket() 함수로 SYS_USER_CONNECT 패킷을 전송
    11. [PacketManager] ProcessUserConnect()

 


 

  • Packet.h
    • 모든 패킷은 패킷크기, 패킷 Id, 패킷 타입을 포함한 패킷 헤더를 가짐
    • 패킷의 종류에 따라 패킷 헤더를 상속하면서 추가로 필요한 데이터를 가지도록 구성
    • #pragma pack(push, 1)으로 구조체에서 발생할 수 있는 패딩비트(구조체 크기를 가장 큰 자료형 기준으로 배수만큼 만들어주기 위해 추가되는 비트)를 제거
#pragma pack(push, 1)
struct PACKET_HEADER
{
	UINT16	PacketLength;
	UINT16	PacketId;
	UINT8	Type;
};

const UINT32 PACKET_HEADER_LENGTH = sizeof(PACKET_HEADER);

// 로그인 요청
const int	MAX_USER_ID_LEN = 32;
const int	MAX_USER_PW_LEN = 32;

struct LOGIN_REQUEST_PACKET : public PACKET_HEADER
{
	char	UserID[MAX_USER_ID_LEN + 1];
	char	UserPW[MAX_USER_PW_LEN + 1];
};

const size_t LOGIN_REQUEST_PACKET_SIZE = sizeof(LOGIN_REQUEST_PACKET);

struct LOGIN_RESPONSE_PACKET : public PACKET_HEADER
{
	UINT16	Result;
};

// 룸 입장 요청
struct ROOM_ENTER_REQUEST_PACKET : public PACKET_HEADER
{
	INT32	RoomNumber;
};

struct ROOM_ENTER_RESPONSE_PACKET : public PACKET_HEADER
{
	INT16	Result;
};

// 룸 퇴장 요청
struct ROOM_LEAVE_REQUEST_PACKET : public PACKET_HEADER
{
};

struct ROOM_LEAVE_RESPONSE_PACKET : public PACKET_HEADER
{
	INT16	Result;
};

// 룸 채팅
const int MAX_CHAT_MSG_SIZE = 256;

struct ROOM_CHAT_REQUEST_PACKET : public PACKET_HEADER
{
	char	Message[MAX_CHAT_MSG_SIZE + 1] = { 0, };
};

struct ROOM_CHAT_RESPONSE_PACKET : public PACKET_HEADER
{
	INT16	Result;
};

struct ROOM_CHAT_NOTIFY_PACKET : public PACKET_HEADER
{
	char	UserID[MAX_USER_ID_LEN + 1] = { 0, };
	char	Msg[MAX_CHAT_MSG_SIZE + 1] = { 0, };
};
#pragma pack(pop)

 

  • User.h
    • SetPacketData()
      • 버퍼에 데이터를 저장할 위치를 mPacketDataBufferWPos, 버퍼에서 꺼내올 위치를 mPacketDataBufferRPos로 설정
      • 데이터를 저장할 위치부터 남은 메모리가 저장할 데이터의 크기보다 작은 경우 링버퍼 구조로 적절하게 WPos와 RPos를 조정
    • GetPacket()
      • 버퍼에 남아있는 remainByte가 패킷 헤더의 크기보다 작을경우 빈 패킷 반환
      • 패킷 헤더에 저장된 패킷 크기가 버퍼에 남아있는 remainByte보다 클 경우 빈 패킷 반환
      • 정상적인 경우 버퍼에 있는 패킷 데이터들로 새 패킷을 만들어 반환하고, 패킷 헤더에 저장된 패킷 크기만큼 RPos 이동
void SetPacketData(const UINT32 dataSize_, char *pData_)
{
    if ((mPacketDataBufferWPos + dataSize_) >= PACKET_DATA_BUFFER_SIZE)
    {
        auto remainDataSize = mPacketDataBufferWPos - mPacketDataBufferRPos;

        if (remainDataSize > 0)
        {
            CopyMemory(&mPacketDataBuffer[0], &mPacketDataBuffer[mPacketDataBufferRPos], remainDataSize);
            mPacketDataBufferWPos = remainDataSize;
        }
        else
        {
            mPacketDataBufferWPos = 0;
        }

        mPacketDataBufferRPos = 0;
    }

    CopyMemory(&mPacketDataBuffer[mPacketDataBufferWPos], pData_, dataSize_);
    mPacketDataBufferWPos += dataSize_;
}

PacketInfo GetPacket()
{
    const int PACKET_SIZE_LENGTH = 2;
    const int PACKET_TYPE_LENGTH = 2;
    short packetSize = 0;

    UINT32 remainByte = mPacketDataBufferWPos - mPacketDataBufferRPos;

    if (remainByte < PACKET_HEADER_LENGTH)
    {
        return PacketInfo();
    }

    auto pHeader = (PACKET_HEADER*)&mPacketDataBuffer[mPacketDataBufferRPos];

    if (pHeader->PacketLength > remainByte)
    {
        return PacketInfo();
    }

    PacketInfo packetInfo;
    packetInfo.PacketId = pHeader->PacketId;
    packetInfo.DataSize = pHeader->PacketLength;
    packetInfo.pDataPtr = &mPacketDataBuffer[mPacketDataBufferRPos];

    mPacketDataBufferRPos += pHeader->PacketLength;

    return packetInfo;
}

 

  • ChatServer.h
    • Run()
      • std::function : Callable("()" 를 붙여 호출할 수 있는 모든 것)을 객체의 형태로 보관할 수 있는 클래스
      • m_pPacketManager는 make_unique()를 통해 unique_ptr으로 생성되어 PacketManager 객체에 유일한 소유권을 부여
void Run(const UINT32 maxClient)
{
    auto sendPacketFunc = [&](UINT32 clientIndex_, UINT16 packetSize, char* pSendPacket)
    {
        SendMsg(clientIndex_, packetSize, pSendPacket);
    };

    m_pPacketManager = std::make_unique<PacketManager>();
    m_pPacketManager->SendPacketFunc = sendPacketFunc;
    m_pPacketManager->Init(maxClient);
    m_pPacketManager->Run();

    StartServer(maxClient);
}

 

  • ProcessManager.h / cpp
    • Init() 
      • 키값으로 int, 밸류값으로 PROCESS_RECV_PACKET_FUNCTION을 가지는 unordered_map인 mRecvFunctionDictionary를 초기화
      • 키값으로는 Packet.h에 정의해둔 PacketId, 밸류값은 PacketManager의 멤버 함수 포인터를 사용
    • ProcessPacket()
      • DequePacketData() 함수로 패킷 데이터를 가져옴
    • ProcessRecvPacket()
      • 패킷의 PacketId를 키값으로 하여 mRecvFunctionDictionary의 밸류값을 찾아 그에따른 멤버 함수 실행
void PacketManager::Init(const UINT32 maxClient_)
{
	//mRecvFuntionDictionary = std::unordered_map<int, PROCESS_RECV_PACKET_FUNCTION>();

	mRecvFuntionDictionary[(int)PACKET_ID::SYS_USER_CONNECT] = &PacketManager::ProcessUserConnect;
	mRecvFuntionDictionary[(int)PACKET_ID::SYS_USER_DISCONNECT] = &PacketManager::ProcessUserDisConnect;
	
	mRecvFuntionDictionary[(int)PACKET_ID::LOGIN_REQUEST] = &PacketManager::ProcessLogin;

	CreateComponent(maxClient_);
}

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);
		}

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

void PacketManager::EnqueuePacketData(const UINT32 clientIndex_)
{
	std::lock_guard<std::mutex> guard(mLock);

	auto pUser = mUserManager->GetUserByConnIdx(clientIndex_);
	auto packetData = pUser->GetPacket();
	mPacketQueue.push_back(packetData);
}

PacketInfo PacketManager::DequePacketData()
{

	std::lock_guard<std::mutex> guard(mLock);
	if (mPacketQueue.empty())
	{
		return PacketInfo();
	}

	auto packetData = mPacketQueue.front();
	mPacketQueue.pop_front();

	return packetData;
}

 

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

IOCP 서버 제작 실습 8  (0) 2023.03.27
IOCP 서버 제작 실습 7  (0) 2023.03.25
IOCP 서버 제작 실습 5  (0) 2023.03.23
IOCP 서버 제작 실습 4  (0) 2023.03.22
IOCP 서버 제작 실습 3  (0) 2023.03.22

댓글