반응형
  • 티스토리 홈
  • 프로필사진
    묭묭.cpp
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
묭묭.cpp
  • 프로필사진
    묭묭.cpp
    • 분류 전체보기 (103)
      • 데이터베이스 (2)
      • 포트폴리오 (25)
      • 윈도우 인터널즈 (20)
      • 네트워크 (4)
      • IOCP 게임서버 (11)
      • C (2)
      • 디스어셈블리 디버깅 (1)
      • WindowsAPI (11)
      • 학원 강의 정리 모음 (20)
      • 운영체제 (5)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
        등록된 공지가 없습니다.
      # Home
      # 공지사항
      #
      # 태그
      # 검색결과
      # 방명록
      • Select 모델 서버 프로젝트 03 - 네트워크 코어 함수 구현
        2024년 10월 26일
        • 묭묭.cpp
        • 작성자
        • 2024.10.26.: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를 통해 콘텐츠 서버로 전달

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

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

        반응형

        '포트폴리오' 카테고리의 다른 글

        Select 모델 서버 프로젝트 06 - 게임 콘텐츠 구현(이동)  (0) 2024.10.29
        Select 모델 서버 프로젝트 05 - 메시지(패킷) 자동화  (0) 2024.10.28
        Select 모델 서버 프로젝트 04 - 게임 콘텐츠 서버 구현  (0) 2024.10.26
        Select 모델 서버 프로젝트 02 - 게임 서버 설계  (0) 2024.10.25
        Select 모델 서버 프로젝트 01 - 서버 엔진 구조 잡기  (0) 2024.10.24
        다음글
        다음 글이 없습니다.
        이전글
        이전 글이 없습니다.
        댓글
      조회된 결과가 없습니다.
      스킨 업데이트 안내
      현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
      ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
      목차
      표시할 목차가 없습니다.
        • 안녕하세요
        • 감사해요
        • 잘있어요

        티스토리툴바