포트폴리오

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 서버를 설계하고 구현하는 것

반응형