Node.js에서 Buffer는 바이너리 데이터를 처리하기 위한 객체이다.
일반적으로 자바스크립트는 텍스트 기반(유니코드) 데이터 처리를 잘하지만, 바이너리 데이터를 직접 다룰 때는 부족한 점이 있다. 이러한 한계를 극복하기 위해 버퍼가 바이너리 데이터를 효율적으로 처리하고 저장하도록 만들어졌다.
1 (하나의 비트)
0 (하나의 비트)01000001 (1바이트, 'A'의 ASCII 코드)
11111111 (1바이트, 십진수로 255)문자 표현
'A' = 01000001 (1바이트)
'가' = 11101010 10100100 (2바이트, UTF-8)
숫자 표현
숫자 5 (1바이트) = 00000101
숫자 1000 (2바이트) = 00000011 11101000
파일 크기 단위
1KB (킬로바이트) = 1,024바이트
1MB (메가바이트) = 1,024KB
1GB (기가바이트) = 1,024MB
// 1바이트 크기의 버퍼
const buf1 = Buffer.alloc(1); // 8비트 저장 가능
// 4바이트 크기의 버퍼 (32비트 정수 저장 가능)
const buf4 = Buffer.alloc(4); // 32비트 저장 가능
const buf = Buffer.alloc(1);
buf[0] = 65; // 'A'의 ASCII 코드 (01000001)
console.log(buf.toString()); // 출력: 'A'
// 플레이어 위치 데이터 (8바이트)
const position = Buffer.alloc(8);
position.writeInt32BE(x, 0); // x좌표 (4바이트)
position.writeInt32BE(y, 4); // y좌표 (4바이트)
바이너리 데이터는 컴퓨터가 이해하는 가장 기본적인 데이터 형식으로, 0과 1로만 이루어진 이진수 데이터이다.
텍스트 데이터
{"name": "John", "age": 25}바이너리 데이터
01001000 01100101 01101100 01101100 01101111 (Hello의 바이너리 표현)이미지 파일
FF D8 FF E0 (JPEG 파일의 시작 부분)
숫자의 바이너리 표현
00000000 00000000 00000000 00000101
게임에서의 위치 데이터
{"x": 100, "y": 200} (약 20바이트)00000064 000000C8 (8바이트)효율성
정확성
보안
Buffer.alloc(size)
Buffer.alloc(0)은 0바이트 크기의 빈 버퍼를 생성한다.const buf = Buffer.alloc(5); // <Buffer 00 00 00 00 00>Buffer.from(data)
const buf = Buffer.from('Hello'); // <Buffer 48 65 6c 6c 6f>Buffer.concat([buffer1, buffer2, ...])
const buf1 = Buffer.from('Hello');
const buf2 = Buffer.from('World');
const buf3 = Buffer.concat([buf1, buf2]); // HelloWorld
readUInt32BE(offset)
readUInt8(offset)
slice(start, end)
패킷은 네트워크에서 전송되는 데이터의 기본 단위이다. 우리 프로젝트의 각 패킷은 다음과 같은 구조로 설계되어 있다:
[패킷 길이(4바이트)] [패킷 타입(1바이트)] [실제 데이터(N바이트)]
데이터 완전성 확인
패킷 경계 구분
// 1. 빈 버퍼로 시작
socket.buffer = Buffer.alloc(0); // 새로운 연결이 생성될 때 빈 버퍼 할당
// 2. 새로운 데이터가 도착하면 기존 버퍼에 추가
socket.buffer = Buffer.concat([socket.buffer, data]);
// 예: 첫 번째 데이터 조각이 도착하면 [buffer(0바이트) + 새로운 데이터]
// 두 번째 데이터 조각이 도착하면 [이전 데이터 + 새로운 데이터]
// 3. 패킷 길이 확인 (4바이트)
const length = socket.buffer.readUInt32BE(0); // 예: 100바이트
// 4. 전체 패킷이 도착했는지 확인
if (socket.buffer.length >= length) {
// 5. 패킷 추출 및 처리
const packet = socket.buffer.slice(5, length); // 헤더(5바이트) 이후부터 패킷 길이까지
socket.buffer = socket.buffer.slice(length); // 처리한 패킷은 버퍼에서 제거
}
TCP는 데이터를 패킷(packet) 단위로 주고 받는다.
여기서 중요한 것은, 한 번에 보내려는 데이터가 쪼개지거나 여러 패킷이 합쳐질 수 있다는 것이다.
예를 들어, 게임 클라이언트에서 "플레이어 위치 데이터"를 서버로 보낼 때:
버퍼는 이런 쪼개진 데이터를 모으거나, 합쳐진 데이터를 분리해서 원래 의미 있는 데이터로 복원하는데 필수적이다.
게임 데이터는 텍스트보다 바이너리로 압축해서 보내는게 훨씬 효율적이다.
텍스트(JSON 같은 거)는 크기가 크고 파싱이 느리지만, 바이너리는 데이터 크기가 작아서 네트워크 비용을 줄이고 성능을 높여준다.
protobuf.js는 Protocol Buffers라는 데이터 직렬화 라이브러리다.
우리 프로젝트에서는 다음과 같은 패킷 구조를 사용한다:
// common.proto
message CommonPacket {
uint32 handlerId = 1; // 핸들러 ID (4바이트)
string userId = 2; // 유저 ID (UUID)
string version = 3; // 클라이언트 버전
bytes payload = 4; // 실제 데이터
}
서버에서 데이터를 받을 때의 처리 과정을 보면:
// 각 소켓 연결마다 고유한 버퍼 할당
socket.buffer = Buffer.alloc(0);
// 새로운 데이터가 오면 기존 버퍼에 추가
socket.buffer = Buffer.concat([socket.buffer, data]);
// 1. 패킷 길이 정보 읽기 (4바이트)
const length = socket.buffer.readUInt32BE(0);
// 2. 패킷 타입 정보 읽기 (1바이트)
const packetType = socket.buffer.readUInt8(4);
// 3. 실제 데이터 추출
const packet = socket.buffer.slice(5, length);
// Protocol Buffers로 디코딩
const { handlerId, payload, userId } = packetParser(packet);
// 적절한 핸들러로 처리
const handler = getHandlerById(handlerId);
await handler({ socket, userId, payload });
메모리 관리
데이터 무결성
에러 처리
Buffer는 TCP 게임 서버에서 필수적인 핵심 요소이다. 효율적인 데이터 전송과 처리를 위해 Protocol Buffers와 함께 사용되며, 적절한 관리와 주의사항을 철저히 지키면서 사용해야 한다.