[TIL] 24.11.03 SUN

GDORI·2024년 11월 3일
0

TIL

목록 보기
91/143
post-thumbnail

오늘은 지난 과제간 있었던 트러블에 대하여 정리하는 시간을 가졌다.

Latency

데이터가 한 지점에서 다른 지점까지 도달하는데 걸리는 시간을 뜻한다.
금요일 제출한 CH5 개인과제에서도 서버-클라이언트 간 PING 패킷을 이용하여 레이턴시 측정을 수행하고
그 기반으로 추측항법을 적용하였다.

근데, 보통 CMD에서 PING을 보내면 로컬환경에서 1ms 미만이지 않는가?

물론 CMD에서 보내는 핑은 TCP가 아닌 ICMP, Internet Control Message Protocol 으로 측정을 하지만 아무리 그래도
로컬환경에서 측정했을 때 40~50ms가 나오진 않을 것이다.
아, 근데 왜 로컬 환경에서 테스트 하는 나의 서버 - 클라이언트 간 레이턴시가 평균 4~50ms가 나오는지 이해가 되지 않았다.
(친구들 왈, 그 정도면 아시아 서버 아니냐고 ...)

첫번째 시도.

이상하다 싶어서 컴퓨터 내부를 살피기 시작했다.

  1. 전에 http 서버 과제 때 나를 괴롭혔던 프록시 프로그램부터 확인했다. > 아 이놈 아니다..
  2. 그렇다면 버퍼의 크기 설정 문제인가? > 늘려도 같았다.
  3. 패킷의 크기가 작고 많은 량을 보내다 보니 Nagle 알고리즘을 통해 밀려서 그렇게 되는가? > 비활성화 해도 같다.
  4. 일단 패킷량을 최대한 줄여보자 > 미세한 차이로 5ms 정도 줄어들었다.
  5. 요즘 컴퓨터 상태가 좋지 못하니 맥북과 윈도우 서버로 테스트 > 동일하다..

두번째 시도..

패킷 처리 코드에 문제가 있다고 생각했다.
패킷 처리하는 코드 부분에 console.time(지정명) / console.timeEnd(지정명) 을 이용하여 테스트를 진행.

protoBuf 직렬화 속도가 생각보다 엄청 빠르구나..
이것도 아니네..

세번째 시도...

유니티 클라이언트가 아닌 js로 클라이언트를 구현하여 테스트 하였다.
아.. 유니티 코드에 문제가 있겠구나, 패킷 받는 부분에 문제가 있을 것이다 생각하고 debug.log를 찍었다.

유니티 자체에서 핑을 받고 처리 후 보내는데 걸리는 시간은 1~2ms ..
핑을 받는 시간이 3~40ms..

도저히 모르겠어서 튜터님께 Help 요청

와... 이런게 있구나
Tick rate는 일반적으로 초당 몇 번의 "tick"이 발생하는지를 나타내며 예를 들어, 30FPS의 tick rate는 매초 30번의 업데이트가 발생한다.

즉, 30FPS 의 환경이라면 1000/30, 33.333ms 60FPS 의 환경이라면 1000/60, 16.666ms 마다 처리를 한다는 뜻이고
현재 클라이언트 코드는 Target Frame Rate가 30, 소켓 처리 코드가 Tick rate와 연관이 되어있어 33.333ms마다 수신을
처리한다.

아.. 바로 테스트 Target Frame Rate 60으로 변경


ㅋㅋㅋ 맞네 .. 이거였네.

그렇다면 프레임과 상관없이 측정할 수는 없는가?

비동기 수신을 통한 이벤트 기반 처리

패킷을 수신할 때마다 이벤트를 발생시켜 게임 주기랑 관련이 없이 처리하는 방법이 있다고 한다. 이 경우 수신한 데이터를
별도의 큐에 저장하고 게임 업데이트 주기와는 상관없이 처리하는 방법이라고 한다.

별도의 스레드나 Task를 사용하는 방법

비동기 수신을 별도의 스레드 또는 Task에서 실행하여 게임 메인 루프와 분리시키는 방법이 있고, 이렇게 하면 tick rate와는 상관
없이 처리할 수 있다고 한다.

데이터 수신 전용 루프를 사용하는 방법

게임 업데이트 루프와 별개로 핑 수신을 위한 전용 루프를 만들어서 독립적으로 실행되게 처리하는 방법

결론

코드에는 문제가 없었고, 만약 낮추고 싶다면 rate 빈도수를 올리던지 별도의 루프를 사용하는 방법이 있겠지만
유니티를 만질 줄 몰라 추후에 익힐 때 참고 해야겠다.

Latency 내용과 별개로 load ProtoBuf 관련하여 ,

이번 과제를 마치고 TCP 기본 틀을 편하게 가져다 쓰려고 git repository를 만들고 있는데 packetNames.js 없이
protoMessages를 등록시키는 방법이 없을까 생각을 해보았다.

nested

기존의 경우 root.load를 이용하여 저장된 proto경로를 모두 등록하고 packageNames.js를 이용하여 객체를 생성했었다.

근데, load된 root의 nested 값을 이용하면 패키지네임과 타입을 획득할 수 있다.

아, root.nested의 key값만 가져와서 반복문으로 돌려서 그 안의 nested 값이 존재하면 패키지 이니까 재귀로 돌려주고
존재하지 않으면 해당 key값(ex. common.CommonPacket) 을 split으로 분리시킨 후 protoMessages에 등록시켜주면 된다.

getTypes

function getTypes(root, prefix = '') {
  Object.keys(root.nested).forEach((key) => {
    const nestedObject = root.nested[key];
    const fullName = prefix ? `${prefix}.${key}` : key;

    if (nestedObject.nested) {
      // 패키지인 경우 재귀적으로 탐색
      getTypes(nestedObject, fullName);
    } else if (nestedObject instanceof protobuf.Type) {
      // 메시지 타입일 경우만 저장
      const [packageName, type] = fullName.split('.');

      // 패키지 이름이 이미 존재하지 않으면 객체 초기화
      if (!protoMessages[packageName]) protoMessages[packageName] = {};

      // 메시지 타입을 protoMessages 객체에 저장
      protoMessages[packageName][type] = root.lookupType(fullName);
    }
  });
}

loadProtos

export const loadProtos = async () => {
  try {
    const root = new protobuf.Root(); // 프로토콜 버퍼의 루트 객체 생성

    // 모든 .proto 파일을 로드하여 병합
    await Promise.all(protoFiles.map((file) => root.load(file)));
    // protoMessages 등록
    getTypes(root);
    const date = new Date();
    console.log(`[${formatDate(date)} - LOAD] Success to load protobuf files`);
  } catch (err) {
    const date = new Date();
    console.error(`[${formatDate(date)} - FAIL] Fail to load protobuf files`);
  }
};
profile
하루 최소 1시간이라도 공부하자..

0개의 댓글