-
[TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-4)TIL-sparta 2024. 7. 4. 18:01
> 과제 진행 간 완료한 사항 및 문제점과 해결 과정을 정리해보았다.학습 키워드: Node.js, net, TCP, socket, Buffer, Protobuf, MySQL, RDS
1. 완료한 과제 진행 사항1) (도전 요구사항) DB 연동하기 (완료):
DB 연동하기 part 2 by donkim1212 · Pull Request #11 · donkim1212/sparta-ch5-tcp-game-server
작업 내역 #6 의 'DB 연동' 4번 클라이언트 수정 파트 완료 서버 서버의 proto message 객체 생성에 사용될 class들 작성 gameNotification 패키지에 GameStart 메세지 타입 생성 LocationUpdate 메세지 타입을 game 패
github.com
- 작업 내용은 위 PR 참고
2) Latency 기반 추측 항법 구현을 위한 사전 작업:
- 서버에서 유저 클라이언트에 ping을 보내고 handlePong으로 수신하는 함수, onData 에서 PING 패킷 타입을 처리하는 case 작성 완료
- 참고 사항: 위 작업에서 ping 함수가 socket에 지속적으로 쓰기를 시도하기 때문에 지난번에 언급한 에디터 실행 클라이언트의 end 이벤트 미발생 문제로 구현해뒀던 timeout 기능(지정 시간동안 socket 통신하지 않으면 자동으로 연결 해제)이 동작하지 않게 되었다.
-
2. 과제 진행 간 문제점
1) GameStart 패킷 serialization 검증 실패:
Figure 1. GameStart 패킷 검증 실패 // game-notification.proto ... message GameStart { string gameId = 1; uint32 timestamp = 2; float x = 3; float y = 4; }
// index.init.js ... export const initServer = async () => { try { // load assets / proto files here await loadProtoFiles(); await testAllDbConnections(pools); gameSessionsManager.addGameSession(0); } catch (err) { // console.error(err); process.exit(1); } };
에러 코드도 있고 메세지도 출력되는 버그여서 금방 고친 문제였다. 기존에 game session 생성시 ID 값을 위와 같이 0으로 지정해 뒀었는데, 새로 추가한 GameStart 메세지 타입은 gameId를 string 형태로 받기 때문에 타입 불일치로 검증이 실패했다. Proto 파일을 바꾸는 것 보단 gameId를 proto에 맞추는게 맞다고 생각하여 constants를 추가하고 초기 게임 세션의 ID를 MAIN_GAME_ID 라는 이름으로 매핑하여 사용하기로 했다.
2) Overflow로 인한 Deserialization 실패:
Figure 2. 클라이언트 역직렬화 시 overflow 발생 이 또한 단순한 타입 문제였는데, 클라이언트의 Packets.cs에는 timestamp의 값을 uint로 저장하고 있고, 서버 쪽 proto 파일에서는 uint64로 전송하고 있었다. uint64가 long이어서 클라이언트의 uint 타입으로 연산 중 overflow가 발생하고 있다고 추측하여 서버 쪽 timestamp 타입을 uint32로 변경해줘서 해결했다.
C#에서는 proto 파일 대신 protobuf-net을 사용하며, [ProtoContract] 라는 attribute를 붙인 class를 쓰는데, dotnet(.NET) 문서를 확인해보니 uint64 타입은 ulong으로 치환된다고 한다.
3. 기타 사항
1) callback 함수 bind 테스트:
Figure 3. user socket에 write 시 undefined로 인한 TypeError 발생 // game.model.js class Game { constructor(id) { this.id = id; this.users = []; this.intervalManager = new IntervalManager(); this.state = gameStateConstants.WAITING; } addUser(user) { if (this.users.length >= gameConstants.MAX_PLAYERS) { throw new Error("Game session is full."); } this.users.push(user); // this.intervalManager.addPlayer(user.id, user.ping.bind(user), 1000); this.intervalManager.addPlayer(user.id, user.ping, 1000); // 문제 위치 } ...
// user.model.js class User { ... ping() { const now = Date.now(); console.log(`[${this.id}] PING`); this.socket.write(serialize(protoTypeNames.common.Ping, new PingData(now))); } ...
// interval.manager.js class IntervalManager extends BaseManager { ... addPlayer(playerId, callback, interval, type = "user") { if (!this.intervals.has(playerId)) { this.intervals.set(playerId, new Map()); } this.intervals.get(playerId).set(type, setInterval(callback, interval)); } ...
Game 클래스에는 addUser method가 있는데, 여기서 Game 인스턴스에 속한 IntervalManager 인스턴스의 addPlayer method를 호출한다. 이 addPlayer는 두 번째 인자로 callback 함수를 받아서 세 번째 인자의 interval 만큼을 주기로 해당 함수를 호출하도록 setInterval을 설정해주는 역할을 하는데, 여기서 callback 함수를 넘겨주는 과정에 bind를 안쓰면 어떻게 되는지 궁금해서 제거해보니 생긴 에러다.
알아보니 setInterval가 context를 유지해주지 않아서 user.ping method가 기존의 user context를 잃어버리게 되고, 결과적으로 ping method 내부의 this가 global 객체를 가리키는 상태가 된다. 따라서 context 유지를 위해 bind를 사용한다고 이해하면 되겠다.
class Temp { constructor(a) { this.a = a; } getA () { return this.a } } const temp = new Temp (13); console.log(temp.getA()); // 13 const getA = temp.getA; // console.log(getA()); // undefined error const boundGetA = temp.getA.bind(temp); console.log(boundGetA()) // 13
이해를 돕기위해 좀 더 자세하게 설명하면 위와 같은 예시를 들 수 있는데, Temp라는 클래스에서 constructor를 통해 a라는 값을 전달받아 this.a에 저장하도록 해두고 getA라는 method를 이용해 해당 값을 반환하는 단순한 구조로 작성했다. const temp = new Temp(13)을 통해 인스턴스를 생성하면 temp.getA() method를 통해 인스턴스의 a 값을 얻어낼 수 있지만, 만약 그 다음줄 처럼 const getA = temp.getA를 하게 되면, getA라는 method를 함수 째로 뜯어내서 getA라는 변수에 저장하여, 추후에 getA()를 호출할 때 getA의 실행 context가 해당 위치로 바뀌게 되고, this가 temp 인스턴스가 아닌 모듈의 global context(여기서는 undefined)로 변경되어 undefined.a를 호출하면서 잘못된 참조로 에러가 발생하는 것이다.
예전 TIL(링크, 3. bind, call, apply 참고)에서 간단하게 학습했듯이 bind는 실행 context를 함수에 묶어주는 역할을 할 수 있다. 따라서 const boundGetA = temp.getA.bind(temp) 를 호출하면 boundGetA에 저장된 함수 getA에 temp라는 context가 묶이면서, 추후에 boundGetA를 호출할 때 해당 함수가 temp 인스턴스의 context 내에서 this를 찾게되어 temp의 a 값인 13을 정상적으로 반환할 수 있게 되는 것이다.
---
REFERENCES:Chapter 5 게임서버 주특기 플러스 개인과제 | Notion
Intro: “지금까지는 튜토리얼”
teamsparta.notion.site
> 과제 spec
GitHub - donkim1212/sparta-ch5-tcp-game-server: [내일배움캠프] 스파르타) Chapter 5 게임서버 주특기 플러스
[내일배움캠프] 스파르타) Chapter 5 게임서버 주특기 플러스 개인과제. Contribute to donkim1212/sparta-ch5-tcp-game-server development by creating an account on GitHub.
github.com
> 과제 repo
728x90'TIL-sparta' 카테고리의 다른 글
[TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-2) (0) 2024.07.06 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-3) (0) 2024.07.05 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-5) (2) 2024.07.03 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-6) (0) 2024.07.02 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-7) (0) 2024.07.01