포트폴리오

Select 모델 서버 프로젝트 03 - 네트워크 코어 함수 구현

묭묭.cpp 2024. 10. 26. 16:20

Select 모델 서버 프로젝트 03 - 네트워크 코어 함수 구현

CServerCore 클래스 멤버 함수 구현

CServerCore::Start

함수 선언부

BOOL CServerCore::Start(CONST CHAR *openIp, CONST USHORT port, INT maxSessionCount);

구현한 것

  • WSADATA를 초기화
  • 리슨 소켓 생성
  • bind, listen
  • 논 블로킹 소켓 전환
    • select 함수를 통해 논 블로킹 소켓을 제어할 것이므로
  • 링거 옵션 설정
    • 서버 측에서 연결을 끊었을 때 TIME_WAIT 상태에 빠지지 않게 하기 위함
    • 서버 측에서 연결을 끊어야할 경우는 timeout 체크에 걸렸을 경우
      • 접속하고 아무 행위를 하지 않는 세션
      • 실제 게임이라면 핵 유저 감지 등이 있을 수 있음
  • 최대 세션 수 설정

딱히 특별한 것은 없음

CServerCore::Stop

함수 선언부

VOID CServerCore::Stop();

아직 구현하지 않음 : TODO

CServerCore::Select

함수 선언부

BOOL CServerCore::Select();

구현한 것

  • 63개 씩 unordered_map의 iterator를 통해 세션의 소켓에 접근하여 Select 모델의 fd_set에 등록함
  • Read Set에는 listen 소켓도 등록하여 Accept를 처리
  • FD_ISSET을 통해 readSet과 writeSet을 검사
    • 할일이 있다면 Recv와 Send를 진행
    • 사실 Send는 할일이 있다의 기준이 아니긴 함
      • 상대방의 윈도우(수신 버퍼)가 가득찬 경우가 아니면 WSAEWOULDBLOCK이 아님
      • 그런데 이런 경우는 상대방 측의 recv가 늦거나 비정상적인 상황
      • 정상적인 상황이라면 항상 readSet에서 반환될 것
    • Recv는 수신 버퍼에 PSH 비트가 켜진 데이터가 도착했다면 writeSet에서 반환됨
      • 없을 경우 WSAEWOULDBLOCK

iterator를 어떻게 63칸 씩 밀어줄까?

  1. C++ 표준의 std::advance
    • 이 함수는 iterator와 어느만큼 밀어줄지를 지정해서 사용

처음에 생각한 것은 iterator의 시작점을 받아 오프셋만큼 이동시키자!

  • 매 select 루프마다 시작점부터 오프셋을 계산하여 대입
  • 그런데 매우 느리다는 결과를 얻을 수 있었음

std::advance의 구현 코드를 보면

_EXPORT_STD template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off) { // increment iterator by offset
    if constexpr (_Is_ranges_random_iter_v<_InIt>) {
        _Where += _Off;
    } else {
        if constexpr (is_signed_v<_Diff> && !_Is_ranges_bidi_iter_v<_InIt>) {
            _STL_ASSERT(_Off >= 0, "negative advance of non-bidirectional iterator");
        }

        decltype(auto) _UWhere      = _STD _Get_unwrapped_n(_STD move(_Where), _Off);
        constexpr bool _Need_rewrap = !is_reference_v<decltype(_STD _Get_unwrapped_n(_STD move(_Where), _Off))>;

        if constexpr (is_signed_v<_Diff> && _Is_ranges_bidi_iter_v<_InIt>) {
            for (; _Off < 0; ++_Off) {
                --_UWhere;
            }
        }

        for (; 0 < _Off; --_Off) {
            ++_UWhere;
        }

        if constexpr (_Need_rewrap) {
            _STD _Seek_wrapped(_Where, _STD move(_UWhere));
        }
    }
}
  • 실제로 _Off 만큼 반복문을 돌리며 이동시키는 것을 볼 수 있음
  • 현재 매번 루프마다 63 * loopCount 만큼 이동시키므로 접속 세션이 많아질수록 비효율적임
  1. 이전 루프의 iterator를 저장했다가 std::advance를 항상 이전 iterator의 63칸만 이동시키자!
    • 훨씬 괜찮은 결과를 얻을 수 있었음
  2. std::advance를 사용하지 말자
    • iterator를 저장하고 하는 방식에서 좀 더 개선
    • startIt과 endIt 총 두번 std:advance를 호출하는 부분을 개선
      • for 문 반복 1번에 같이 이동시킴

테스트 코드

#include <stdio.h>
#include <string>
#include <map>
#include <tchar.h>
#include <time.h>
#include "windows.h"
#include "CProfiler.h"
#include <unordered_map>

std::unordered_map<INT, INT> unMap;

// TEST01
void stdAdvance01()
{
    PROFILE_BEGIN(__WFUNC__, 0);

    int loopCount = 0;

    while (true)
    {
        auto startIt1 = unMap.begin();
        auto endIt = startIt1;
        std::advance(startIt1, loopCount * 63);
        auto startIt2 = startIt1; // 백업용 또 써야하니깐
        std::advance(endIt, (loopCount + 1) * 63);

        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;
            // 등록 작업
        }

        startIt1 = startIt2;
        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;

            // Recv

            // Send
        }

        if (startIt1 == unMap.end())
            break;

        loopCount++;
    }
}

// TEST02
void stdAdvance02()
{
    PROFILE_BEGIN(__WFUNC__, 0);

    int loopCount = 0;

    auto startIt1 = unMap.begin();
    auto endIt = startIt1;
    auto startIt2 = startIt1;
    std::advance(endIt, 63);

    while (true)
    {
        startIt1 = startIt2;
        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;
            // 등록 작업
        }

        startIt1 = startIt2;
        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;

            // Recv

            // Send
        }

        std::advance(startIt2, 63);
        std::advance(endIt, 63);

        if (startIt1 == unMap.end())
            break;

        loopCount++;
    }
}

// TEST03
void stdAdvance03()
{
    PROFILE_BEGIN(__WFUNC__, 0);

    int loopCount = 0;

    auto startIt1 = unMap.begin();
    auto endIt = startIt1;
    auto startIt2 = startIt1;
    for (int i = 0; i < 63; i++)
    {
        ++endIt;
        if (endIt == unMap.end())
            break;
    }

    while (true)
    {
        startIt1 = startIt2;
        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;
            // 등록 작업
        }

        startIt1 = startIt2;
        for (; startIt1 != endIt; ++startIt1)
        {
            if (startIt1 == unMap.end())
                break;

            // Recv

            // Send
        }

        for (int i = 0; i < 63; i++)
        {
            ++startIt2;
            ++endIt;

            if (endIt == unMap.end())
                break;
        }

        if (startIt1 == unMap.end())
            break;

        loopCount++;
    }
}

int main()
{
    for (int i = 0; i < 10000; i++)
    {
        unMap.insert(std::make_pair(i, i));
    }


    for (int i = 0; i < 10; i++)
    {
        stdAdvance01();

        stdAdvance02();

        stdAdvance03();
    }

    return 0;
}
  • std::unordered_map에 1만개의 요소를 저장하고 순회 테스트

테스트 결과

test01

  • 1번 테스트 결과 : 매우 느림
  • 2번 테스트 결과 : 상당히 괜찮아진 모습을 보임
  • 3번 테스트 결과 : 2번 테스트보다 약간 괜찮아짐

따라서 std::advance를 사용하지 않고 직접 iterator를 옮기는 방식을 채택

CServerCore::TimeoutCheck

VOID CServerCore::TimeoutCheck();
  • Recv를 할때마다 각 세션에 m_iPrevRecvTime을 갱신함
  • 이것을 기준으로 TIME_OUT을 검사하여 삭제 대기 큐에 등록

CServerCore::Disconnect

BOOL CServerCore::Disconnect();
  • 실제 연결을 끊어주는 작업을 진행
  • OnCLientLeave 콜백을 호출하고
  • 세션 맵과 소켓을 제거, 세션 객체를 할당해제함

CServerCore::SendPacket

BOOL CServerCore::SendPacket(CONST UINT64 sessionId, CSerializableBuffer *message);
  • sessionId와 직렬화 버퍼를 매개변수로 받아 대상 세션의 sendBuffer에 Enqueue 함
  • 여기서 받은 직렬화 버퍼의 네트워크 단 헤더는 등록되지 않은 상태
    • 헤더를 생성하고 여기서 EnqueueHeader를 진행함

직렬화 버퍼 구조

  • [네트워크 헤더][페이로드]
  • 2바이트 - 패킷 식별자 + 크기
  • 페이로드 - 패킷 코드 + 실제 데이터
  • 크기 부분에는 패킷 코드와 네트워크 헤더의 크기가 제외된 실제 데이터의 크기만 들어감
    • CGameServer의 섹터 단위 전송 함수에서 이미 네트워크 헤더가 삽입된 경우가 있을 수 있음
    • 이 경우에는 EnqueueHeader 함수 내부에서 이미 삽입됨을 확인하고 그냥 반환하도록 직렬화 버퍼의 구조를 변경하였음

CServerCore::Accept

BOOL CServerCore::Accept();
  • Select 함수에서 리슨 소켓이 select에 의해 반환되었을 때 호출됨
  • accept를 수행하고 성공한 경우 세션 객체를 할당함
  • 세션 객체에 클라이언트 IP와 소켓, 할당한 id를 등록
  • 최종적으로는 OnAccept 콜백 함수를 호출하고 반환

CServerCore::Recv

BOOL CServerCore::Recv(CSession *pSession);
  • sendBuffer의 즉시 Enqueue 가능한 크기를 구해서 그 크기만큼 recv를 호출
  • recv의 반환값 검사
    • SOCKET_ERROR일 경우 WSAGetLastError 호출
    • select 모델을 사용하므로 WSAEWOULDBLOCK은 나올리 없음
      • 그러나 일단 검사하고 다른 에러일 경우 로그를 남기고 해당 세션을 삭제 대기 큐에 삽입함
    • 0일 경우 정상 종료로 처리
  • Process 함수를 호출하고 Recv 동작은 마무리

CServerCore::Send

BOOL CServerCore::Send(CSession *pSession);
  • Recv와 비슷하게 구현
  • 다른 점은 SendBuffer의 커서를 초기화 하는 부분이 추가됨
  • 리턴 값과 처음에 계산한 즉시 Dequeue 가능 크기를 비교
    • 같다면 SendBuffer의 커서를 초기값으로 변경
    • 이렇게 하면 링버퍼의 경계에 걸리는 일을 줄일 수 있음

CServerCore::Process

BOOL CServerCore::Process(CSession *pSession);
  • Recv 함수에서 호출됨
  • 현재 처리할 수 있는 모든 패킷을 처리할 때까지 반복됨
  • 네트워크 부분의 헤더를 해석하고 직렬화 버퍼를 만듬
  • 네트워크 헤더를 제외한 부분을 OnRecv를 통해 콘텐츠 서버로 전달

여기까지 구현하면 이제 상속받아 콘텐츠 서버를 만들 준비는 끝

다음 목표는 콘텐츠 서버를 구현하고 캐릭터가 접속되는 것까지 확인

반응형