본문으로 건너뛰기

1. WebSocket 프레임의 기본 구조

  • WebSocket 프레임은 TCP 기반의 스트림 지향 프로토콜에서 메시지 단위의 통신을 가능하게 하는 핵심 구조입니다.
  • RFC 6455에 정의된 WebSocket 프레임의 구조는 다음과 같습니다.
    • 클라이언트에서 서버로(또는 그 반대) 전송되는 모든 프레임은 같은 구조를 가지고 있습니다.
 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) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
  • FIN
    • 프레임의 마지막 프래그먼트 여부를 나타냅니다.
    • 1이면 마지막 프래그먼트, 0이면 연속 프래그먼트가 있음을 의미합니다.
  • RSV1, RSV2, RSV3: 확장을 위한 예약 비트
  • Opcode: 메시지 타입 (text, binary, ping, pong, close, continuation)
  • Mask: 클라이언트 → 서버 메시지에만 사용되는 마스킹 여부
    • 클라이언트가 전송하는 프레임은 반드시 마스킹 필요
    • 클라이언트에서 오는 메시지는 반드시 마스킹되어야 하므로, 서버는 이 값이 1일 것으로 예상해야 합니다
    • 실제로 스펙에 따르면 서버는 클라이언트가 마스킹되지 않은 메시지를 보내는 경우 해당 클라이언트와의 연결을 끊어야 합니다.
  • Payload Length: 페이로드 길이 표현
  • Masking Key: 마스킹 키 (4바이트)

1.1 프레임 크기의 특징

  • 최소 크기: 2바이트 (서버에서 페이로드 없는 close 메시지)
  • 최대 헤더 크기: 14바이트 (클라이언트에서 64KB 이상의 페이로드를 가진 메시지)
  • 클라이언트 → 서버 메시지 예시: "over9000" 전송 시 14바이트 (마스킹으로 인한 랜덤성 포함)
  • 서버 → 클라이언트 메시지 예시: 동일한 "over9000" 전송 시 10바이트로 고정

2. Payload Length(페이로드 길이)

  • 페이로드 데이터를 제대로 읽으려면 어디까지가 데이터인지 알아야 합니다.
  • 그래서 페이로드의 길이를 아는 게 중요한데, 이걸 알아내는 과정에 대해 알아봅니다.

2.1 페이로드 길이 표현

  1. 먼저 9-15번째 비트를 읽어서 숫자로 변환합니다.
    • 이 숫자가 125 이하면? 그게 바로 실제 길이입니다.
    • 더 이상 볼 것 없이 끝!
    • 126이면 2단계로 가야 합니다.
    • 127이면 3단계로 가야 합니다.
  2. (126인 경우) 그 다음 16비트를 더 읽어서 그게 실제 길이가 됩니다.
  3. (127인 경우) 그 다음 64비트를 읽어서 그게 실제 길이가 됩니다. 단, 이때 맨 앞 비트는 반드시 0이어야 합니다.

3. Masking(마스킹)

3.1 마스킹의 목적

  • 웹소켓 데이터 프레임에서 마스킹을 하는 주된 이유는 캐시 포이즈닝(cache poisoning) 공격을 방지하기 위해서입니다.
  • 마스크 키는 일반적인 의미의 비밀번호나 암호화 키와는 다릅니다.
  • 재미있게도 마스크 키가 프레임에 그대로 포함되어 있다는 점에서 보안을 위한 것이 아닙니다.
  • 이는 웹 프록시 서버나 캐시 서버의 "캐시 포이즈닝"이라는 공격을 방지하기 위한 것입니다.
    • 악의적인 웹사이트가 프록시 서버의 캐시를 오염시키려고 할 수 있습니다
    • 마스킹을 하면 매번 같은 메시지라도 다르게 보이게 됩니다 (마스크 키가 매번 랜덤으로 바뀌므로)
    • 브라우저가 매 프레임마다 무작위로 마스크를 생성하므로 악의적인 코드가 의도적으로 특정 패턴의 데이터를 만들어낼 수 없음
    • 이로 인해 프록시 서버가 WebSocket 메시지를 캐시하지 못하게 됩니다

3.2 마스킹 규칙

  • 클라이언트 → 서버: 모든 메시지는 반드시 마스킹 필요
  • 서버 → 클라이언트: 마스킹 사용 금지
  • 마스킹 키: 4바이트 길이, 예측 불가능해야 함

3.3 마스킹 프로세스

// "hello" 메시지의 마스킹 예시 (ASCII/UTF-8)
원본: 104, 101, 108, 108, 111
마스크: 1, 2, 3, 4
마스킹
결과: 105, 103, 111, 104, 110

// 각 바이트별 XOR 연산
'h' ^ 1, 'e' ^ 2, 'l' ^ 3, 'l' ^ 4, 'o' ^ 1
경고

마스킹을 우회하는 것(예: 0,0,0,0 마스크 사용)은 보안상 위험할 수 있으며, 프로토콜 명세를 위반합니다.

4. Message Type(메시지 타입)

  • WebSocket 프레임은 6가지 타입을 지원합니다:
    • text: UTF-8 검증 필요
    • binary: 바이너리 데이터
    • ping: 연결 상태 확인
    • pong: ping에 대한 응답
    • close: 연결 종료
    • continuation: 프래그먼트 연속

4.1 타입 특성

  • text vs binary: UTF-8 유효성 검사가 필요한 text보다 binary 사용 권장
  • ping/pong: 애플리케이션 레벨에서 구현하는 것이 더 효율적
  • close: 페이로드의 처음 2바이트는 close code를 포함해야 함

4.2 Ping/Pong

  • 핸드쉐이크 이후 언제든지, 클라이언트나 서버는 상대방에게 ping을 보내기로 선택할 수 있습니다.
  • ping을 받으면, 수신자는 가능한 한 빨리 pong을 보내야 합니다.
  • 예를 들어, 이를 통해 클라이언트가 여전히 연결되어 있는지 확인할 수 있습니다.
  • ping이나 pong은 일반적인 프레임이지만, 이는 '컨트롤 프레임'입니다.
  • ping은 0x9의 opcode를 가지고, pong은 0xA의 opcode를 가집니다.
  • ping을 받으면, ping과 정확히 동일한 Payload Data를 가진 pong을 보내야 합니다 (ping과 pong의 경우, 최대 페이로드 길이는 125입니다).
  • ping을 보내지 않았는데도 pong을 받을 수 있습니다; 이런 일이 발생하면 무시하세요.

4.3 Close

  • 클라이언트나 서버 중 어느 쪽이든 종료 핸드쉐이크를 시작하기 위해 특정 제어 시퀀스가 포함된 데이터를 가진 컨트롤 프레임을 보낼 수 있습니다.
  • 이러한 프레임을 받으면, 다른 피어는 응답으로 Close 프레임을 보냅니다. 그런 다음 첫 번째 피어가 연결을 종료합니다.
  • 연결이 종료된 후 수신된 추가 데이터는 모두 폐기됩니다.
  • Close 프레임을 받은 엔드포인트가 이전에 Close 프레임을 보내지 않았다면, 반드시 응답으로 Close 프레임을 보내야 합니다
    • 응답할 때 일반적으로 받은 상태 코드를 그대로 반환합니다
    • 가능한 한 빨리 응답해야 합니다
    • 현재 메시지가 전송될 때까지 Close 프레임 전송을 지연할 수 있습니다
      • 하지만 이미 Close 프레임을 보낸 엔드포인트가 계속해서 데이터를 처리한다는 보장은 없습니다

5. Fragmentation(프래그멘테이션)

5.1 프래그멘테이션의 목적

  1. 스트리밍: 전체 길이를 모르는 상태에서 데이터 전송
  2. 대형 메시지 중단: 컨트롤 프레임 삽입 가능
  3. 프록시 지원: 작은 버퍼로 메시지 분할 가능

5.2 프래그멘테이션 규칙

  • text/binary 프레임만 분할 가능
  • 프래그먼트 사이에는 컨트롤 프레임만 삽입 가능
  • 한 번에 하나의 프래그먼트 메시지만 처리
위험

프래그멘테이션은 구현의 복잡성을 크게 증가시키며, 메모리 관리를 어렵게 만듭니다.