Quick Flow
TCP는 순서 있는 byte stream이므로 data callback 한 번이 application message 한 개와 같지 않습니다. 하나의 message가 여러 chunk로 나뉠 수도 있고, 여러 message가 한 chunk에 붙어 올 수도 있습니다. 수신 버퍼에 누적한 뒤, header가 가리키는 길이만큼 준비됐을 때만 한 message를 꺼냅니다.
const MAX_PAYLOAD = 8 * 1024;
let pending = Buffer.alloc(0);
function onData(chunk: Buffer) {
pending = Buffer.concat([pending, chunk]);
while (pending.length >= 2) {
const size = pending.readUInt16BE(0);
if (size > MAX_PAYLOAD) {
closeForProtocolError("payload too large");
return;
}
if (pending.length < 2 + size) return;
const payload = pending.subarray(2, 2 + size);
pending = pending.subarray(2 + size);
handleMessage(payload);
}
}| 수신 상태 | 처리 | 잘못 가정했을 때 |
|---|---|---|
| header도 덜 옴 | buffer에 계속 보관 | 빈 header를 읽어 예외 발생 |
| header는 왔지만 body가 덜 옴 | 다음 chunk까지 대기 | 잘린 JSON·binary를 parse |
| 한 chunk에 여러 message | loop로 모두 꺼냄 | 뒤 message가 다음 수신까지 지연 |
| 선언한 body가 너무 큼 | 즉시 연결 종료 또는 오류 처리 | memory 고갈과 parser 공격 |
프레임 형식의 계약
프레임(frame)은 stream에서 하나의 application message를 꺼낼 수 있도록 정한 byte 형식입니다. 가장 단순한 형식은 길이 + payload입니다. 위 예시는 2 byte unsigned big-endian 길이 다음에 payload를 둡니다. 길이 field의 byte 수, byte order, payload가 JSON인지 binary인지, message type을 어디에 둘지는 sender와 receiver가 정확히 같아야 합니다.
message type은 header에 고정 길이로 넣거나 payload 첫 field로 넣을 수 있습니다. 빈번한 binary protocol이라면 version, type, sequence, payloadLength를 header로 두는 방식이 흔합니다. JSON을 쓴다고 framing 문제가 사라지지는 않습니다. JSON.parse()는 한 JSON 문서의 끝을 TCP stream에서 찾아 주지 않으므로, 길이·delimiter·명시적인 encoding 규칙 중 하나가 필요합니다.
delimiter 기반 framing은 사람이 읽기 쉽지만 payload에 delimiter가 들어갈 때 escaping 또는 length 규칙이 추가됩니다. length prefix는 binary·JSON 모두에 쓸 수 있지만 length 검증을 빼면 공격자가 거대한 buffer를 기다리게 만들 수 있습니다. 게임 message는 최대 크기를 type별로 좁게 두고, snapshot처럼 커질 수 있는 data는 pagination·chunking 또는 interest management를 별도로 설계합니다.
수신 버퍼를 다루는 위치
socket 하나마다 독립 pending buffer가 있어야 합니다. 전역 buffer를 공유하면 두 client의 byte가 섞입니다. connection이 닫힐 때 header나 body가 남아 있으면 완전한 message로 처리하지 말고 버립니다. reconnect 후 이전 connection의 pending data를 새 session에 넘기지 않습니다.
handleMessage()가 오래 걸리면 receive loop가 밀릴 수 있습니다. framing 단계에서는 bytes를 완전한 message로 분리하고 크기·기본 형식만 빠르게 검사한 뒤, 게임 규칙 적용은 server tick의 command queue로 넘깁니다. 이 경계는 게임 서버 tick이 네트워크 수신 순서 대신 simulation 순서를 소유하게 합니다.
끊어야 하는 입력
길이가 음수처럼 해석되는 encoding, 최대 payload 초과, 알 수 없는 protocol version, 허용하지 않은 message type, 반복되는 parse 실패는 연결을 종료하거나 명시적인 protocol error로 처리합니다. body를 모두 받은 뒤에만 크기를 검사하면 이미 memory를 많이 쓴 뒤일 수 있으므로 header를 읽는 즉시 먼저 제한합니다.
partial write도 반대편에서 같은 문제를 만듭니다. 송신 API가 받은 buffer 전체를 한 번에 보냈는지, backpressure에서 어떤 queue를 쓰는지 런타임 문서를 확인합니다. TCP가 순서를 보장해도 application message의 길이와 처리 경계는 server protocol이 직접 책임집니다.
data event 한 번을 login request 한 번으로 연결하는 코드는 테스트에서는 우연히 통과할 수 있습니다. chunk 분할과 여러 frame 결합을 강제로 만드는 test를 먼저 두어야 합니다.
참고 링크
2 sources