포트폴리오
Select 모델 서버 프로젝트 01 - 서버 엔진 구조 잡기
묭묭.cpp
2024. 10. 24. 19:58
Select 모델 서버 프로젝트 01 - 서버 엔진 구조 잡기
목표
- 네트워크 코드와 컨텐츠 코드의 분리
- 네트워크 코드를 상속받아 컨텐츠 코드를 구현함으로 쉽게 재사용이 가능한 구조가 목표
- RPC(Remote Procedure Call) 구조
- RPC 구조를 통해 클라이언트에서 쉽게 서버단의 함수를 호출 가능
- 반복되는 코드의 자동화를 통해 빠른 개발이 가능
- 컨텐츠 개발자가 작성해야할 것은 받은 패킷을 처리하여 게임에 반영하는 코드 뿐
클래스 설계
CServerCore 클래스
/*
네트워크 코어 엔진
* 게임 서버 등의 콘텐츠 서버는 이를 상속받아 구현할 것
*/
class CServerCore
{
public:
friend class CProcessPacket;
BOOL Start(const CHAR *openIp, const USHORT port, INT maxSessionCount);
VOID Stop();
BOOL Select();
BOOL SendPacket(CONST UINT64 sessionId, char *packet, int size);
VOID TimeoutCheck();
BOOL Disconnect();
private:
BOOL Accept();
BOOL Recv(CSession *pSession);
BOOL Send(CSession *pSession);
public:
// 상속받는 쪽에서 호출할 콜백을 구현하여 등록해야함
virtual void OnAccept(CONST UINT64 sessionId) = 0;
virtual void OnClientLeave(CONST UINT64 sessionId) = 0;
virtual bool OnRecv(CONST UINT64 sessionId, CSerializableBuffer *message) = 0;
private:
std::unordered_map<INT, CSession *> m_mapSessions;
// Session 관련
UINT64 m_iCurSessionIDValue = 0; // 유저에게 부여할 ID 번호
int m_iMaxSessionCount = 0;
// DeleteQueue
std::deque<CSession *> m_deqDeleteQueue;
// 소켓 관련
SOCKET m_listenSocket;
fd_set m_readSet;
fd_set m_writeSet;
};
Start 함수
- ip와 port, 최대 세션 접속 수를 받음
- 최대 세션 접속 수를 초과하면 연결을 거부할 것임
- 기본적인 서버의 초기화를 담당함
- WSAStartup
- 리슨 소켓 생성
- ip, port 바인딩
- listen
- 소켓 옵션 설정까지 담당
Stop 함수
- 서버를 완전히 종료하고 다시 Start를 호출하여 재시작시킬 수 있음
- 이때 종료는 프로세스의 종료를 의미하는 것이 아닌 모든 사용중인 객체를 반환하고 다시 서버를 시작할 수 있는 상태로 만드는 것
Select 함수
- 실질적인 CServerCore 클래스의 핵심
- 1개의 리슨 소켓과 63개의 클라이언트 소켓을 set에 등록하고 select 함수를 호출
- Session을 저장하는 자료구조를 63개씩 순회하며 반복 호출
- 현재 자료구조는 std::unordered_map을 사용하고 있지만 성능에 대한 고민이 필요함
- 논블로킹 함수의 수행 가능 시점을 확인하고 가능한 소켓에 대해 Accept와 Send, Recv를 진행함
SendPacket 함수
- session 포인터를 받지 않고 sessionId를 받고 있음
- 이는 컨텐츠 코드에서 호출할 함수임을 의미
- 컨텐츠에서 session 정보에 접근이 불가능하게 설계
- session ID와 player ID는 현재 같음
- 이것 또한 고민이 필요한 부분
- 컨텐츠 쪽에는 player ID만 알고 이를 session ID로 변환하는 방법 등
TimeoutCheck 함수
- 일정 시간 동안 통신이 없는 세션을 찾아 delete 대상 큐에 등록
Disconnect 함수
- delete 대상 큐에 등록된 세션을 실제로 끊음
- 컨텐츠 측에서 구현한 OnClientLeave 콜백 함수를 호출함
Accept 함수
- select 함수에서 accept가 가능하다고 판정되었을 때 호출됨
- accept 함수로 접속을 수용하고 세션 구조체를 만듬
- 여기서 컨텐츠 쪽에서 구현한 OnAccept 콜백 함수가 호출됨
Recv 함수
- select 함수에서 recv가 가능하다고 판정되었을 때 호출됨
- recv를 수행하고 실패한 경우 delete 대상 큐로 세션을 넣음
- session 구조체의 prevRecvTime을 갱신
- CProcessPacket 클래스의 Process 함수를 호출
- 해당 함수 내부에서 네트워크 패킷의 헤더를 확인
- 패킷의 페이로드를 OnRecv 콜백 함수를 통하여 컨텐츠 쪽으로 넘김
Send 함수
- 컨텐츠 쪽에서 SendPacket을 통하여 send 링버퍼에 등록한 데이터를 전송함
- 링버퍼의 경계에 걸리지 않으면 모든 데이터를 전송함
- 경계에 걸린 경우 넘어서는 데이터는 다음번 Select에 전송됨
콜백 함수
OnAccept 함수
- 컨텐츠 측에서 Player 생성 등의 작업 처리를 위해 순수 가상 함수를 정의함
- Accept 함수에서 호출됨
OnClientLeave 함수
- 컨텐츠 측에서 Player 삭제 처리를 위하여 순수 가상 함수를 정의함
- Disconnect 함수에서 호출됨
OnRecv 함수
- 컨텐츠 측에서 도착한 패킷의 네트워크 헤더를 제외한 페이로드를 받아 직접 처리하도록 순수 가상 함수를 정의함
- Recv 함수에서 호출될 것 같지만 구조상 ProcessPacket의 Process 함수에서 호출됨
m_deqDeleteQueue 자료구조
- Select 호출 도중 세션의 삭제가 일어나면 문제가 발생함
- 63개씩 자료구조의 iterator를 통하여 순회 중이므로 중간 삭제 시 반복자의 무효화가 발생 가능
- 컨텐츠 측의 처리해야할 루틴이 처리되지 않을 수 있음
- 해당 프레임에 대한 처리는 모두 마친 후 삭제해야 함
CSession 클래스
class CSession
{
public:
friend class CServerCore;
friend class CProcessPacket;
CSession() = default;
CSession(int id, SOCKET sock) : m_iId(id), m_ClientSocket(sock) {}
private:
BOOL m_isVaild = TRUE;
UINT64 m_iId;
SOCKET m_ClientSocket;
WCHAR m_szClientIP[16] = { 0 };
INT m_iPrevRecvTime = 0;
CRingBuffer m_RecvBuffer;
CRingBuffer m_SendBuffer;
};
세션에 대한 정보를 저장하고 있음
- ServerCore와 ProcessPacket 클래스에서 Session 구조체에 접근할 일이 있기 때문에 friend 처리함
- ProcessPacket 클래스에서는 Process 함수에서만 접근되므로 클래스 멤버 함수에 대한 friend 처리를 하면 충분하지만 불완전한 형식 에러로 잘 되지 않음
- 나중에 알아보고 고칠 것
ProcessPacket 클래스
class CProcessPacketInterface
{
public:
virtual bool Process(CSession *session) = 0;
virtual bool ConsumePacket(PACKET_CODE code, UINT64 sessionId, CSerializableBuffer *message) = 0;
virtual bool PacketProcCSPacketName(UINT64 sessionId, CSerializableBuffer *message) = 0;
// ...
void SetServer(CServerCore *server) { m_Server = server; }
protected:
CServerCore *m_Server;
};
class CProcessPacket : public CProcessPacketInterface
{
public:
bool Process(CSession *session)
{
int size = session->m_RecvBuffer.GetUseSize();
while (size > 0)
{
PacketHeader header;
int ret = session->m_RecvBuffer.Peek((char *)&header, sizeof(PacketHeader));
// PacketHeader + PacketType + size
if (session->m_RecvBuffer.GetUseSize() < sizeof(PacketHeader) + 1 + header.bySize)
break;
session->m_RecvBuffer.MoveFront(ret);
CSerializableBuffer *buffer = new CSerializableBuffer;
ret = session->m_RecvBuffer.Dequeue(buffer->GetContentBufferPtr(), header.bySize + 1);
if (m_Server->OnRecv(session->m_iId, buffer))
return false;
delete buffer;
}
return true;
}
bool ConsumePacket(PACKET_CODE code, UINT64 sessionId, CSerializableBuffer *message)
{
switch (code)
{
case PACKET_CODE::CSPacketName:
return PacketProcCSPacketName(sessionId, message);
// ...
default:
break;
}
return false;
}
bool PacketProcCSPacketName(UINT64 sessionId, CSerializableBuffer *message);
// ...
};
ServerCore의 포인터를 들고 있는 이유
- OnRecv 콜백의 호출을 위해
- ServerCore 생성자나 초기화 시점에 이를 설정해주어야 함
Process 함수
- ServerCore 클래스에서 네트워크 부분의 헤더를 처리하기 위해 존재
- 직렬화 버퍼에서 네트워크 헤더 뒤에 쓰고 앞에 헤더를 저장하는 기능을 지원해야 함
- 예전에 구현해놓았음
- 여기서 네트워크 부분의 헤더를 처리하고 ServerCore 클래스의 OnRecv 콜백을 호출함
ConsumePacket 함수
- OnRecv 콜백 함수에서 호출될 것
- 여기서 실질적인 분기를 타서 패킷 타입에 맞는 처리를 진행함
비효율적인 구조로 보이지만
- RPC를 편하게 설계하려다보니 나온 구조
- 함수 코드를 자동으로 작성하는 프로그램을 위해 이런 구조가 나옴
- 다른 클래스로 구현하면 자동화 프로그램이 갱신할 때 코드가 덮어씌워지는 문제 발생
추가적인 서버를 위한 클래스
직렬화 버퍼
- 구조체 방식을 사용하면 편리하나 가변 길이 메시지가 불가능해짐
- 현재 가변 길이 메시지는 없으나 나중을 위해 직렬화 버퍼를 사용함
링 버퍼
- Send, Recv 횟수를 줄이기 위해 필요
- 버퍼링하여 Send와 Recv 횟수를 최소화함
- Send와 Recv는 굉장히 느림
오브젝트 풀
- 프리 리스트 기반의 오브젝트 풀
- 지금 당장은 성능 비교를 위해 사용하지는 않을 것
- 추후 성능 테스트 진행 예정
크래시 덤프
- 디버깅 목적 크래시 덤프
- 처리되지 않은 예외 발생 시 덤프 파일을 만듬
로그 클래스
- 로그를 편리하게 남기기 위한 목적
- 콘솔 모드와 파일 모드 분리
- 에러 레벨 또한 분리가 필요
- Err, System, Debug 모드 지원
성능 프로파일러
- 코드 성능 수행 시간을 모두 기록하여 통계를 냄
- 최적화 진행 시 이것을 기반으로 성능 비교를 진행할 것
모두 구현은 해뒀음.
다음 목표는 ServerCore를 상속받은 Game 서버를 설계하고 구현하는 것
반응형