← posts/b.log()

blog92@web:~$ cat posts/websocket-06-frame-structure.md

NETWORK2 min read

WebSocket 완전 정복 6편 — 프레임 구조: FIN, opcode, Payload len, 그리고 마스킹

WebSocket 프레임의 비트 레이아웃을 도식으로 읽고 FIN·RSV·opcode·Payload len·Masking-key를 필드별로 해설한 뒤, 클라이언트에서 서버로 가는 프레임만 마스킹하는 이유가 프록시 캐시 오염 방지라는 점과 학습용 파서 코드를 다룹니다.

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, 이후는 0x0 continuation).
  • RSV1~3: 확장용. permessage-deflate는 RSV1을 "압축됨" 표시로 사용.
  • opcode: 0x1 텍스트(UTF-8 필수), 0x2 바이너리, 0x8 Close, 0x9 Ping, 0xA Pong. 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는 프레임 경계를 보장하지 않으므로 실제 구현은 부분 프레임·다중 프레임 버퍼를 처리해야 합니다. 실무에서는 검증된 라이브러리를 쓰세요.

더 깊이

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..