나만의 작은 도서관
[TIL] 240726 캠프 103일차: 클라이언트가 패킷을 제대로 수신하지 못하고 오류해결(OnRecvCompleted Failed Google.ProtoBuf......) 본문
[TIL] 240726 캠프 103일차: 클라이언트가 패킷을 제대로 수신하지 못하고 오류해결(OnRecvCompleted Failed Google.ProtoBuf......)
pledge24 2024. 7. 26. 21:13오늘의 Trouble Shooting
Problem 1. 클라이언트가 패킷을 제대로 수신하지 못하고 오류 발생
지금까지 우리 팀원들은 클라이언트가 패킷을 제대로 수신하지 못하는 문제에 대해서 끙끙 앓았다. 아무리 서버측 코드를 검증해봐도 문제가 전혀 없었기 때문. 다양한 추측과 의견들이 오갔지만 마땅한 원인을 알 수 없었다. 그러다 튜터님들에게 클라이언트 코드가 문제가 있는 것이 아닌가 하는 의견을 제시했고, 그렇게 튜터님들이 클라이언트 코드들을 뜯어본 결과 오늘 오류의 원인을 알 수 있었다.
발생한 오류
팀원에 따라 해당 오류가 발생하는 빈도조차 다르고, 100%도 아닌 확률적으로 발생하는 이 오류는 빠르게 패킷을 전송하는, 일명 버튼을 연타하여 많은 양의 패킷을 전송하는 경우 빈번하게 발생하였다. 이 경우, 클라이언트도, 서버도 치명적인 오류가 뜨는 것이 아니라서 오류가 발생한 이유에도 패킷을 전송하면 전송을 할 수는 있었다. 문제는 클라이언트에서 전송에 대한 응답을 전혀하지 못한다는 것이다.
문제 해결...?: console.log()떡칠
나를 포함해서 우리 팀원들은 대부분의 기능에서 해당 오류가 발생한다는 것을 인지하고, 테스트 할 수 있는 중요 분기점마다 서버 측은 console.log(), 클라이언트 측은 Debug.Log()를 마구마구 찍어대었다. 그렇게 하나하나 추적하는 도중, 이상한 점을 발견했다. 분명 console.log()를 찍었을 때는 오류가 사라졌지만, console.log()를 지웠더니 귀신깥이 오류가 발생하였다. console.log()가 매우 중요한 역할을 하는 것처럼 작동하는 것이었다! 특히 클라이언트에게 패킷을 보내는 createResponse함수 안에 console.log()를 찍었을 때 오류가 덜 발생하였다. 심지어 console.log()의 수가 많아질수록 오류가 발생하는 비율은 0에 수렴했다!
원인은 모르겠지만 결과가 확실하다는 것을 깨닫고 팀원들은 기능 구현을 위해 createResponse함수 안에 console.log()를 여러 번 찍어 테스트 하게 되었다.
패킷을 보내는 방법
createResponse함수는 패킷에 들어갈 페이로드를 매개변수로 넣어주면 완성된 패킷을 뱉어준다.
// 사용 예시
const enterDungeonResponse = createResponse('response', 'S_EnterDungeon', {
dungeonInfo,
player: playerStatus,
screenText,
battleLog,
});
socket.write(enterDungeonResponse);
createResponse함수
import { getProtoMessages } from '../../init/loadProtos.js';
import { config } from '../../config/config.js';
import { PACKET_TYPE } from '../../constants/header.js';
export const createResponse = (packageType, packetId, data = null) => {
const protoMessages = getProtoMessages();
const Response = protoMessages[packageType][packetId];
const buffer = Response.encode(data).finish();
const packetLength = Buffer.alloc(config.packet.totalLength);
packetLength.writeUInt32BE(
buffer.length + config.packet.totalLength + config.packet.typeLength,
0,
);
const packetType = Buffer.alloc(config.packet.typeLength);
packetType.writeUInt8(PACKET_TYPE[packetId], 0);
return Buffer.concat([packetLength, packetType, buffer]);
};
Solve. 클라이언트 코드의 문제: 패킷 파싱 시 offset 누락
오늘 저녁 쯤에 튜터님에게 해당 오류의 원인을 알아냈다며, 그 이유를 알려주셨다. 그 이유는 바로, 수신한 패킷을 파싱하는 코드에 offset이 누락되었다는 것이다. 아래 예시로 문제가 발생했던 이유를 알아보자.
여러 패킷이 수신된 경우
패킷 A, B, C가 짧은 시간 내에 수신되었다고 해보자. (각 패킷의 길이는 21, 10, 37이며, 각 패킷의 헤더는 동일하며 각각 5바이트의 길이로 가정한다) 각 패킷이 차례대로 들어왔을 때, A 패킷의 길이는 21이므로 수신 패킷을 처리하는 함수는 파싱으로 헤더에서 21이라는 크기를 얻어 수신버퍼에서 5~21까지의 데이터를 처리한다. 처리가 완료되었다면, 수신 버퍼에서 0~21까지의 데이터를 삭제하게된다.
그런데, 만약 A 패킷이 처리가 완료되어 데이터를 삭제하기 전에 B 패킷을 파싱하려고 한다면 어떻게 해야할까? 패킷 A, B, C는 짧은 시간 내에 수신되었기 때문에 위와 같은 상황은 충분히 벌어질 수 있다. 만약 이러한 경우가 발생하여 패킷 B를 처리해야 하는 경우, 처리가 완료되지 않은 데이터의 길이를 저장하는 offset이라는 변수를 사용해 offset부터 파싱을 하면된다. 즉, 패킷 A의 길이가 21이므로 패킷 A에 처리가 완료되기 전까진 offset은 21이 되고, B는 수신버퍼에서 0부터가 아닌 21부터 파싱을 진행하면 된다. 같은 이유로, 패킷 A, B가 처리되기 전에 패킷 C를 처리하려고 한다면 21+10인 31부터 파싱을 진행하면 된다.
하지만, 제공받은 코드에서 해당 offset코드가 누락되어있었고, 이러한 이유로 앞에 있는 패킷이 빠르게 처리되어 패킷 A가 처리되지 전 패킷 B가 파싱하는 경우가 발생하지 않았을 때는 오류가 나지 않았던 것이고, 그렇지 않은 경우에서는 오류가 났던 것이다.
결국, 코드를 아래와 같이 변경되었고, 변경된 후로는 한 번도 오류가 발생하지 않았다.
문제의 그 코드... 단 4줄!
// 변경 전
sizeBuffer[0] = buffer.Array[3;
sizeBuffer[1] = buffer.Array[2];
sizeBuffer[2] = buffer.Array[1];
sizeBuffer[3] = buffer.Array[0];
// 변경 후
sizeBuffer[0] = buffer.Array[3 + buffer.Offset];
sizeBuffer[1] = buffer.Array[2 + buffer.Offset];
sizeBuffer[2] = buffer.Array[1 + buffer.Offset];
sizeBuffer[3] = buffer.Array[0 + buffer.Offset];
오늘 한 일
- 최종 프로젝트
- 캐릭터 피격시 체력 감소 및 사망 처리
- 2번째 던전 입장 시 입장 불가 오류 해결
'Today I Learn' 카테고리의 다른 글
[TIL] 240725 캠프 102일차: 하이젠버그(Heisenbug)와 console.log (0) | 2024.07.25 |
---|---|
[TIL] 240724 캠프 101일차: 캐릭터 디스폰을 배열로 묶어서 하는 이유, 유니티: protoBuf를 잘못 파싱하면 빈 중괄호가 나온다. (0) | 2024.07.24 |
[TIL] 240723 캠프 100일차: 프로토버프 컴파일러, 유니티 EventSystem (0) | 2024.07.23 |
[TIL] 240722 캠프 99일차: 2D 플랫포머-아틀라스와 애니메이션 (0) | 2024.07.22 |
[TIL] 240719 캠프 96일차: 유니티 2D 플랫포머 게임 제작 준비 (0) | 2024.07.19 |