WebSocket 완전 정복 · 2부. 중급: 프로토콜 내부와 설계 패턴 · 16편 중 6편
이전 편: 5편 — 첫 번째 서버
5편까지는 send()로 보내고 onmessage로 받는 API 표면만 다뤘습니다. 이번 편은 그 아래로 내려가 선 위를 실제로 오가는 프레임의 비트 레이아웃을 필드별로 읽습니다 — 다음 편의 Ping·Pong·Close도 여기서 정의되는 opcode이고, 클라이언트가 보내는 프레임만 마스킹되는 이유도 이 구조를 봐야 설명이 됩니다.
프레임 레이아웃
text
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64 bits) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (0 or 4 bytes) |
+---------------------------------------------------------------+
| Payload Data |
+---------------------------------------------------------------+필드별로 읽기
- FIN: 메시지의 마지막 프레임이면 1. 큰 메시지는 여러 프레임으로 분할 가능(첫 프레임은 실제 opcode, 이후는
0x0continuation). - RSV1~3: 확장용. permessage-deflate는 RSV1을 "압축됨" 표시로 사용.
- opcode:
0x1텍스트(UTF-8 필수),0x2바이너리,0x8Close,0x9Ping,0xAPong.0x8이상은 제어 프레임으로 페이로드 ≤125바이트, 분할 불가, 분할된 데이터 메시지 중간에 끼어들 수 있음. - Payload len: 0~125는 그대로, 126이면 다음 16비트, 127이면 다음 64비트가 길이.
- MASK / Masking-key: 클라이언트→서버는 반드시 마스킹, 서버→클라이언트는 마스킹 금지. 4바이트 키로 XOR. 암호화가 아니라, 브라우저를 이용해 HTTP 요청처럼 보이는 바이트열을 보내 투명 프록시 캐시를 오염시키는 공격(cache poisoning)을 막기 위한 장치.
학습용 파서
ts
function parseFrame(buf: Buffer) {
const b0 = buf[0], b1 = buf[1];
const fin = (b0 & 0x80) !== 0;
const opcode = b0 & 0x0f;
const masked = (b1 & 0x80) !== 0;
let len = b1 & 0x7f;
let offset = 2;
if (len === 126) { len = buf.readUInt16BE(2); offset = 4; }
else if (len === 127) { len = Number(buf.readBigUInt64BE(2)); offset = 10; }
let payload: Buffer;
if (masked) {
const mask = buf.subarray(offset, offset + 4);
offset += 4;
payload = Buffer.from(buf.subarray(offset, offset + len));
for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
} else {
payload = buf.subarray(offset, offset + len);
}
return { fin, opcode, payload, frameLength: offset + len };
}실무에서는 검증된 라이브러리를 쓴다
TCP는 프레임 경계를 보장하지 않으므로 실제 구현은 부분 프레임·다중 프레임 버퍼를 처리해야 합니다. 실무에서는 검증된 라이브러리를 쓰세요.
더 깊이
- 풀스택 개발자를 위한 네트워크 4장 — 전송 계층: TCP와 UDP, 포트, 소켓: "TCP는 프레임 경계를 보장하지 않는다"는 이 편 마지막 문장의 출처입니다. 바이트 스트림이라 두 번 쓴 것이 한 덩어리로 도착할 수도, 엉뚱한 자리에서 잘려 도착할 수도 있다는 것을 소켓 수준에서 봅니다.
- 11편 — 서브프로토콜과 확장: 버전 협상과 permessage-deflate: RSV1 비트를 "압축됨" 표시로 쓰는 permessage-deflate가 어떻게 협상되고 어떤 비용이 따르는지를 다룹니다.