
SFU 해부학 시리즈 #5
지금까지 SFU의 연결 과정을 하나씩 따라왔다.
이 모든 게 하나의 UDP 포트에서 일어난다.
30명이 화상회의를 하면, 각자 오디오 1개 + 비디오 1개씩 보낸다. 60개의 미디어 스트림이 서버의 같은 포트로 들어온다. 거기에 ICE 연결을 유지하는 STUN 패킷, 암호화 키를 교환하는 DTLS 패킷, "패킷 3개 유실됐어", "키프레임 보내줘" 같은 제어 메시지(RTCP)까지 섞여 있다. 전부 같은 UDP 포트, 같은 소켓이다.
이 뒤섞인 패킷들을 종류별로 골라내야 한다. 이 분류 작업을 demux(demultiplexing)라고 부른다. 택배 허브에서 컨베이어 벨트 위의 상자를 목적지별로 나누는 것과 같다.
"참여자마다 포트를 따로 쓰면 안 되나?" 당연히 된다. 실제로 초기 VoIP 시스템은 그렇게 했다. 하지만 문제가 있다.
포트 하나로 모든 트래픽을 처리하면 이 문제가 전부 사라진다. 방화벽에서 포트 하나만 열면 되고, 포트포워딩도 하나면 끝이다.
Discord, mediasoup, LiveKit, Janus — 주요 SFU가 전부 Single-Port 방식을 쓴다. 문제는 하나의 포트로 들어오는 온갖 종류의 패킷을 정확하게 분류하는 것이다.
하나의 UDP 포트에는 최소 4종류의 패킷이 들어온다:
UDP 포트 10000으로 들어오는 패킷들:
+------+------+------+------+------+------+------+
| STUN | DTLS | SRTP | SRTP | RTCP | STUN | SRTP |
+------+------+------+------+------+------+------+
| | | | | | |
v v v v v v v
ICE 암호화 A의 B의 품질 ICE C의
연결 핸드 오디오 비디오 제어 유지 오디오
수립 셰이크
※ 전부 같은 소켓, 같은 포트
※ 각 패킷이 무엇인지 어떻게 아는가?
패킷이 도착하면 SFU는 이 4가지 중 무엇인지를 먼저 판단해야 한다. 틀리면 DTLS 패킷을 SRTP로 복호화하려 하거나, STUN 패킷을 미디어로 처리하려는 참사가 벌어진다.
다행히 각 프로토콜은 패킷의 첫 번째 바이트가 다르다. 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 명령어 몇 개로 끝나니까 병목이 되지 않는다.
첫 바이트로 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)과 겹치지 않으므로 두 번째 바이트로 구분할 수 있다.
프로토콜은 구분했다. SRTP 패킷이라는 건 안다. 그런데 이 패킷이 30명 중 누구의 것인가?
여기서 두 가지 정보를 조합한다:
패킷 식별 흐름:
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:Port | O(1) 해시맵 |
| 스트림 식별 | SSRC | O(1) 해시맵 |
| SRTP 키 조회 | 참여자별 키 테이블 | O(1) |
패킷이 포트에 도착해서 모든 수신자에게 복사 전달될 때까지, 모든 판단이 상수 시간이다. 이게 하나의 포트에서 수백 스트림을 처리할 수 있는 구조적 이유다.
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 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.