TIL

TIL_2025-01-08

explosion149 2025. 1. 8.

 

 

 

오늘 발생했던 에러

 

"패킷 디코딩 중 오류가 발생했습니다."

"클라이언트 버전이 일치하지 않습니다." 이건 갑자기 생겼다가 없어졌다.

"알 수 없는 핸들러 ID"

"패킷 구조가 일치하지 않습니다."

"필수 필드가 누락되었습니다"

 "Unknown packet type: Ping"

 

 

서버 코드 수정

 

PacketParser.js

 

더보기
/* 
이 코드는 수신한 데이터 패킷을 파싱하여 유효성을 검사하는 기능을 제공합니다.
패킷을 디코딩하고, 클라이언트 버전 및 핸들러 ID를 검증하며, 필요한 필드의 존재를 확인합니다.
오류가 발생할 경우 사용자 정의 오류를 생성하여 적절한 오류 코드를 반환합니다.
*/

import { getProtoMessages } from '../../init/loadProtos.js';  // 프로토 메시지 로드 함수 임포트
import { getProtoTypeNameByHandlerId } from '../../handlers/index.js';  // 핸들러 ID에 대한 프로토 타입 이름 가져오기
import { config } from '../../config/config.js';  // 설정 파일 임포트
import CustomError from '../error/customError.js';  // 사용자 정의 오류 클래스 임포트
import { ErrorCodes } from '../error/errorCodes.js';  // 오류 코드 상수 임포트

// 패킷 파서 함수
// export const packetParser = (data) => {
//   const protoMessages = getProtoMessages();  // 프로토 메시지 가져오기

//   // 공통 패킷 구조를 디코딩
//   const Packet = protoMessages.common.Packet;
//   let packet;
//   try {
//     packet = Packet.decode(data);  // 패킷 디코딩
//   } catch (error) {
//     console.log(data);
//     throw new CustomError(ErrorCodes.PACKET_DECODE_ERROR, '패킷 디코딩 중 오류가 발생했습니다.');  // 디코딩 오류 처리
//   }

//   const handlerId = packet.handlerId;  // 핸들러 ID
//   const userId = packet.userId;        // 사용자 ID
//   const clientVersion = packet.clientVersion;  // 클라이언트 버전
//   const sequence = packet.sequence;    // 시퀀스 번호

//   // clientVersion 검증
//   if (clientVersion !== config.client.version) {
//     throw new CustomError(

//       ErrorCodes.CLIENT_VERSION_MISMATCH,
//       '클라이언트 버전이 일치하지 않습니다.',  // 버전 불일치 처리
//     );
//   }

//   // 핸들러 ID에 따라 적절한 payload 구조를 디코딩
//   const protoTypeName = getProtoTypeNameByHandlerId(handlerId);
//   if (!protoTypeName) {
//     throw new CustomError(ErrorCodes.UNKNOWN_HANDLER_ID, `알 수 없는 핸들러 ID: ${handlerId}`);  // 알 수 없는 핸들러 ID 처리
//   }

//   const [namespace, typeName] = protoTypeName.split('.');  // 네임스페이스와 타입 이름 분리
//   const PayloadType = protoMessages[namespace][typeName];  // Payload 타입 가져오기
//   let payload;
//   try {
//     payload = PayloadType.decode(packet.payload);  // payload 디코딩
//   } catch (error) {
//     throw new CustomError(ErrorCodes.PACKET_STRUCTURE_MISMATCH, '패킷 구조가 일치하지 않습니다.');  // 구조 불일치 처리
//   }

//   // 필드가 비어 있거나, 필수 필드가 누락된 경우 처리
//   const expectedFields = Object.keys(PayloadType.fields);  // 예상 필드 목록
//   const actualFields = Object.keys(payload);  // 실제 필드 목록
//   const missingFields = expectedFields.filter((field) => !actualFields.includes(field));  // 누락된 필드 확인
//   if (missingFields.length > 0) {
//     throw new CustomError(
//       ErrorCodes.MISSING_FIELDS,
//       `필수 필드가 누락되었습니다: ${missingFields.join(', ')}`,  // 누락된 필드 처리
//     );
//   }

//   return { handlerId, userId, payload, sequence };  // 결과 반환
// };

// 버퍼를 16진수 문자열로 변환하는 함수
function bufferToHex(buffer) {
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(' ');
}

// 패킷을 파싱하는 함수
export const packetParser = (data) => {
  try {
    // 수신한 원시 패킷 데이터 로그 출력
    console.log("Received raw packet data:", bufferToHex(data));

    // Proto 메시지 가져오기
    const protoMessages = getProtoMessages();
    const Packet = protoMessages.common.Packet;

    // 패킷 디코딩
    let packet;
    try {
      packet = Packet.decode(data);
      // 디코딩된 패킷 정보 로그 출력
      console.log("Decoded packet:", {
        handlerId: packet.handlerId,
        userId: packet.userId,
        version: packet.version,
        sequence: packet.sequence,
        payloadLength: packet.payload ? packet.payload.length : 0
      });
    } catch (error) {
      // 패킷 디코딩 오류 처리
      console.error("Packet decode error:", error);
      throw new CustomError(ErrorCodes.PACKET_DECODE_ERROR, '패킷 디코딩 중 오류가 발생했습니다.');
    }

    // 핸들러 ID, 사용자 ID, 클라이언트 버전, 시퀀스 정보 추출
    const handlerId = packet.handlerId;
    const userId = packet.userId;
    const clientVersion = packet.version;
    const sequence = packet.sequence;

    // 클라이언트 버전 검증
    if (clientVersion !== config.client.version) {
      console.error(`Version mismatch - Client: ${clientVersion}, Server: ${config.client.version}`);
      throw new CustomError(
        ErrorCodes.CLIENT_VERSION_MISMATCH,
        '클라이언트 버전이 일치하지 않습니다.'
      );
    }

    // 핸들러 ID에 따른 Payload 구조 확인
    const protoTypeName = getProtoTypeNameByHandlerId(handlerId);
    if (!protoTypeName) {
      console.error(`Unknown handler ID: ${handlerId}`);
      throw new CustomError(
        ErrorCodes.UNKNOWN_HANDLER_ID,
        `알 수 없는 핸들러 ID: ${handlerId}`
      );
    }

    // Payload 타입 가져오기
    const [namespace, typeName] = protoTypeName.split('.');
    const PayloadType = protoMessages[namespace][typeName];

    // Payload 디코딩
    let payload;
    try {
      payload = PayloadType.decode(packet.payload);
      // 디코딩된 Payload 정보 로그 출력
      console.log("Decoded payload:", payload);
    } catch (error) {
      // Payload 디코딩 오류 처리
      console.error("Payload decode error:", error);
      throw new CustomError(
        ErrorCodes.PACKET_STRUCTURE_MISMATCH,
        '패킷 구조가 일치하지 않습니다.'
      );
    }

    // 필수 필드 검증
    const expectedFields = Object.keys(PayloadType.fields);
    const actualFields = Object.keys(payload);
    const missingFields = expectedFields.filter(
      (field) => !actualFields.includes(field)
    );

    // 누락된 필드가 있는지 확인
    if (missingFields.length > 0) {
      console.error("Missing fields:", missingFields);
      throw new CustomError(
        ErrorCodes.MISSING_FIELDS,
        `필수 필드가 누락되었습니다: ${missingFields.join(', ')}`
      );
    }

    // 성공적으로 패킷 파싱 로그 출력
    console.log("Successfully parsed packet:", {
      handlerId,
      userId,
      payload,
      sequence
    });

    // 파싱된 결과 반환
    return { handlerId, userId, payload, sequence };

  } catch (error) {
    // 이미 CustomError인 경우 그대로 전파
    if (error instanceof CustomError) {
      throw error;
    }
    // 그 외의 경우 새로운 CustomError로 래핑
    console.error("Unexpected error in packet parser:", error);
    throw new CustomError(
      ErrorCodes.PACKET_DECODE_ERROR,
      '패킷 처리 중 예상치 못한 오류가 발생했습니다.'
    );
  }

};

 

 

 

이전

더보기
// 패킷 파서 함수
export const packetParser = (data) => {
  const protoMessages = getProtoMessages();  // 프로토 메시지 가져오기

  // 공통 패킷 구조를 디코딩
  const Packet = protoMessages.common.Packet;
  let packet;
  try {
    packet = Packet.decode(data);  // 패킷 디코딩
  } catch (error) {
    console.log(data);
    throw new CustomError(ErrorCodes.PACKET_DECODE_ERROR, '패킷 디코딩 중 오류가 발생했습니다.');  // 디코딩 오류 처리
  }

  const handlerId = packet.handlerId;  // 핸들러 ID
  const userId = packet.userId;        // 사용자 ID
  const clientVersion = packet.clientVersion;  // 클라이언트 버전
  const sequence = packet.sequence;    // 시퀀스 번호

  // clientVersion 검증
  if (clientVersion !== config.client.version) {
    throw new CustomError(

      ErrorCodes.CLIENT_VERSION_MISMATCH,
      '클라이언트 버전이 일치하지 않습니다.',  // 버전 불일치 처리
    );
  }

  // 핸들러 ID에 따라 적절한 payload 구조를 디코딩
  const protoTypeName = getProtoTypeNameByHandlerId(handlerId);
  if (!protoTypeName) {
    throw new CustomError(ErrorCodes.UNKNOWN_HANDLER_ID, `알 수 없는 핸들러 ID: ${handlerId}`);  // 알 수 없는 핸들러 ID 처리
  }

  const [namespace, typeName] = protoTypeName.split('.');  // 네임스페이스와 타입 이름 분리
  const PayloadType = protoMessages[namespace][typeName];  // Payload 타입 가져오기
  let payload;
  try {
    payload = PayloadType.decode(packet.payload);  // payload 디코딩
  } catch (error) {
    throw new CustomError(ErrorCodes.PACKET_STRUCTURE_MISMATCH, '패킷 구조가 일치하지 않습니다.');  // 구조 불일치 처리
  }

  // 필드가 비어 있거나, 필수 필드가 누락된 경우 처리
  const expectedFields = Object.keys(PayloadType.fields);  // 예상 필드 목록
  const actualFields = Object.keys(payload);  // 실제 필드 목록
  const missingFields = expectedFields.filter((field) => !actualFields.includes(field));  // 누락된 필드 확인
  if (missingFields.length > 0) {
    throw new CustomError(
      ErrorCodes.MISSING_FIELDS,
      `필수 필드가 누락되었습니다: ${missingFields.join(', ')}`,  // 누락된 필드 처리
    );
  }

  return { handlerId, userId, payload, sequence };  // 결과 반환
};

 

 

이후

 

더보기
// 버퍼를 16진수 문자열로 변환하는 함수
function bufferToHex(buffer) {
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(' ');
}

// 패킷을 파싱하는 함수
export const packetParser = (data) => {
  try {
    // 수신한 원시 패킷 데이터 로그 출력
    console.log("Received raw packet data:", bufferToHex(data));

    // Proto 메시지 가져오기
    const protoMessages = getProtoMessages();
    const Packet = protoMessages.common.Packet;

    // 패킷 디코딩
    let packet;
    try {
      packet = Packet.decode(data);
      // 디코딩된 패킷 정보 로그 출력
      console.log("Decoded packet:", {
        handlerId: packet.handlerId,
        userId: packet.userId,
        version: packet.version,
        sequence: packet.sequence,
        payloadLength: packet.payload ? packet.payload.length : 0
      });
    } catch (error) {
      // 패킷 디코딩 오류 처리
      console.error("Packet decode error:", error);
      throw new CustomError(ErrorCodes.PACKET_DECODE_ERROR, '패킷 디코딩 중 오류가 발생했습니다.');
    }

    // 핸들러 ID, 사용자 ID, 클라이언트 버전, 시퀀스 정보 추출
    const handlerId = packet.handlerId;
    const userId = packet.userId;
    const clientVersion = packet.version;
    const sequence = packet.sequence;

    // 클라이언트 버전 검증
    if (clientVersion !== config.client.version) {
      console.error(`Version mismatch - Client: ${clientVersion}, Server: ${config.client.version}`);
      throw new CustomError(
        ErrorCodes.CLIENT_VERSION_MISMATCH,
        '클라이언트 버전이 일치하지 않습니다.'
      );
    }

    // 핸들러 ID에 따른 Payload 구조 확인
    const protoTypeName = getProtoTypeNameByHandlerId(handlerId);
    if (!protoTypeName) {
      console.error(`Unknown handler ID: ${handlerId}`);
      throw new CustomError(
        ErrorCodes.UNKNOWN_HANDLER_ID,
        `알 수 없는 핸들러 ID: ${handlerId}`
      );
    }

    // Payload 타입 가져오기
    const [namespace, typeName] = protoTypeName.split('.');
    const PayloadType = protoMessages[namespace][typeName];

    // Payload 디코딩
    let payload;
    try {
      payload = PayloadType.decode(packet.payload);
      // 디코딩된 Payload 정보 로그 출력
      console.log("Decoded payload:", payload);
    } catch (error) {
      // Payload 디코딩 오류 처리
      console.error("Payload decode error:", error);
      throw new CustomError(
        ErrorCodes.PACKET_STRUCTURE_MISMATCH,
        '패킷 구조가 일치하지 않습니다.'
      );
    }

    // 필수 필드 검증
    const expectedFields = Object.keys(PayloadType.fields);
    const actualFields = Object.keys(payload);
    const missingFields = expectedFields.filter(
      (field) => !actualFields.includes(field)
    );

    // 누락된 필드가 있는지 확인
    if (missingFields.length > 0) {
      console.error("Missing fields:", missingFields);
      throw new CustomError(
        ErrorCodes.MISSING_FIELDS,
        `필수 필드가 누락되었습니다: ${missingFields.join(', ')}`
      );
    }

    // 성공적으로 패킷 파싱 로그 출력
    console.log("Successfully parsed packet:", {
      handlerId,
      userId,
      payload,
      sequence
    });

    // 파싱된 결과 반환
    return { handlerId, userId, payload, sequence };

  } catch (error) {
    // 이미 CustomError인 경우 그대로 전파
    if (error instanceof CustomError) {
      throw error;
    }
    // 그 외의 경우 새로운 CustomError로 래핑
    console.error("Unexpected error in packet parser:", error);
    throw new CustomError(
      ErrorCodes.PACKET_DECODE_ERROR,
      '패킷 처리 중 예상치 못한 오류가 발생했습니다.'
    );
  }

 

 

 

user.session.js

더보기
/* 
이 코드는 사용자 세션을 관리하는 기능을 제공합니다.
사용자 추가, 제거, ID 또는 소켓을 통한 사용자 조회 및 다음 시퀀스 값을 가져오는 기능을 포함합니다.
각 사용자는 'User' 클래스의 인스턴스로 생성되며, 세션 관리를 위해 사용자 배열에 저장됩니다.
*/

import { userSessions } from './sessions.js';  // 사용자 세션 목록
import User from '../classes/models/user.class.js';  // User 클래스 임포트

// // 사용자 추가 함수
// export const addUser = (id, socket) => {
//   const user = new User(id, socket);  // 새 사용자 생성
//   userSessions.push(user);              // 사용자 목록에 추가
//   return user;                          // 생성된 사용자 반환
// };

export const addUser = (id, socket) => {
  console.log(`Adding user to sessions - ID: ${id}`);

  // 이미 존재하는 세션인지 확인
  const existingSession = userSessions.find(user => user.id === id);
  if (existingSession) {
    console.log(`Removing existing session for user ${id}`);
    removeUser(existingSession.socket);
  }

  const user = new User(id, socket);
  userSessions.push(user);

  console.log(`User added successfully. Total sessions: ${userSessions.length}`);
  console.log('Current sessions:', userSessions.map(u => u.id));

  return user;
};

// // 사용자 제거 함수
// export const removeUser = (socket) => {
//   const index = userSessions.findIndex((user) => user.socket === socket);  // 사용자 인덱스 찾기
//   if (index !== -1) {
//     return userSessions.splice(index, 1)[0];  // 사용자 제거 및 반환
//   }
// };

export const removeUser = (socket) => {
  const index = userSessions.findIndex(user => user.socket === socket);
  if (index !== -1) {
    const removedUser = userSessions.splice(index, 1)[0];
    console.log(`Removed user ${removedUser.id} from sessions`);
    return removedUser;
  }
  console.log('No user found to remove');
  return null;
};

// // ID로 사용자 조회 함수
// export const getUserById = (id) => {
//   return userSessions.find((user) => user.id === id);  // 사용자 조회
// };

export const getUserById = (id) => {
  console.log(`Looking for user with ID: ${id}`);
  console.log('Current sessions:', userSessions.map(u => u.id));

  const user = userSessions.find(user => user.id === id);

  console.log("저장된 유저 보기", userSessions);


  if (user) {
    console.log('User found');
    return user;
  } else {
    console.log('User not found');
    return null;
  }
};

// 소켓으로 사용자 조회 함수
export const getUserBySocket = (socket) => {
  return userSessions.find((user) => user.socket === socket);  // 사용자 조회
};



// 다음 시퀀스 값 가져오기 함수
export const getNextSequence = (id) => {
  const user = getUserById(id);  // 사용자 조회
  if (user) {
    return user.getNextSequence();  // 다음 시퀀스 값 반환
  }
  return null;  // 사용자가 없을 경우 null 반환
};

 

 

 

 

수정전

더보기
// 사용자 추가 함수
export const addUser = (id, socket) => {
  const user = new User(id, socket);  // 새 사용자 생성
  userSessions.push(user);              // 사용자 목록에 추가
  return user;                          // 생성된 사용자 반환
};

 

 

 

수정후

더보기
export const addUser = (id, socket) => {
  console.log(`Adding user to sessions - ID: ${id}`);

  // 이미 존재하는 세션인지 확인
  const existingSession = userSessions.find(user => user.id === id);
  if (existingSession) {
    console.log(`Removing existing session for user ${id}`);
    removeUser(existingSession.socket);
  }

  const user = new User(id, socket);
  userSessions.push(user);

  console.log(`User added successfully. Total sessions: ${userSessions.length}`);
  console.log('Current sessions:', userSessions.map(u => u.id));

  return user;
};

 

 

 

 

수정전

더보기
// 사용자 제거 함수
export const removeUser = (socket) => {
  const index = userSessions.findIndex((user) => user.socket === socket);  // 사용자 인덱스 찾기
  if (index !== -1) {
    return userSessions.splice(index, 1)[0];  // 사용자 제거 및 반환
  }
};

 

수정후

더보기
export const removeUser = (socket) => {
  const index = userSessions.findIndex(user => user.socket === socket);
  if (index !== -1) {
    const removedUser = userSessions.splice(index, 1)[0];
    console.log(`Removed user ${removedUser.id} from sessions`);
    return removedUser;
  }
  console.log('No user found to remove');
  return null;
};

 

 

 

수정전

더보기
// ID로 사용자 조회 함수
export const getUserById = (id) => {
  return userSessions.find((user) => user.id === id);  // 사용자 조회
};

 

 

 

수정후

더보기
export const getUserById = (id) => {
  console.log(`Looking for user with ID: ${id}`);
  console.log('Current sessions:', userSessions.map(u => u.id));

  const user = userSessions.find(user => user.id === id);

  console.log("저장된 유저 보기", userSessions);


  if (user) {
    console.log('User found');
    return user;
  } else {
    console.log('User not found');
    return null;
  }
};

 

game.session.js

 

더보기
/* 
이 코드는 게임 세션을 관리하는 기능을 제공합니다.
세션 추가, 제거, 특정 세션 조회 및 모든 세션 조회 기능을 포함합니다.
각 세션은 'Game' 클래스의 인스턴스로 생성되며, 세션 ID를 사용하여 관리합니다.
*/

import { gameSessions } from './sessions.js';  // 게임 세션 목록
import Game from '../classes/models/game.class.js';  // Game 클래스 임포트
import { v4 as uuidv4 } from 'uuid';

export const createGameSession = () => {
  const gameId = uuidv4();  // 게임 ID 생성
  const session = new Game(gameId);
  gameSessions.push(session);
  console.log(`Game session created with ID: ${gameId}`);
  return session;
};

// 게임 세션 추가 함수
export const addGameSession = (id) => {
  const session = new Game(id);  // 새 게임 세션 생성
  gameSessions.push(session);      // 세션 목록에 추가
  return session;                  // 생성된 세션 반환
};

// 게임 세션 제거 함수
export const removeGameSession = (id) => {
  const index = gameSessions.findIndex((session) => session.id === id);  // 세션 인덱스 찾기
  if (index !== -1) {
    return gameSessions.splice(index, 1)[0];  // 세션 제거 및 반환
  }
};

// 특정 게임 세션 조회 함수
export const getGameSession = (id) => {
  return gameSessions.find((session) => session.id === id);  // 세션 조회
};

// 모든 게임 세션 조회 함수
export const getAllGameSessions = () => {
  return gameSessions;  // 모든 세션 반환
};

inital.proto

 

더보기
/* 
이 코드는 최초 패킷 구조를 정의합니다.
초기화 과정에서 장치 ID를 포함하는 메시지를 사용하여 클라이언트와 서버 간의 연결을 설정합니다.
장치 ID는 각 클라이언트를 식별하는 데 사용됩니다.
*/

syntax = "proto3";  // Protobuf 버전 설정

package initial;  // 패키지 이름



// 최초 패킷 구조
message InitialPacket  {
    string deviceId = 1;  // 클라이언트에서 보내는 deviceId
    uint32 playerId = 2;  // 클라이언트에서 보내는 playerId
    float latency = 3;    // 클라이언트의 latency
}

 

 

game.proto

 

더보기
/* 
이 코드는 게임 생성, 참가, 위치 정보 업데이트를 위한 메시지 구조를 정의합니다.
각 메시지는 관련된 필드를 포함하여 게임의 상태와 사용자 위치 정보를 효과적으로 전달합니다.
게임 ID, 생성 및 참가 시각, 위치 좌표를 포함합니다.
*/

syntax = "proto3";  // Protobuf 버전 설정

package game;  // 패키지 이름

// 게임 생성 핸들러 payload
message CreateGamePayload {
  int64 timestamp = 1;       // 게임 생성 시각
}

// 게임 참가 핸들러 payload
message JoinGamePayload {
  string gameId = 1;         // 게임 ID (UUID)
  int64 timestamp = 2;       // 게임 참가 시각
}

// 위치 정보 업데이트 payload
message LocationUpdatePayload {
    required float x = 1;
    required float y = 2;
}

 

common.proto

더보기
/* 
이 코드는 공통 패킷 구조와 Ping 메시지를 정의합니다.
핸들러 ID, 사용자 ID, 클라이언트 버전, 호출 수 및 실제 데이터를 포함한 패킷 구조를 제공합니다.
Ping 메시지는 서버와 클라이언트 간의 연결 상태를 확인하기 위해 사용됩니다.
*/

syntax = "proto3";  // Protobuf 버전 설정

package common;  // 패키지 이름


// 공통 패킷 구조
message Packet {
  uint32 handlerId = 1;      // 핸들러 ID (4바이트)
  string userId = 2;         // 유저 ID (UUID, 16바이트)
  string clientVersion = 3;  // 클라이언트 버전 (문자열)
  uint32 sequence = 4;       // 유저의 호출 수 (42억)
  bytes payload = 5;         // 실제 데이터
}

// Ping 메시지
message Ping {
  int64 timestamp = 1;       // Ping 타임스탬프
}

 

 

inital.handler.js

더보기
/* 
이 코드는 초기 사용자 핸들러 함수를 정의합니다.
장치 ID를 통해 사용자를 조회하고, 존재하지 않으면 새로 생성하며,
사용자 정보를 응답으로 클라이언트에 전송합니다. 오류 발생 시 적절히 처리합니다.
*/

import { addUser, getUserById, removeUser } from '../../session/user.session.js';  // 사용자 세션에 사용자 추가
import { userSessions } from '../../session/sessions.js';
import { HANDLER_IDS, RESPONSE_SUCCESS_CODE } from '../../constants/handlerIds.js';  // 핸들러 ID 및 응답 코드 상수
import { createResponse } from '../../utils/response/createResponse.js';  // 응답 생성 유틸리티
import { handleError } from '../../utils/error/errorHandler.js';  // 오류 처리 유틸리티
import { createUser, findUserByDeviceID, updateUserLogin } from '../../db/user/user.db.js';  // 데이터베이스 사용자 관련 함수
import { getAllGameSessions } from '../../session/game.session.js';

// 초기 사용자 핸들러 함수
// const initialHandler = async ({ socket, userId, payload }) => {
//   try {
//     const { deviceId, latency, playerId } = payload;  // 요청으로부터 장치 ID 추출

//     let user = await findUserByDeviceID(deviceId);  // 장치 ID로 사용자 조회

//     if (!user) {
//       user = await createUser(deviceId);  // 사용자 없으면 새로 생성
//     } else {
//       await updateUserLogin(user.id);  // 사용자 존재 시 로그인 업데이트
//     }

//     addUser(user.id, socket);  // 사용자 세션에 소켓 추가

//     // 유저 정보 응답 생성
//     const initialResponse = createResponse(
//       HANDLER_IDS.INITIAL,
//       RESPONSE_SUCCESS_CODE,
//       { userId: user.id },  // 사용자 ID 포함
//       deviceId,
//     );

//     // 소켓을 통해 클라이언트에게 응답 메시지 전송
//     socket.write(initialResponse);
//   } catch (error) {
//     handleError(socket, error);  // 오류 처리
//   }
// };


// const initialHandler = async ({ socket, userId, payload }) => {
//   try {
//     const { deviceId, playerId, latency } = payload;

//     let user = await findUserByDeviceID(deviceId);
//     if (!user) {
//       user = await createUser(deviceId);
//     } else {
//       await updateUserLogin(user.id);
//     }

//     addUser(user.id, socket);

//     // 응답에 playerId 포함
//     const initialResponse = createResponse(
//       HANDLER_IDS.INITIAL,
//       RESPONSE_SUCCESS_CODE,
//       {
//         userId: user.id,
//         playerId: playerId,  // playerId 추가
//         latency: latency     // latency 추가
//       },
//       deviceId
//     );

//     socket.write(initialResponse);
//   } catch (error) {
//     handleError(socket, error);
//   }
// };


// const initialHandler = async ({ socket, userId, payload }) => {
//   try {
//     const { deviceId, playerId, latency } = payload;
//     console.log(`Initial connection request - DeviceId: ${deviceId}, PlayerId: ${playerId}`);

//     let user = await findUserByDeviceID(deviceId);

//     console.log('Initial handler - Existing user:', user);

//     if (!user) {
//       user = await createUser(deviceId);
//       console.log('Initial handler - New user created:', user);
//     } else {
//       await updateUserLogin(user.id);
//       console.log('Initial handler - User login updated');
//     }

//     // 이미 세션에 있는 사용자인지 확인
//     const existingUser = getUserById(user.id);
//     if (existingUser) {
//       console.log('Initial handler - Removing existing user session');
//       removeUser(existingUser.socket);
//     }

//     // 새로운 세션 추가
//     const newUser = addUser(user.id, socket);
//     console.log('Initial handler - New user session added:', {
//       id: newUser.id,
//       sessionCount: userSessions.length
//     });

//     const initialResponse = createResponse(
//       HANDLER_IDS.INITIAL,
//       RESPONSE_SUCCESS_CODE,
//       {
//         userId: user.id,
//         playerId: playerId,
//         latency: latency
//       },
//       deviceId
//     );

//     socket.write(initialResponse);

//     // 디버깅을 위한 현재 세션 상태 출력
//     console.log('Current user sessions after initialization:',
//       userSessions.map(u => ({ id: u.id, socketConnected: !!u.socket }))
//     );
//   } catch (error) {
//     console.error('Error in initialHandler:', error);
//     handleError(socket, error);
//   }
// };


// const initialHandler = async ({ socket, userId, payload }) => {
//   try {
//     const { deviceId, playerId, latency } = payload;
//     console.log(`Initial connection request - DeviceId: ${deviceId}, PlayerId: ${playerId}`);

//     let user = await findUserByDeviceID(deviceId);

//     console.log('Initial handler - Existing user:', user);

//     if (!user) {
//       user = await createUser(deviceId);
//       console.log('Initial handler - New user created:', user);
//     } else {
//       await updateUserLogin(user.id);
//       console.log('Initial handler - User login updated');
//     }

//     // deviceId를 userId로 사용
//     const existingUser = getUserById(deviceId);
//     if (existingUser) {
//       console.log('Initial handler - Removing existing user session');
//       removeUser(existingUser.socket);
//     }

//     // 새로운 세션 추가할 때도 deviceId를 userId로 사용
//     const newUser = addUser(deviceId, socket);
//     console.log('Initial handler - User session added:', {
//       id: newUser.id,
//       sessionsCount: userSessions.length
//     });

//     const initialResponse = createResponse(
//       HANDLER_IDS.INITIAL,
//       RESPONSE_SUCCESS_CODE,
//       {
//         userId: deviceId,  // deviceId를 userId로 사용
//         playerId: playerId,
//         latency: latency
//       },
//       deviceId
//     );

//     socket.write(initialResponse);

//   } catch (error) {
//     console.error('Error in initialHandler:', error);
//     handleError(socket, error);
//   }
// };



const initialHandler = async ({ socket, userId, payload }) => {
  try {
    const { deviceId, playerId, latency } = payload;
    console.log(`Initial connection request - DeviceId: ${deviceId}, PlayerId: ${playerId}`);

    let user = await findUserByDeviceID(deviceId);

    console.log('Initial handler - Existing user:', user);

    if (!user) {
      user = await createUser(deviceId);
      console.log('Initial handler - New user created:', user);
    } else {
      await updateUserLogin(user.id);
      console.log('Initial handler - User login updated');
    }

    // deviceId를 userId로 사용
    const existingUser = getUserById(deviceId);
    if (existingUser) {
      console.log('Initial handler - Removing existing user session');
      removeUser(existingUser.socket);
    }

    const gameSessions = getAllGameSessions();
    if (gameSessions.length === 0) {
      throw new Error('No active game sessions available');
    }

    // 첫 번째 게임 세션 사용 (또는 다른 로직으로 게임 세션 선택)
    const gameSession = gameSessions[0];

    // 새로운 세션 추가할 때도 deviceId를 userId로 사용
    const newUser = addUser(deviceId, socket);
    console.log('Initial handler - User session added:', {
      id: newUser.id,
      sessionsCount: userSessions.length
    });
    gameSession.addUser(newUser);

    const initialResponse = createResponse(
      HANDLER_IDS.INITIAL,
      RESPONSE_SUCCESS_CODE,
      {
        userId: deviceId,  // deviceId를 userId로 사용
        playerId: playerId,
        latency: latency,
        gameId: gameSession.id  // 게임 세션 ID 포함
      },
      deviceId
    );

    socket.write(initialResponse);

  } catch (error) {
    console.error('Error in initialHandler:', error);
    handleError(socket, error);
  }
};



export default initialHandler;  // 핸들러 함수 내보내기

 

 

updateLocation.handler.js

더보기
/* 
이 코드는 사용자의 위치를 업데이트하는 핸들러 함수를 정의합니다.
주어진 게임 세션에 사용자가 존재하는지 확인하고, 위치를 업데이트한 후
모든 사용자의 위치 정보를 소켓으로 전송합니다. 오류 발생 시 적절히 처리합니다.
*/

//import { getGameSession } from '../../session/game.session.js';  // 게임 세션 관리 함수
import { userSessions } from '../../session/sessions.js';
import { getUserById } from '../../session/user.session.js';
import { createLocationPacket } from '../../utils/notification/game.notification.js';
import { handleError } from '../../utils/error/errorHandler.js';  // 오류 처리 유틸리티
import CustomError from '../../utils/error/customError.js';  // 사용자 정의 오류 클래스
import { ErrorCodes } from '../../utils/error/errorCodes.js';  // 오류 코드 상수

// 위치 업데이트 핸들러 함수
// const updateLocationHandler = ({ socket, userId, payload }) => {
//   // try {
//   //   const { gameId, x, y } = payload;  // 요청으로부터 게임 ID와 좌표 추출
//   //   const gameSession = getGameSession(gameId);  // 게임 세션 가져오기

//   //   if (!gameSession) {
//   //     throw new CustomError(ErrorCodes.GAME_NOT_FOUND, '게임 세션을 찾을 수 없습니다.');  // 게임 세션 없음 오류 처리
//   //   }

//   //   const user = gameSession.getUser(userId);  // 게임 세션에서 사용자 정보 가져오기
//   //   if (!user) {
//   //     throw new CustomError(ErrorCodes.USER_NOT_FOUND, '유저를 찾을 수 없습니다.');  // 사용자 없음 오류 처리
//   //   }
//   //   user.updatePosition(x, y);  // 사용자 위치 업데이트
//   //   const packet = gameSession.getAllLocation(userId);  // 모든 사용자 위치 정보 가져오기

//   //   socket.write(packet);  // 소켓을 통해 위치 정보 전송
//   // } catch (error) {
//   //   handleError(socket, error);  // 오류 처리
//   // }

//   try {
//     const { x, y } = payload;
//     const user = getUserById(userId);
//     if (!user) {
//       throw new CustomError(ErrorCodes.USER_NOT_FOUND, '유저를 찾을 수 없습니다.');
//     }
//     user.updatePosition(x, y);
//     // 모든 클라이언트에게 업데이트된 위치 정보 브로드캐스트
//     broadcastLocationUpdate(socket, user);
//   } catch (error) {
//     handleError(socket, error);
//   }
// };

// const updateLocationHandler = ({ socket, userId, payload }) => {
//   try {
//     const { x, y } = payload;


//     // 디버그용 로그 추가
//     console.log(`Updating location for user ${userId}: x=${x}, y=${y}`);
//     console.log('Current userSessions:', userSessions);

//     const user = getUserById(userId);

//     if (!user) {
//       throw new CustomError(ErrorCodes.USER_NOT_FOUND, '유저를 찾을 수 없습니다.');
//     }

//     user.updatePosition(x, y);

//     // 위치 정보를 다른 클라이언트들에게 브로드캐스트
//     const locationPacket = createLocationPacket([{
//       id: userId,
//       x: x,
//       y: y
//     }]);

//     // 모든 연결된 클라이언트에게 전송
//     socket.write(locationPacket);

//   } catch (error) {
//     handleError(socket, error);
//   }
// };

const updateLocationHandler = ({ socket, userId, payload }) => {
  try {
    const { x, y } = payload;

    console.log(`Processing location update for user ${userId}:`, { x, y });
    console.log('Current active sessions:', userSessions.map(u => u.id));

    const user = getUserById(userId);

    if (!user) {
      console.log(`User ${userId} not found in active sessions`);
      throw new CustomError(ErrorCodes.USER_NOT_FOUND, '유저를 찾을 수 없습니다.');
    }

    user.updatePosition(x, y);

    // 위치 업데이트 패킷 생성
    const locationPacket = createLocationPacket([{
      id: userId,
      x: x,
      y: y
    }]);

    // 다른 모든 사용자에게 위치 업데이트 브로드캐스트
    userSessions.forEach(otherUser => {
      if (otherUser.id !== userId && otherUser.socket && otherUser.socket.writable) {
        otherUser.sendUpdate(locationPacket);
      }
    });

  } catch (error) {
    console.error('Error in updateLocationHandler:', error);
    handleError(socket, error);
  }
};



const broadcastLocationUpdate = (socket, user) => {
  const locationPacket = createLocationPacket([{
    id: user.id,
    playerId: user.playerId,
    x: user.x,
    y: user.y
  }]);
  socket.write(locationPacket);
};

export default updateLocationHandler;  // 핸들러 함수 내보내기

 

 

 

onData.js

더보기
/* 
이 코드는 소켓에서 수신된 데이터를 처리하는 함수를 정의합니다.
버퍼에 데이터를 추가하고, 패킷 헤더를 파싱하여 다양한 패킷 타입에 따라 적절한 핸들러를 호출합니다.
오류 발생 시 오류를 처리하는 로직도 포함되어 있습니다.
*/

import { config } from '../config/config.js';  // 설정 파일 가져오기
import { PACKET_TYPE } from '../constants/header.js';  // 패킷 타입 상수
import { packetParser } from '../utils/parser/packetParser.js';  // 패킷 파서 유틸리티
import { getHandlerById } from '../handlers/index.js';  // 핸들러 가져오기
import { getUserById, getUserBySocket } from '../session/user.session.js';  // 사용자 세션 관리
import { handleError } from '../utils/error/errorHandler.js';  // 오류 처리 유틸리티
import CustomError from '../utils/error/customError.js';  // 사용자 정의 오류
import { ErrorCodes } from '../utils/error/errorCodes.js';  // 오류 코드 상수
import { getProtoMessages } from '../init/loadProtos.js';  // 프로토 메시지 로드

// 소켓에서 수신된 데이터를 처리하는 함수
export const onData = (socket) => async (data) => {
  // 기존 버퍼에 새로 수신된 데이터를 추가
  socket.buffer = Buffer.concat([socket.buffer, data]);

  // 패킷의 총 헤더 길이 (패킷 길이 정보 + 타입 정보)
  const totalHeaderLength = config.packet.totalLength + config.packet.typeLength;

  // 버퍼에 최소한 전체 헤더가 있을 때만 패킷을 처리
  while (socket.buffer.length >= totalHeaderLength) {
    // 1. 패킷 길이 정보 수신 (4바이트)
    const length = socket.buffer.readUInt32BE(0);

    // 2. 패킷 타입 정보 수신 (1바이트)
    const packetType = socket.buffer.readUInt8(config.packet.totalLength);
    // 3. 패킷 전체 길이 확인 후 데이터 수신
    if (socket.buffer.length >= length) {
      // 패킷 데이터를 자르고 버퍼에서 제거
      const packet = socket.buffer.slice(totalHeaderLength, length);
      socket.buffer = socket.buffer.slice(length);

      try {
        switch (packetType) {
          case PACKET_TYPE.PING:  // 핑 패킷 처리
            {
              const protoMessages = getProtoMessages();
              const Ping = protoMessages.common.Ping;
              const pingMessage = Ping.decode(packet);
              const user = getUserBySocket(socket);
              if (!user) {
                throw new CustomError(ErrorCodes.USER_NOT_FOUND, '유저를 찾을 수 없습니다.');
              }
              user.handlePong(pingMessage);  // 핑 응답 처리
            }
            break;
          case PACKET_TYPE.NORMAL:  // 일반 패킷 처리
            const { handlerId, sequence, payload, userId } = packetParser(packet);

            const user = getUserById(userId);
            // 유저가 접속해 있는 상황에서 시퀀스 검증

            if (handlerId !== 0 && user) {
              // 시퀀스 업데이트
              user.sequence = sequence;
            }

            if (user && user.sequence !== sequence) {
              throw new CustomError(ErrorCodes.INVALID_SEQUENCE, '잘못된 호출 값입니다. ');
            }

            const handler = getHandlerById(handlerId);  // 핸들러 가져오기
            await handler({  // 핸들러 호출
              socket,
              userId,
              payload,
            });
            break;
        }
      } catch (error) {
        handleError(socket, error);  // 오류 발생 시 처리
      }
    } else {
      // 아직 전체 패킷이 도착하지 않음
      break;
    }
  }
};

 

 

handlerIds.js

더보기
/* 
이 모듈은 성공 응답 코드를 정의하고 
게임 관련 핸들러 ID를 포함하는 객체를 제공합니다.
핸들러 ID는 다양한 게임 작업을 식별하는 데 사용됩니다.
*/

export const RESPONSE_SUCCESS_CODE = 0;  // 성공 응답 코드 정의

// 핸들러 ID를 정의하는 객체
export const HANDLER_IDS = {
  INITIAL: 0,          // 초기화 핸들러 ID
  UPDATE_LOCATION: 2,  // 위치 업데이트 핸들러 ID
  CREATE_GAME: 4,     // 게임 생성 핸들러 ID
  JOIN_GAME: 5,       // 게임 참여 핸들러 ID
};

 

user.class.js

 

더보기
/* 
User 클래스는 각 사용자에 대한 정보를 관리하며, 
위치 업데이트, 핑/퐁 응답 처리 및 위치 추정 기능을 제공합니다.
사용자의 ID, 소켓, 위치 및 지연 시간을 추적합니다.
*/

import { createPingPacket } from '../../utils/notification/game.notification.js';  // 핑 패킷 생성 함수 가져오기


class User {
  constructor(id, socket) {
    this.id = id;  // 사용자 ID
    this.socket = socket;  // 사용자 소켓
    this.x = 0;  // x 좌표 초기화
    this.y = 0;  // y 좌표 초기화
    this.sequence = 0;  // 패킷 순서 초기화
    this.lastUpdateTime = Date.now();  // 마지막 업데이트 시간 초기화
  }

  // sequence 속성의 getter와 setter 추가
  set sequence(value) {
    this._sequence = value;
  }

  get sequence() {
    return this._sequence;
  }


  // 사용자 위치 업데이트 메서드
  updatePosition(x, y) {
    this.x = x;  // x 좌표 업데이트
    this.y = y;  // y 좌표 업데이트
    this.lastUpdateTime = Date.now();  // 마지막 업데이트 시간 갱신
    console.log(`Updated position for user ${this.id}: x=${x}, y=${y}`);
  }

  // 다음 패킷 순서 가져오기
  getNextSequence() {
    return ++this.sequence;  // 순서 증가 후 반환
  }

  sendUpdate(packet) {
    if (this.socket && this.socket.writable) {
      this.socket.write(packet);
    }
  }

  // 핑 메서드
  ping() {
    const now = Date.now();  // 현재 시간 가져오기

    // console.log(`${this.id}: ping`);
    this.socket.write(createPingPacket(now));  // 핑 패킷 전송
  }

  // 퐁 응답 처리 메서드
  handlePong(data) {
    const now = Date.now();  // 현재 시간 가져오기
    this.latency = (now - data.timestamp) / 2;  // 지연 시간 계산
    // console.log(`Received pong from user ${this.id} at ${now} with latency ${this.latency}ms`);
  }

  // 추측항법을 사용하여 위치를 추정하는 메서드
  calculatePosition(latency) {
    const timeDiff = latency / 1000; // 레이턴시를 초 단위로 계산
    const speed = 1; // 속도 고정
    const distance = speed * timeDiff;  // 이동 거리 계산

    // x, y 축에서 이동한 거리 계산
    return {
      x: this.x + distance,  // x 좌표 업데이트
      y: this.y,  // y 좌표 유지
    };
  }
}

export default User;  // User 클래스를 기본으로 내보내기

 

 

 

 

더보기
/* 
이 클래스는 게임 세션을 관리하며, 최대 플레이어 수를 설정하고 
사용자를 추가하거나 제거하며 게임 상태를 추적합니다.
게임이 시작되면 사용자 위치를 계산하고 알림을 전송합니다.
*/

import IntervalManager from '../managers/interval.manager.js';  // IntervalManager 클래스 가져오기
import {
  createLocationPacket,
  gameStartNotification,
} from '../../utils/notification/game.notification.js';  // 게임 관련 알림 패킷 가져오기


const MAX_PLAYERS = 2;  // 최대 플레이어 수 설정

class Game {
  constructor(id) {
    this.id = id;  // 게임 ID
    this.users = [];  // 사용자 목록 초기화
    this.intervalManager = new IntervalManager();  // 인터벌 관리 객체 생성
    this.state = 'waiting'; // 게임 상태 ('waiting', 'inProgress')
  }

  // 사용자 추가 메서드
  addUser(user) {
    if (this.users.length >= MAX_PLAYERS) {
      throw new Error('Game session is full');  // 최대 플레이어 수 초과 시 오류 발생
    }
    this.users.push(user);  // 사용자 목록에 추가

    // 사용자 핑을 1초마다 체크
    this.intervalManager.addPlayer(user.id, user.ping.bind(user), 1000);
    // 최대 플레이어 수에 도달하면 게임 시작 타이머 설정
    if (this.users.length === MAX_PLAYERS) {
      setTimeout(() => {
        this.startGame();
      }, 3000);
    }
  }

  // 사용자 ID로 사용자 정보 가져오기
  getUser(userId) {
    return this.users.find((user) => user.id === userId);
  }

  // 사용자 제거 메서드
  removeUser(userId) {
    this.users = this.users.filter((user) => user.id !== userId);  // 사용자 목록에서 제거
    this.intervalManager.removePlayer(userId);  // 인터벌 관리에서 제거

    // 플레이어 수가 줄어들면 상태를 'waiting'으로 변경
    if (this.users.length < MAX_PLAYERS) {
      this.state = 'waiting';
    }
  }

  // 최대 지연 시간 계산 메서드
  getMaxLatency() {
    let maxLatency = 0;  // 최대 지연 시간 초기화
    this.users.forEach((user) => {
      console.log(`${user.id}: ${user.latency}`);  // 각 사용자의 지연 시간 출력
      maxLatency = Math.max(maxLatency, user.latency);  // 최대 지연 시간 업데이트
    });
    return maxLatency;  // 최대 지연 시간 반환
  }

  // 게임 시작 메서드
  startGame() {
    this.state = 'inProgress';  // 상태를 'inProgress'로 변경
    const startPacket = gameStartNotification(this.id, Date.now());  // 게임 시작 알림 패킷 생성
    console.log(`max latency: ${this.getMaxLatency()}`);  // 최대 지연 시간 출력

    // 모든 사용자에게 게임 시작 패킷 전송
    this.users.forEach((user) => {
      user.socket.write(startPacket);
    });
  }

  // 모든 사용자 위치 계산 메서드
  getAllLocation(excludeUserId) {
    const maxLatency = this.getMaxLatency();  // 최대 지연 시간 가져오기

    // 각 사용자의 위치 계산
    const locationData = this.users
      .filter((user) => user.id !== excludeUserId) // 자신을 제외한 모든 유저를 받아야함
      .map((user) => {

        const { x, y } = user.calculatePosition(maxLatency);  // 위치 계산
        return { id: user.id, x, y };  // 사용자 ID와 위치 반환
      });
    return createLocationPacket(locationData);  // 위치 데이터 패킷 생성
  }
}

export default Game;  // Game 클래스를 기본으로 내보내기

 

 

server.js

더보기
/* 
이 코드는 TCP 서버를 초기화하고 실행하는 기능을 제공합니다.
서버는 클라이언트의 연결을 처리하기 위해 `onConnection` 이벤트 핸들러를 사용하며,
서버가 성공적으로 시작되면 실행 중인 주소를 로그로 출력합니다.
초기화 과정에서 오류가 발생할 경우, 오류 메시지를 출력하고 프로세스를 종료합니다.
*/

import net from 'net';  // net 모듈 임포트
import initServer from './init/index.js';  // 서버 초기화 함수 임포트
import { config } from './config/config.js';  // 설정 파일 임포트
import { onConnection } from './events/onConnection.js';  // 연결 이벤트 핸들러 임포트
import { createGameSession } from './session/game.session.js';

const server = net.createServer(onConnection);  // 서버 생성

// 서버 초기화 및 실행
const startServer = async () => {
  try {
    await initServer(); // 서버 초기화

    // 서버 시작 시 게임 인스턴스 생성
    const gameSession = createGameSession();
    console.log('Initial game session created:', gameSession.id);

    server.listen(config.server.port, config.server.host, () => {
      console.log(`서버가 ${config.server.host}:${config.server.port}에서 실행 중입니다.`);
      console.log(server.address()); // 서버 주소 출력
    });
  } catch (err) {
    console.error(err); // 오류 로그 출력
    process.exit(1); // 오류 발생 시 프로세스 종료
  }
};

// 서버 초기화 및 실행
startServer();

 

 

 

 

'TIL' 카테고리의 다른 글

TIL_2025-01-10  (0) 2025.01.10
TIL_2025-01-09  (0) 2025.01.10
TIL_2025-01-07  (0) 2025.01.07
TIL_2025-01-06  (0) 2025.01.06
TIL_2025-01-03  (0) 2025.01.03

댓글

💲 추천 글