[ 2024.10.31 TIL ] multi-player-game

박지영·2024년 10월 31일
0

Today I Learned

목록 보기
72/84

multi-player-game

더미 클라이언트 만들기

더미 클라이언트 틀

더미 클라이언트에 사용할 틀은 강의에서 사용했던 client.js를 패킷 부분을 수정해서 사용해볼려고 한다.

  • 패킷 생성
const createPacket = (handlerId, userId, payload, clientVersion = '1.0.0', type, name) => {
  const protoMessages = getProtoMessages();
  const PayloadType = protoMessages[type][name];

  if (!PayloadType) throw new Error('PayloadType을 찾을 수 없습니다.');

  const payloadMessage = PayloadType.create(payload);
  const payloadBuffer = PayloadType.encode(payloadMessage).finish();

  return {
    handlerId,
    userId,
    version: clientVersion,
    payload: payloadBuffer,
  };
};
  • 패킷 보내기
const sendPacket = (socket, packet) => {
  const protoMessages = getProtoMessages();
  const Packet = protoMessages.common.Packet;
  if (!Packet) {
    console.error('Packet 메시지를 찾을 수 없습니다.');
    return;
  }

  const buffer = Packet.encode(packet).finish();

  // 패킷 길이 정보를 포함한 버퍼 생성
  const packetLength = Buffer.alloc(TOTAL_LENGTH);
  // 패킷 길이에 타입 바이트 포함
  packetLength.writeUInt32BE(buffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0);

  // 패킷 타입 정보를 포함한 버퍼 생성
  const packetType = Buffer.alloc(PACKET_TYPE_LENGTH);
  packetType.writeUInt8(1, 0); // NORMAL TYPE

  // 길이 정보와 메시지를 함께 전송
  const packetWithLength = Buffer.concat([packetLength, packetType, buffer]);

  socket.write(packetWithLength);
};
  • 핑 보내기
const sendPong = (socket, timestamp) => {
  const protoMessages = getProtoMessages();
  const Ping = protoMessages.common.Ping;

  const pongMessage = Ping.create({ timestamp });
  const pongBuffer = Ping.encode(pongMessage).finish();
  // 패킷 길이 정보를 포함한 버퍼 생성
  const packetLength = Buffer.alloc(TOTAL_LENGTH);
  packetLength.writeUInt32BE(pongBuffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0);

  // 패킷 타입 정보를 포함한 버퍼 생성
  const packetType = Buffer.alloc(1);
  packetType.writeUInt8(0, 0);

  // 길이 정보와 메시지를 함께 전송
  const packetWithLength = Buffer.concat([packetLength, packetType, pongBuffer]);

  socket.write(packetWithLength);
};
  • 연결시 초기 패킷 전송
socket.connect(PORT, HOST, async () => {
      const successPacket = createPacket(
        0,
        deviceId,
        {
          deviceId,
          playerId: 1,
          latency,
        },
        CLIENT_VERSION,
        'initial',
        'InitialPayload',
      );
      await sendPacket(socket, successPacket);
      await delay(500);
    });
  • data 이벤트
    • PING, NORMAL만 구현
socket.on('data', (data) => {
      const length = data.readUInt32BE(0);
      const totalHeaderLength = TOTAL_LENGTH + PACKET_TYPE_LENGTH;
      const packetType = data.readUInt8(4);
      const packet = data.subarray(totalHeaderLength, totalHeaderLength + length);

      if (packetType === PACKET_TYPE.NORMAL) {
        const Response = protoMessages.response.Response;

        try {
          const response = Response.decode(packet);
          const responseData = JSON.parse(Buffer.from(response.data).toString());

          if (response.handlerId === 0) {
            userId = responseData.userId;
          }
        } catch (e) {
          console.error(e);
        }
      } else if (packetType === PACKET_TYPE.PING) {
        try {
          const Ping = protoMessages.common.Ping;
          const actualPingData = packet.subarray(0, 7);
          const pingMessage = Ping.decode(actualPingData);

          const timestampLong = new Long(
            pingMessage.timestamp.low,
            pingMessage.timestamp.high,
            pingMessage.timestamp.unsigned,
          );
          sendPong(socket, timestampLong.toNumber());
        } catch (pongError) {
          console.error('Ping 처리 중 오류 발생:', pongError);
          console.error('Ping packet length:', packet.length);
          console.error('Raw ping packet:', packet);
        }
      }

더미 생성

  • node src/utils/dummy/dummyClient.js 로 더미 생성
(async () => {
  await loadProtos();
  const LIMIT = 200;
  const DELAY_MS = 25; // 0.05초
  const dummies = [];

  for (let i = 0; i < LIMIT; i++) {
    await delay(DELAY_MS); // 0.05초 대기

    const deviceId = uuidv4().slice(0, 5);
    const dummy = createDummyClient(deviceId);
    dummies.push(dummy);
    dummy.init();
  }

  console.log(`${dummies.length}개의 클라이언트가 추가되었습니다.`);
})();


더미 랜덤 이동

  • 반경 15 안에 랜덤한 위치에서 시작하고 그 점을 중심으로 작은 원을 계속 돌게 한다.
const createDummyClient = (deviceId) => {
  const protoMessages = getProtoMessages();
  let userId;
  let socket = new net.Socket();
  let x = 0;
  let y = 0;
  let latency = 50 + Math.random() * 100;
  let speed = 3;

  let framerate = 30; // 고정 30 프레임
  let radius = 10; // 캐릭터가 이동할 원의 반지름 (작은 원)
  let maxRadius = 15; // 반경 200 안에서 시작 위치를 설정
  let centerX = Math.random() * (2 * maxRadius) - maxRadius; // 중심 x 좌표 (랜덤)
  let centerY = Math.random() * (2 * maxRadius) - maxRadius; // 중심 y 좌표 (랜덤)
  let angle = Math.random() * 2 * Math.PI; // 랜덤 초기 각도

  const sendLocationUpdate = () => {
    if (!userId) return;

    setTimeout(() => {
      const deltaTime = 1 / framerate;
      const angularSpeed = speed / radius; // 각속도

      angle += angularSpeed * deltaTime; // 각도 업데이트

      // 작은 원을 따라 이동할 새로운 x, y 좌표
      x = centerX + radius * Math.cos(angle);
      y = centerY + radius * Math.sin(angle);

      const packet = createPacket(
        2,
        deviceId,
        { x, y },
        CLIENT_VERSION,
        'game',
        'UpdateLocationPayload',
      );

      sendPacket(socket, packet);
    }, latency);
  };
...
sendLocationUpdate();
...
}

트러블 슈팅

더미 클라이언트 핑 패킷 교환 시 바이트 길이 문제

문제

더미 클라이언트와 서버가 핑 패킷을 교환했을 때 패킷의 길이가 다르게 전달받는 문제

Ping 처리 중 오류 발생: RangeError: index out of range: 12 + 
10 > 12

사실 수집

  • 패킷 구조 확인
message Ping {
  int64 timestamp = 1;
}
  • 패킷 길이 확인
    원래 전달받아야 하는 길이가 8 이어야 하지만 7만큼만 전달됨
Ping packet too short: 7

원인추론

  • createPingPacket 함수에서 timestamp를 Long 타입으로 변환하지 않고 직접 JavaScript의 number를 사용하는 것.

  • Protocol Buffers의 int64 타입은 JavaScript의 number로는 정확하게 표현할 수 없을 수 있음.

해결 시도 및 해결

  1. createPingPacket의 timestamp Long 타입으로 변환하고 전송 로그 확인
export const createPingPacket = (timestamp) => {
  const protoMessages = getProtoMessages();
  const ping = protoMessages.common.Ping;

  // 디버깅을 위한 로그 추가
  console.log('Original timestamp:', timestamp);
  
  const timestampLong = Long.fromNumber(timestamp);
  console.log('Converted to Long:', timestampLong);
  
  const payload = { timestamp: timestampLong };
  console.log('Payload:', payload);
  
  const message = ping.create(payload);
  console.log('Created message:', message);
  
  const pingPacket = ping.encode(message).finish();
  console.log('Encoded packet length:', pingPacket.length);
  console.log('Encoded packet:', pingPacket);
  
  const serialized = serializer(pingPacket, PACKET_TYPE.PING);
  console.log('Final packet length:', serialized.length);
  console.log('Final packet:', serialized);

  return serialized;
};
Original timestamp: 1730359491371
Converted to Long: Long { low: -512328917, high: 402, unsigned: false }
Payload: { timestamp: Long { low: -512328917, high: 402, unsigned: false } }
Created message: Ping {
  timestamp: Long { low: -512328917, high: 402, unsigned: false }
}
Encoded packet length: 7
Encoded packet: <Buffer 08 ab f6 d9 8b ae 32>
Final packet length: 12
Final packet: <Buffer 00 00 00 0c 00 08 ab f6 d9 8b ae 32> 

Long 타입으로 정상적으로 변환되지만

Protocol Buffers가 이를 인코딩할 때 varint 형식으로 압축하여 7바이트로 인코딩됨.

  1. 재시도 후 로그 확인
Ping 처리 중 오류 발생: RangeError: index out of range: 12 + 
10 > 12
    at indexOutOfRange (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:13:12)
    at BufferReader.read_uint32 [as uint32] (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:98:19)     
    at Reader.skipType (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:372:37)
    at Type.Ping$decode [as decode] (eval at Codegen (D:\github\multi-player-game\node_modules\@protobufjs\codegen\index.js:50:33), <anonymous>:15:5)
    at Socket.<anonymous> (file:///D:/github/multi-player-game/src/utils/dummy/dummyClient.js:161:36)
    at Socket.emit (node:events:519:28)
    at addChunk (node:internal/streams/readable:559:12)      
    at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
    at Readable.push (node:internal/streams/readable:390:5)  
    at TCP.onStreamRead (node:internal/stream_base_commons:191:23)
Ping packet length: 12
Raw ping packet: <Buffer 08 8b 95 e3 8b ae 32 00 00 08 24 03>

Ping 메시지는 앞의 7바이트(08 8b 95 e3 8b ae 32)만 해당하고 뒷 자리에 불필요한 바이트들이 붙어있음.

  1. 핑 패킷을 앞 7바이트만 잘라서 사용하도록 수정하고 확인
else if (packetType === PACKET_TYPE.PING) {
  try {
    const Ping = protoMessages.common.Ping;
    
    // 실제 varint로 인코딩된 timestamp만 읽기 위해
    // 첫 7바이트만 사용
    const actualPingData = packet.slice(0, 7);  // varint 인코딩된 실제 데이터만 추출
    
    const pingMessage = Ping.decode(actualPingData);
    
    const timestampLong = new Long(
      pingMessage.timestamp.low,
      pingMessage.timestamp.high,
      pingMessage.timestamp.unsigned,
    );
    
    sendPong(this._socket, timestampLong.toNumber());
  } catch (pongError) {
    console.error('Ping 처리 중 오류 발생:', pongError);
    console.error('Ping packet length:', packet.length);
    console.error('Raw ping packet:', packet);
  }
}

오류없이 정상적으로 핑 교환 확인

// 서버 터미널
59252 ping
pong 59252 at 1730374376064 latency 2
14de1 ping
ffe06 ping
pong ffe06 at 1730374376078 latency 0.5
pong 14de1 at 1730374376080 latency 2
78b15 ping
4b818 ping
pong 4b818 at 1730374376096 latency 1.5
pong 78b15 at 1730374376096 latency 2
92995 ping
5fb72 ping
49b21 ping
pong 92995 at 1730374376100 latency 0.5
pong 5fb72 at 1730374376100 latency 0
pong 49b21 at 1730374376101 latency 0.5
8aa7c ping
f4f3b ping
pong f4f3b at 1730374376141 latency 1
pong 8aa7c at 1730374376143 latency 2
88b5d ping
02376 ping
16120 ping
pong 88b5d at 1730374376173 latency 2
pong 02376 at 1730374376173 latency 2
pong 16120 at 1730374376173 latency 2
be501 ping
pong be501 at 1730374376187 latency 1.5
0ec71 ping
pong 0ec71 at 1730374376205 latency 2.5
ebce9 ping
pong ebce9 at 1730374376206 latency 0
// 클라이언트 터미널
100개의 클라이언트가 추가되었습니다.
profile
신입 개발자

0개의 댓글