카테고리 없음

20250122

foreiner852 2025. 1. 22. 22:12

패킷파서

    socket.buffer = Buffer.concat([socket.buffer, data]);
    console.log('=== 새로운 패킷 수신 ===');
    console.log('수신된 데이터:', data);

    // 패킷의 버전 길이까지의의 헤더 길이 (패킷 길이 정보 + 버전 길이이 정보)
    const leastHeaderLength = config.packet.packetTypeLength + config.packet.versionLengthLength;
    let totalHeaderLength =
      config.packet.packetTypeLength +
      config.packet.versionLengthLength +
      config.packet.sequenceLength +
      config.packet.payloadLengthLength;
    let versionLength = 0;
    if (socket.buffer.length >= leastHeaderLength) {
      versionLength = socket.buffer.readUInt8(config.packet.packetTypeLength);
    }
    totalHeaderLength += versionLength;

    // 버퍼에 최소한 전체 헤더가 있을 때만 패킷을 처리
    while (socket.buffer.length >= totalHeaderLength) {
      let readHeadBuffer = 0;

      //패킷타입정보 수신(2바이트)
      const packetType = socket.buffer.readUInt16BE(readHeadBuffer);
      readHeadBuffer += config.packet.packetTypeLength + config.packet.versionLengthLength;

      //버전 수신
      const version = socket.buffer.slice(readHeadBuffer, readHeadBuffer + versionLength).toString('utf-8');
      readHeadBuffer += versionLength;

      // clientVersion 검증
      if (version !== config.client.version) {
        throw new CustomError(
          ErrorCodes.CLIENT_VERSION_MISMATCH,
          '클라이언트 버전이 일치하지 않습니다.',
        );
      }
      // 시퀀스 수신(4바이트)
      const sequence = socket.buffer.readUInt32BE(readHeadBuffer);
      readHeadBuffer += config.packet.sequenceLength;

      // 패이로드 길이 수신(4바이트)
      const payloadLength = socket.buffer.readUInt32BE(readHeadBuffer);
      readHeadBuffer += config.packet.payloadLengthLength;

      //패이로드
      testLog(0, `[onData] packetType: ${packetType} / versionLength: ${versionLength} / \n
        version: ${version} / sequence: ${sequence} / payloadLength: ${payloadLength} /   `, 'green');

      // 3. 패킷 전체 길이 확인 후 데이터 수신
      if (socket.buffer.length >= payloadLength + readHeadBuffer) {
        // 패킷 데이터를 자르고 버퍼에서 제거
        const packet = socket.buffer.slice(readHeadBuffer, readHeadBuffer + payloadLength);
        socket.buffer = socket.buffer.slice(readHeadBuffer + payloadLength);

        try {
          const user = getUserBySocket(socket);
          let game;
          // 유저가 접속해 있는 상황에서 시퀀스 검증
          if (user && user.getNextSequence() !== sequence) {
            throw new CustomError(ErrorCodes.INVALID_SEQUENCE, '잘못된 호출 값입니다. ');
          }
          if(user !== undefined){
            game = getGameByUser(user);
          }

          testLog(0, `from ondata  packet: ${packet} `);
          const payload = packetParser(packetType, packet);
          const handler = getHandlerById(packetType);
          await handler({
            socket,
            userId: user !== undefined ? user.id : null,
            payload,
            user,
          });
        } catch (error) {
          handleError(socket, error);
        }
      }

 

데이터를 분리하고 핸들러로 넘겨주는 내용이다. 처음 생각하기에는 큰 문제가 없어 보였지만 큰 문제는 패킷파서 함수에서 발생했다.

 


  const [namespace, typeName] = protoTypeName.split('.');
  const expectedPayloadType = protoMessages[namespace][typeName];
  const PayloadType = protoMessages['test']['GamePacket'];

 

수정이 된 이후의 패이로드 타입이다.

전에는 expectedPayloadType을 기반으로 데이터를 디코딩 했었는데 생각한 데이터 구조와 큰 차이가 있었다.

 

Payload 1: 0a170a04617364661204617364661a09617364664061736466
  • 0a: Field number and wire type.
  • 17: Length of the upcoming field.
  • 0a0461736466: Represents the string "asdf".
  • 12: Field number and wire type.
  • 0461736466: Represents the string "asdf".
  • 1a: Field number and wire type.
  • 09617364664061736466: Represents the string "asdf@asdf".
Payload 2: 0a06746573746572120831323334616263641a0f656d61696c40676d61696c2e636f6d
  • 0a: Field number and wire type.
  • 06: Length of the upcoming field.
  • 746573746572: Represents the string "tester".
  • 12: Field number and wire type.
  • 08: Length of the upcoming field.
  • 3132333461626364: Represents the string "1234abcd".
  • 1a: Field number and wire type.
  • 0f: Length of the upcoming field.
  • 656d61696c40676d61696c2e636f6d: Represents the string "email@gmail.com".

패이로드 1번이 클라이언트에서 넘어온 정보이고

페이로드 2번이 우리가 만든 테스트 클라이언트에서 넘어온 정보이다.

우리가 생각한 데이터의 형태는 당연히 테스트 클라이언트의 것이었다.

그러나 실제 클라이언트에서 넘어온 내용은 디코딩을 하기위한 정보가 부족했고 실제로 불가능했다.

 

// 최상위 GamePacket 메시지
message GamePacket {
    oneof payload {
        // 회원가입 및 로그인
        C2SRegisterRequest registerRequest = 1;
        S2CRegisterResponse registerResponse = 2;
        C2SLoginRequest loginRequest = 3;
        S2CLoginResponse loginResponse = 4;

        // 매칭
        C2SMatchRequest matchRequest = 5;
        S2CMatchStartNotification matchStartNotification = 6;

        // 상태 동기화
        S2CStateSyncNotification stateSyncNotification = 7;

        // 타워 구입 및 배치
        C2STowerPurchaseRequest towerPurchaseRequest = 8;
        S2CTowerPurchaseResponse towerPurchaseResponse = 9;
        S2CAddEnemyTowerNotification addEnemyTowerNotification = 10;

        // 몬스터 생성
        C2SSpawnMonsterRequest spawnMonsterRequest = 11;
        S2CSpawnMonsterResponse spawnMonsterResponse = 12;
        S2CSpawnEnemyMonsterNotification spawnEnemyMonsterNotification = 13;

        // 전투 액션
        C2STowerAttackRequest towerAttackRequest = 14;
        S2CEnemyTowerAttackNotification enemyTowerAttackNotification = 15;
        C2SMonsterAttackBaseRequest monsterAttackBaseRequest = 16;

        // 기지 HP 업데이트 및 게임 오버
        S2CUpdateBaseHPNotification updateBaseHpNotification = 17;
        S2CGameOverNotification gameOverNotification = 18;

        // 게임 종료  
        C2SGameEndRequest gameEndRequest = 19;

        // 몬스터 사망 통지
        C2SMonsterDeathNotification monsterDeathNotification = 20;
        S2CEnemyMonsterDeathNotification enemyMonsterDeathNotification = 21;
    }
}

 

게임 패킷 프로토 버퍼에서 패이로드 변환을 한번 해준 덕분에 첫번째 필드에 패킷 전체 크기에 대한 데이터가 들어와 이후의 필드를 나누지 못한 것이다.

 

그러나 게임패킷을 통해 디코딩을 하면 

패킷의 유형/ 해당 패킷의 크기 / 하위 목록

순서대로 값이 들어가며 정상적인 데이터가 디코딩 되었다.

 

프로토버퍼를 기반으로한 게임 데이터 명세를 엔코딩 하는 과정에서도 같은 문제를 마주칠 가능성이 높다.