Single-Port Demux — 수백 스트림을 포트 하나로

kodeholic·2026년 3월 28일

SFU 해부학

목록 보기
5/5
post-thumbnail

SFU 해부학 시리즈 #5

지금까지 SFU의 연결 과정을 하나씩 따라왔다.

  • #2: ICE-Lite로 UDP 파이프를 뚫었다
  • #3: DTLS로 키를 교환하고, SRTP로 미디어를 암호화했다
  • #4: SDP-Free 시그널링으로 트랙을 관리한다

이 모든 게 하나의 UDP 포트에서 일어난다.

30명이 화상회의를 하면, 각자 오디오 1개 + 비디오 1개씩 보낸다. 60개의 미디어 스트림이 서버의 같은 포트로 들어온다. 거기에 ICE 연결을 유지하는 STUN 패킷, 암호화 키를 교환하는 DTLS 패킷, "패킷 3개 유실됐어", "키프레임 보내줘" 같은 제어 메시지(RTCP)까지 섞여 있다. 전부 같은 UDP 포트, 같은 소켓이다.

이 뒤섞인 패킷들을 종류별로 골라내야 한다. 이 분류 작업을 demux(demultiplexing)라고 부른다. 택배 허브에서 컨베이어 벨트 위의 상자를 목적지별로 나누는 것과 같다.


1. 왜 포트 하나인가

"참여자마다 포트를 따로 쓰면 안 되나?" 당연히 된다. 실제로 초기 VoIP 시스템은 그렇게 했다. 하지만 문제가 있다.

  • 포트 고갈: 참여자 30명 × 미디어 2개 = 60개 포트. 동시 회의 100개면 6,000개 포트. UDP 포트는 65,535개가 한계다.
  • 방화벽 설정: 포트 범위를 열어야 한다. "10000~16000번 포트를 허용해주세요"라고 고객에게 요청하면, 보안 담당자가 좋아하지 않는다.
  • 포트포워딩: 지난 편에서 다뤘듯이, 공유기 뒤에 SFU를 놓으면 포트포워딩이 필요하다. 포트 하나면 설정 하나, 6,000개면 설정 6,000개다.

포트 하나로 모든 트래픽을 처리하면 이 문제가 전부 사라진다. 방화벽에서 포트 하나만 열면 되고, 포트포워딩도 하나면 끝이다.

Discord, mediasoup, LiveKit, Janus — 주요 SFU가 전부 Single-Port 방식을 쓴다. 문제는 하나의 포트로 들어오는 온갖 종류의 패킷을 정확하게 분류하는 것이다.


2. 같은 포트에 뭐가 들어오는가

하나의 UDP 포트에는 최소 4종류의 패킷이 들어온다:

  UDP 포트 10000으로 들어오는 패킷들:

  +------+------+------+------+------+------+------+
  | STUN | DTLS | SRTP | SRTP | RTCP | STUN | SRTP |
  +------+------+------+------+------+------+------+
     |      |      |      |      |      |      |
     v      v      v      v      v      v      v
  ICE     암호화   A의    B의    품질   ICE   C의
  연결    핸드     오디오  비디오  제어   유지   오디오
  수립    셰이크

  ※ 전부 같은 소켓, 같은 포트
  ※ 각 패킷이 무엇인지 어떻게 아는가?
  • STUN: ICE 연결 수립 및 유지
  • DTLS: 암호화 키 교환
  • SRTP: 암호화된 미디어 (영상, 음성)
  • SRTCP: 암호화된 제어 메시지 — "패킷이 유실됐다", "키프레임을 요청한다" 같은 품질 관리 신호. RTCP 자체는 다음 시리즈에서 자세히 다룬다

패킷이 도착하면 SFU는 이 4가지 중 무엇인지를 먼저 판단해야 한다. 틀리면 DTLS 패킷을 SRTP로 복호화하려 하거나, STUN 패킷을 미디어로 처리하려는 참사가 벌어진다.


3. 첫 바이트로 분류한다

다행히 각 프로토콜은 패킷의 첫 번째 바이트가 다르다. RFC 7983이 이 분류 규칙을 정의하고 있다.

  패킷의 첫 번째 바이트 값:

  +------------------+------------------+
  |  0x00 ~ 0x03     |  STUN            |
  +------------------+------------------+
  |  0x14 ~ 0x3F     |  DTLS            |
  +------------------+------------------+
  |  0x80 ~ 0xBF     |  RTP / RTCP      |
  +------------------+------------------+

  ※ 첫 1바이트만 보면 프로토콜을 알 수 있다
  ※ 분류 비용: O(1), 나노초 단위

STUN 패킷은 첫 바이트가 0x00~0x03 범위다. DTLS는 0x14~0x3F. RTP/RTCP는 0x80~0xBF. 범위가 겹치지 않으므로 첫 바이트 하나로 프로토콜을 확정할 수 있다.

이 분류는 극도로 빨라야 한다. 미디어 스트림 60개가 동시에 흐르면 초당 수만 개의 패킷이 들어온다. 패킷 하나를 분류하는 데 마이크로초라도 걸리면 전체 처리량이 무너진다. 첫 바이트 비교는 CPU 명령어 몇 개로 끝나니까 병목이 되지 않는다.


4. RTP와 RTCP를 구분한다

첫 바이트로 STUN과 DTLS는 걸러졌다. 하지만 RTP와 RTCP는 같은 범위(0x80~0xBF)에 있다. 둘을 어떻게 구분하는가?

RTP와 RTCP는 둘 다 미디어 관련 패킷이라 같은 범위에 살고 있다. 구분은 두 번째 바이트로 한다. 여기에 "이 패킷이 뭘 담고 있는지"를 나타내는 번호가 들어 있다 — payload type(PT)이라고 부른다.

  RTP 헤더:
  +------+------+------+------+
  | V=2  |  PT  |  Seq Number |  ...
  +------+------+------+------+
          ↑
          Payload Type으로 구분

  RTCP: PT = 200~206 (SR, RR, BYE 등)
  RTP:  PT = 그 외 (96~127이 일반적)

  ※ PT 값이 200 이상이면 RTCP, 아니면 RTP

RTCP의 payload type은 200번대(200=송신 보고, 201=수신 보고, 205=유실 재전송 요청 같은 제어용 번호)다. 일반 미디어의 payload type(보통 96~127)과 겹치지 않으므로 두 번째 바이트로 구분할 수 있다.


5. 어떤 참여자의 패킷인지 식별한다

프로토콜은 구분했다. SRTP 패킷이라는 건 안다. 그런데 이 패킷이 30명 중 누구의 것인가?

여기서 두 가지 정보를 조합한다:

  • 송신자 주소 (IP:Port): UDP 패킷에는 보낸 사람의 IP와 포트가 있다. NAT가 부여한 공인 주소다. ICE 연결 수립 시점에 이 주소가 등록되므로, 주소로 참여자를 특정할 수 있다.
  • SSRC: 같은 사람이 보내도 오디오와 비디오는 별개의 스트림이다. RTP 헤더에는 이걸 구분하는 고유 번호가 들어있다 — SSRC(Synchronization Source)다. "이건 A의 오디오", "이건 A의 비디오"를 SSRC로 구분한다.
  패킷 식별 흐름:

  UDP 패킷 도착
       |
       v
  1. 첫 바이트 → STUN? DTLS? RTP/RTCP?
       |
       v  (RTP인 경우)
  2. 송신자 IP:Port → 어떤 참여자?
       |
       v
  3. RTP 헤더의 SSRC → 어떤 스트림? (audio / video / screenshare)
       |
       v
  4. 해당 참여자의 SRTP 키로 복호화
       |
       v
  5. 구독자들에게 재암호화 + 복사 전달

이 전체 과정이 패킷 하나마다 일어난다. 초당 수만 번. 그래서 각 단계가 O(1) 조회(해시맵 기반)로 설계되어야 한다.


한눈에 정리

단계판별 기준시간 복잡도
프로토콜 분류첫 바이트 범위O(1)
RTP vs RTCP두 번째 바이트 (PT 값)O(1)
참여자 식별송신자 IP:PortO(1) 해시맵
스트림 식별SSRCO(1) 해시맵
SRTP 키 조회참여자별 키 테이블O(1)

패킷이 포트에 도착해서 모든 수신자에게 복사 전달될 때까지, 모든 판단이 상수 시간이다. 이게 하나의 포트에서 수백 스트림을 처리할 수 있는 구조적 이유다.


SFU 해부학을 마치며

5편에 걸쳐 SFU의 내부를 한 겹씩 벗겨봤다.

  SFU가 하는 일 — 전체 스택:

  +--------------------------------------------------+
  |  Application                                      |
  |  Room 관리, 트랙 관리, Floor Control (PTT)         |
  +--------------------------------------------------+
  |  Signaling (SDP-Free)                 [#4]        |
  |  server_config, PUBLISH_TRACKS, TRACKS_UPDATE     |
  +--------------------------------------------------+
  |  SRTP / SRTCP                         [#3]        |
  |  미디어 암호화, 복호화 → 재암호화, 복사 전달    |
  +--------------------------------------------------+
  |  DTLS                                 [#3]        |
  |  키 교환, 인증서 검증                              |
  +--------------------------------------------------+
  |  ICE-Lite                             [#2]        |
  |  STUN Binding, 연결 유지                           |
  +--------------------------------------------------+
  |  Single-Port UDP Demux                [#5]        |
  |  첫 바이트 분류, 참여자 식별, 스트림 라우팅         |
  +--------------------------------------------------+
  |  UDP Socket (Port 10000)              [#1]        |
  +--------------------------------------------------+

"패킷을 전달만 한다"는 한 문장 뒤에 이만큼의 구조가 숨어 있다. 첫 편에서 SFU를 "택배 허브"에 비유했는데, 택배 허브도 분류 시스템이 없으면 상자를 쌓아놓기만 할 뿐이다.

이 시리즈는 SFU의 "뼈"를 다뤘다. 다음 시리즈에서는 그 뼈 위에 붙는 "근육"을 다룬다 — RTCP로 품질을 관리하고, Simulcast로 대역폭을 최적화하고, PLI로 키프레임을 제어하는, SFU의 실시간 판단 체계를.

다음 시리즈: [RTCP 완전정복 #1] RTCP란 무엇인가 — SR/RR/NACK/PLI 기초


참고 자료


이 글은 OxLens — Rust로 만드는 경량 SFU 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.

profile
Creator of 0xLENS — the world's first AI-native, Rust-built SFU with real-time PTT Floor Control. No fork. No framework. Just Rust and resolve.

0개의 댓글