- 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칸 씩 밀어줄까?
- 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 만큼 이동시키므로 접속 세션이 많아질수록 비효율적임
- 이전 루프의 iterator를 저장했다가 std::advance를 항상 이전 iterator의 63칸만 이동시키자!
- 훨씬 괜찮은 결과를 얻을 수 있었음
- 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만개의 요소를 저장하고 순회 테스트
테스트 결과
- 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일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)