일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 백준 28303번
- html5
- 백준 32028번
- map
- 지금 자면 꿈을 꾸지만
- Next
- 게임 서버 아키텍처
- 그리디
- Express.js
- pm2
- ucpc 2024 예선 e번
- router
- ucpc 2023 예선 i번
- ERD
- 더 흔한 색칠 타일 문제
- JavaScript
- HTTP
- Github
- insomnia
- 자바스크립트
- ucpc 2023 예선 d번
- PROJECT
- ccw 알고리즘
- MongoDB
- branch
- localstorage
- MySQL
- Prisma
- string
- 백준 32029번
Archives
- Today
- Total
dh_0e
[Node.js] 강의 내용 정리(4) (서버 로직 개발) + 개인 과제 본문
1. 데이터 테이블 로드
- 파일 시스템을 사용하여 서버에서 필요한 데이터 테이블을 메모리에 로드할 수 있음
- 파일 시스템(file system) - Node.js의 fs 모듈은 파일 시스템에 접근하고, 파일을 읽고 쓰는 기능을 제공함
- 동기적 및 비동기적 방식 모두로 파일 I/O 작업을 수행하며 CRUD 작업을 할 수 있음
- 다양한 형태의 파일 기반 작업을 가능하게 해줌
- DB(DataBase), CDN(Cloud Delivery Network), File 등으로 테이블을 관리하며 file이 가장 간편함
2. 유저 접속 관리
- 유저가 서버에 웹소켓 프로토콜을 통해 접속을 하면 소켓 아이디가 발급됨
- 현재 유저가 서버에 접속해있다는 상태를 저장하기 위해서 웹소켓에서 데이터를 주고받기 위해 존재함
- 소켓 id는 임시적이어서 접속이 끊기면 사라지므로 이를 통해 유저를 특정할 수는 없으며 uuid로 유저를 특정해야함
3. 커넥션 핸들러
- 게임 기획 단계에서 설정한 내용을 토대로 커넥션에 관한 핸들러를 기획, 완성함
// helper.js
export const handleDisconnect = (socket, uuid) => {
removeUser(uuid);
clearStage(uuid);
clearItem(uuid);
console.log('User disconnected: ' + uuid);
console.log('Current users: ', getUser());
};
export const handleConnection = (socket, uuid) => {
console.log(`New user connected: ${uuid} with socket ID ${socket.id}`);
console.log('Current users: ', getUser());
createStage(uuid);
createItem(uuid);
// 본인의 소켓에 보내는 것
socket.emit('connection', { uuid });
};
// register.handler.js
import { addUser } from '../models/user.model.js';
import { v4 as uuidv4 } from 'uuid';
import { handleConnection, handleDisconnect, handleEvent } from './helper.js';
const registerHandler = (io) => {
// 서버에 접속한 모든 유저를 대상한 이벤트
io.on('connection', (socket) => {
const userUUID = uuidv4();
addUser({ uuid: userUUID, socketId: socket.id });
handleConnection(socket, userUUID);
socket.on('event', (data) => handleEvent(io, socket, data));
// 하나의 유저를 대상으로 한 이벤트
socket.on('disconnect', (socket) => {
handleDisconnect(socket, userUUID);
});
});
};
export default registerHandler;
- 서버에서 유저를 위한 데이터를 생성해 저장할 수 있어야 함
4. 이벤트 핸들러
- 게임 기획 단계에서 구상한 이벤트들을 핸들러로 구성
game.handler.js
- 게임 시작, 게임 종료 시 초기화 및 최고점수 갱신이 있으며 이전에 데이터 검증이 포함되어 있음
import { getGameAssets, highScoreRenewal } from '../init/assets.js';
import { clearItem, getItem } from '../models/item.model.js';
import { clearStage, getStage, setStage } from '../models/stage.model.js';
export const gameStart = (uuid, payload) => {
const { stages } = getGameAssets();
clearStage(uuid);
clearItem(uuid);
setStage(uuid, stages.data[0].id, payload.timestamp);
console.log('Stage: ', getStage(uuid));
return { status: 'success' };
};
export const gameEnd = (uuid, payload) => {
const { items, stages, highScore } = getGameAssets();
console.log(uuid, payload);
const { timestamp: gameEndTime, score } = payload;
const myStages = getStage(uuid);
if (!myStages.length) {
return { status: 'fail', message: 'No stage found for user' };
}
let totalScore = 0;
// 스테이지 시간별 점수 추가
myStages.forEach((stage, index) => {
console.log('sdf', stage, index);
let stageEndTime;
if (index === myStages.length - 1) {
stageEndTime = gameEndTime;
} else {
stageEndTime = myStages[index + 1].timestamp;
}
const scorePerSecond = stages.data.find(function (val) {
return val.id === stage.id;
}).scorePerSecond;
const statgeDuration = ((stageEndTime - stage.timestamp) / 1000) * scorePerSecond;
totalScore += statgeDuration; // *(stage.id%10+1); 1초당 1점 stage 모두 동일
});
// 아이템 점수 추가
const ateItems = getItem(uuid);
for (let item of ateItems) {
totalScore += items.data.find(function (val) {
return val.id === item.itemId;
}).score;
}
// 점수와 타임스탬프 검증
// 오차범위 5
console.log(score, totalScore);
if (Math.abs(score - totalScore) > 5) {
return { status: 'fail', message: 'Score verification failed' };
}
//DB에 저장한다고 가정한다면 여기서 저장
//setResult(userId, score, timestamp)
if (highScore.highScore < totalScore) {
highScoreRenewal(totalScore);
return {
status: 'success',
message: `Game ended`,
broadcast: `${uuid} has renew the highest score`,
highScore: totalScore,
};
}
return { status: 'success', message: `Game ended` };
};
handlerMapping.js
- moveStageHandler - 스테이지 이동
- gameStart - 게임 시작
- gameEnd - 게임 종료
- eatItem - 아이템 획득
import { moveStageHandler } from './stage.handler.js';
import { gameStart, gameEnd } from './game.handler.js';
import { eatItem } from './item.handler.js';
const handlerMappings = {
11: moveStageHandler,
2: gameStart,
3: gameEnd,
4: eatItem,
};
export default handlerMappings;
helper.js
- broadcast는 광역적으로 모든 사용자에게 이벤트를 보낸다.
// helper.js
export const handleEvent = (io, socket, data) => {
if (!CLIENT_VERSION.includes(data.clientVersion)) {
socket.emit('response', { status: 'fail', message: 'Client version mismatched' });
return;
}
const handler = handlerMappings[data.handlerId];
if (!handler) {
socket.emit('response', { status: 'fail', message: 'Handler not found' });
return;
}
const response = handler(data.userId, data.payload);
if (response.broadcast) {
io.emit('response', response);
return;
}
socket.emit('response', response);
};
item.handler.js
- 아이템을 획득시 획득 정보를 uuid를 key값으로 저장하고 아이템이 현재 스테이지에 나올 수 있는지 검증함
- + 아이템 획득 패킷을 보내서 어뷰징으로 점수를 얻는 행위 또한 검증
import { getGameAssets } from '../init/assets.js';
import { addItem, getItem } from '../models/item.model.js';
import { getStage } from '../models/stage.model.js';
export const eatItem = async (userId, payload) => {
const { itemUnlocks } = getGameAssets();
const serverTime = Date.now();
// 검증
// 아이템Id가 현재 스테이지에 나올 수 있는지
const stages = await getStage(userId);
console.log(stages);
const currentStage = stages[stages.length - 1].id;
for (let i = 0; i < itemUnlocks.data.length; i++) {
if (itemUnlocks.data[i].stage_id === currentStage) {
if (!itemUnlocks.data[i].item_id.includes(payload.itemId)) {
return { status: 'false', message: "This item can't be in your stage" };
}
break;
}
}
// 아이템 획득 패킷만 전송하는 어뷰징 행위인지 (아이템 생성 간격을 잘 지켰는지)
const items = getItem(userId);
if (items.length > 0) {
const elapsedTime = (serverTime - items[items.length - 1].timestamp) / 1000;
// 5~10초 사이로 랜덤하게 생성되는데 5보다 작으면 false, 오차범위 10%
if (elapsedTime < 4.5) {
return { status: 'false', message: 'Item creation interval Error' };
}
}
// 검증 완료시 데이터 저장
addItem(userId, payload.itemId, serverTime);
console.log(getItem(userId));
return { status: 'success' };
};
stage.handler.js
- 점수에 따라 스테이지 이동을 해주는 함수
- 클라이언트와 서버 비교, 점수 비교를 통해 검증을 함
import { getGameAssets } from '../init/assets.js';
import { getStage, setStage } from '../models/stage.model.js';
export const moveStageHandler = (userId, payload) => {
let currentStages = getStage(userId);
if (!currentStages.length) {
return { status: 'fail', message: 'No stages found for user' };
}
// 오름차순으로 정렬하여 현재 스테이지 확인
currentStages.sort((a, b) => a.id - b.id);
const currentStage = currentStages[currentStages.length - 1];
// 클라이언트 vs 서버 비교
if (currentStage.id !== payload.currentStage) {
return { status: 'fail', message: 'Current Stage mismatched', currentStage: currentStage.id };
}
// 다음 stage로 갈 수 있는지 score 검사
const serverTime = Date.now();
const elapsedTime = (serverTime - currentStage.timestamp) / 1000;
// console.log(serverTime, currentStage.timestamp, elapsedTime);
// 5 => 임의로 정한 오차범위 클라이언트 -> 서버까지 딜레이가 너무 길어 에러 처리
// if (elapsedTime < 95 || elapsedTime > 105) {
// return { status: 'fail', message: 'Invalid elapsed time' };
// }
// targetStage 검증 <- 게임에셋에 존재하는지
const { stages } = getGameAssets();
// some - 배열을 구성하는 것들 중에 조건문이 하나라도 맞으면 true 반환
if (!stages.data.some((stage) => stage.id === payload.targetStage)) {
return { status: 'fail', message: 'Target stage not found' };
}
setStage(userId, payload.targetStage, serverTime);
console.log(getStage(userId));
return { status: 'success' };
};
5. 변경된 패킷구조 정리
- 구현을 하며 이벤트나 로직에 의해 변경된 패킷구조를 다시 정리
+ TCP vs UDP
'내일배움캠프 > Node.js[심화]' 카테고리의 다른 글
[Node.js] 강의 내용 정리(3) (게임 기획의 순서) (0) | 2024.06.13 |
---|---|
[Node.js] 강의 내용 정리(2) (HTTP, TCP, 웹소켓) (0) | 2024.06.13 |
[Node.js] 강의 내용 정리(1) (게임 개발) (0) | 2024.06.10 |