더미 클라이언트에 사용할 틀은 강의에서 사용했던 client.js를 패킷 부분을 수정해서 사용해볼려고 한다.
const createPacket = (handlerId, userId, payload, clientVersion = '1.0.0', type, name) => {
const protoMessages = getProtoMessages();
const PayloadType = protoMessages[type][name];
if (!PayloadType) throw new Error('PayloadType을 찾을 수 없습니다.');
const payloadMessage = PayloadType.create(payload);
const payloadBuffer = PayloadType.encode(payloadMessage).finish();
return {
handlerId,
userId,
version: clientVersion,
payload: payloadBuffer,
};
};
const sendPacket = (socket, packet) => {
const protoMessages = getProtoMessages();
const Packet = protoMessages.common.Packet;
if (!Packet) {
console.error('Packet 메시지를 찾을 수 없습니다.');
return;
}
const buffer = Packet.encode(packet).finish();
// 패킷 길이 정보를 포함한 버퍼 생성
const packetLength = Buffer.alloc(TOTAL_LENGTH);
// 패킷 길이에 타입 바이트 포함
packetLength.writeUInt32BE(buffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0);
// 패킷 타입 정보를 포함한 버퍼 생성
const packetType = Buffer.alloc(PACKET_TYPE_LENGTH);
packetType.writeUInt8(1, 0); // NORMAL TYPE
// 길이 정보와 메시지를 함께 전송
const packetWithLength = Buffer.concat([packetLength, packetType, buffer]);
socket.write(packetWithLength);
};
const sendPong = (socket, timestamp) => {
const protoMessages = getProtoMessages();
const Ping = protoMessages.common.Ping;
const pongMessage = Ping.create({ timestamp });
const pongBuffer = Ping.encode(pongMessage).finish();
// 패킷 길이 정보를 포함한 버퍼 생성
const packetLength = Buffer.alloc(TOTAL_LENGTH);
packetLength.writeUInt32BE(pongBuffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0);
// 패킷 타입 정보를 포함한 버퍼 생성
const packetType = Buffer.alloc(1);
packetType.writeUInt8(0, 0);
// 길이 정보와 메시지를 함께 전송
const packetWithLength = Buffer.concat([packetLength, packetType, pongBuffer]);
socket.write(packetWithLength);
};
socket.connect(PORT, HOST, async () => {
const successPacket = createPacket(
0,
deviceId,
{
deviceId,
playerId: 1,
latency,
},
CLIENT_VERSION,
'initial',
'InitialPayload',
);
await sendPacket(socket, successPacket);
await delay(500);
});
socket.on('data', (data) => {
const length = data.readUInt32BE(0);
const totalHeaderLength = TOTAL_LENGTH + PACKET_TYPE_LENGTH;
const packetType = data.readUInt8(4);
const packet = data.subarray(totalHeaderLength, totalHeaderLength + length);
if (packetType === PACKET_TYPE.NORMAL) {
const Response = protoMessages.response.Response;
try {
const response = Response.decode(packet);
const responseData = JSON.parse(Buffer.from(response.data).toString());
if (response.handlerId === 0) {
userId = responseData.userId;
}
} catch (e) {
console.error(e);
}
} else if (packetType === PACKET_TYPE.PING) {
try {
const Ping = protoMessages.common.Ping;
const actualPingData = packet.subarray(0, 7);
const pingMessage = Ping.decode(actualPingData);
const timestampLong = new Long(
pingMessage.timestamp.low,
pingMessage.timestamp.high,
pingMessage.timestamp.unsigned,
);
sendPong(socket, timestampLong.toNumber());
} catch (pongError) {
console.error('Ping 처리 중 오류 발생:', pongError);
console.error('Ping packet length:', packet.length);
console.error('Raw ping packet:', packet);
}
}
(async () => {
await loadProtos();
const LIMIT = 200;
const DELAY_MS = 25; // 0.05초
const dummies = [];
for (let i = 0; i < LIMIT; i++) {
await delay(DELAY_MS); // 0.05초 대기
const deviceId = uuidv4().slice(0, 5);
const dummy = createDummyClient(deviceId);
dummies.push(dummy);
dummy.init();
}
console.log(`${dummies.length}개의 클라이언트가 추가되었습니다.`);
})();
const createDummyClient = (deviceId) => {
const protoMessages = getProtoMessages();
let userId;
let socket = new net.Socket();
let x = 0;
let y = 0;
let latency = 50 + Math.random() * 100;
let speed = 3;
let framerate = 30; // 고정 30 프레임
let radius = 10; // 캐릭터가 이동할 원의 반지름 (작은 원)
let maxRadius = 15; // 반경 200 안에서 시작 위치를 설정
let centerX = Math.random() * (2 * maxRadius) - maxRadius; // 중심 x 좌표 (랜덤)
let centerY = Math.random() * (2 * maxRadius) - maxRadius; // 중심 y 좌표 (랜덤)
let angle = Math.random() * 2 * Math.PI; // 랜덤 초기 각도
const sendLocationUpdate = () => {
if (!userId) return;
setTimeout(() => {
const deltaTime = 1 / framerate;
const angularSpeed = speed / radius; // 각속도
angle += angularSpeed * deltaTime; // 각도 업데이트
// 작은 원을 따라 이동할 새로운 x, y 좌표
x = centerX + radius * Math.cos(angle);
y = centerY + radius * Math.sin(angle);
const packet = createPacket(
2,
deviceId,
{ x, y },
CLIENT_VERSION,
'game',
'UpdateLocationPayload',
);
sendPacket(socket, packet);
}, latency);
};
...
sendLocationUpdate();
...
}
더미 클라이언트와 서버가 핑 패킷을 교환했을 때 패킷의 길이가 다르게 전달받는 문제
Ping 처리 중 오류 발생: RangeError: index out of range: 12 +
10 > 12
message Ping {
int64 timestamp = 1;
}
Ping packet too short: 7
createPingPacket 함수에서 timestamp를 Long 타입으로 변환하지 않고 직접 JavaScript의 number를 사용하는 것.
Protocol Buffers의 int64 타입은 JavaScript의 number로는 정확하게 표현할 수 없을 수 있음.
export const createPingPacket = (timestamp) => {
const protoMessages = getProtoMessages();
const ping = protoMessages.common.Ping;
// 디버깅을 위한 로그 추가
console.log('Original timestamp:', timestamp);
const timestampLong = Long.fromNumber(timestamp);
console.log('Converted to Long:', timestampLong);
const payload = { timestamp: timestampLong };
console.log('Payload:', payload);
const message = ping.create(payload);
console.log('Created message:', message);
const pingPacket = ping.encode(message).finish();
console.log('Encoded packet length:', pingPacket.length);
console.log('Encoded packet:', pingPacket);
const serialized = serializer(pingPacket, PACKET_TYPE.PING);
console.log('Final packet length:', serialized.length);
console.log('Final packet:', serialized);
return serialized;
};
Original timestamp: 1730359491371
Converted to Long: Long { low: -512328917, high: 402, unsigned: false }
Payload: { timestamp: Long { low: -512328917, high: 402, unsigned: false } }
Created message: Ping {
timestamp: Long { low: -512328917, high: 402, unsigned: false }
}
Encoded packet length: 7
Encoded packet: <Buffer 08 ab f6 d9 8b ae 32>
Final packet length: 12
Final packet: <Buffer 00 00 00 0c 00 08 ab f6 d9 8b ae 32>
Long 타입으로 정상적으로 변환되지만
Protocol Buffers가 이를 인코딩할 때 varint 형식으로 압축하여 7바이트로 인코딩됨.
Ping 처리 중 오류 발생: RangeError: index out of range: 12 +
10 > 12
at indexOutOfRange (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:13:12)
at BufferReader.read_uint32 [as uint32] (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:98:19)
at Reader.skipType (D:\github\multi-player-game\node_modules\protobufjs\src\reader.js:372:37)
at Type.Ping$decode [as decode] (eval at Codegen (D:\github\multi-player-game\node_modules\@protobufjs\codegen\index.js:50:33), <anonymous>:15:5)
at Socket.<anonymous> (file:///D:/github/multi-player-game/src/utils/dummy/dummyClient.js:161:36)
at Socket.emit (node:events:519:28)
at addChunk (node:internal/streams/readable:559:12)
at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
at Readable.push (node:internal/streams/readable:390:5)
at TCP.onStreamRead (node:internal/stream_base_commons:191:23)
Ping packet length: 12
Raw ping packet: <Buffer 08 8b 95 e3 8b ae 32 00 00 08 24 03>
Ping 메시지는 앞의 7바이트(08 8b 95 e3 8b ae 32)만 해당하고 뒷 자리에 불필요한 바이트들이 붙어있음.
else if (packetType === PACKET_TYPE.PING) {
try {
const Ping = protoMessages.common.Ping;
// 실제 varint로 인코딩된 timestamp만 읽기 위해
// 첫 7바이트만 사용
const actualPingData = packet.slice(0, 7); // varint 인코딩된 실제 데이터만 추출
const pingMessage = Ping.decode(actualPingData);
const timestampLong = new Long(
pingMessage.timestamp.low,
pingMessage.timestamp.high,
pingMessage.timestamp.unsigned,
);
sendPong(this._socket, timestampLong.toNumber());
} catch (pongError) {
console.error('Ping 처리 중 오류 발생:', pongError);
console.error('Ping packet length:', packet.length);
console.error('Raw ping packet:', packet);
}
}
오류없이 정상적으로 핑 교환 확인
// 서버 터미널
59252 ping
pong 59252 at 1730374376064 latency 2
14de1 ping
ffe06 ping
pong ffe06 at 1730374376078 latency 0.5
pong 14de1 at 1730374376080 latency 2
78b15 ping
4b818 ping
pong 4b818 at 1730374376096 latency 1.5
pong 78b15 at 1730374376096 latency 2
92995 ping
5fb72 ping
49b21 ping
pong 92995 at 1730374376100 latency 0.5
pong 5fb72 at 1730374376100 latency 0
pong 49b21 at 1730374376101 latency 0.5
8aa7c ping
f4f3b ping
pong f4f3b at 1730374376141 latency 1
pong 8aa7c at 1730374376143 latency 2
88b5d ping
02376 ping
16120 ping
pong 88b5d at 1730374376173 latency 2
pong 02376 at 1730374376173 latency 2
pong 16120 at 1730374376173 latency 2
be501 ping
pong be501 at 1730374376187 latency 1.5
0ec71 ping
pong 0ec71 at 1730374376205 latency 2.5
ebce9 ping
pong ebce9 at 1730374376206 latency 0
// 클라이언트 터미널
100개의 클라이언트가 추가되었습니다.