오늘 발생했던 에러
"패킷 디코딩 중 오류가 발생했습니다."
"클라이언트 버전이 일치하지 않습니다." 이건 갑자기 생겼다가 없어졌다.
"알 수 없는 핸들러 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 |
댓글