[PonsWarp 개념 교실 21] 시그널링 — 방 모델과 메시지 라우팅
쉽게 말하면, 시그널링 서버는 파일 운반자가 아니라 두 브라우저를 소개하는 교환원이다.

파일 바이트는 WebRTC DataChannel로 직접 흐르고, 서버는 방에 피어를 등록한 뒤 Offer·Answer·ICE 같은 연결 정보를 올바른 상대에게 전달한다.
방에 들어가는 흐름
GET /ws
↓
peer_id 발급
↓
JoinRoom(room_id)
↓
RoomUsers / PeerJoined
↓
Offer·Answer·ICE 중계
방은 room_id → peer set으로 관리된다. 같은 피어의 중복 JoinRoom은 무시하고, 최대 인원에 도달하면 RoomFull을 반환한다.
target이 있으면 직접, 없으면 방 전체
시그널링 메시지는 두 방식으로 라우팅된다.
rustif let Some(target_id) = target { send_to_peer(&state, target_id, message).await; } else { broadcast_to_room_except(&state, room_id, from_peer_id, message).await; }
Offer·Answer·ICE는 보통 특정 peer를 target으로 한다. Manifest나 TransferReady처럼 방의 다른 피어들이 알아야 하는 메시지는 발신자를 제외하고 broadcast한다.
서버가 파일을 보지 않는 이유
서버가 전달하는 것은 SDP, ICE candidate, manifest 같은 연결·제어 메타데이터다. 실제 파일 조각은 서버 WebSocket을 통과하지 않는다.
이 경계 덕분에 서버 저장공간은 파일 크기에 비례해 커지지 않는다. 대신 연결 실패·방 정리·메시지 크기 제한은 서버가 책임진다.
PonsWarp 적용 지점
| 항목 | 구현 | 증거 |
|---|---|---|
| 방 생성/참여 | JoinRoom, RoomUsers, PeerJoined | handlers/room.rs:8-98 |
| 방 제한 | MAX_ROOM_SIZE 기본 4 | config.rs:461-464 |
| 직접 라우팅 | target peer 전송 | handlers/signaling.rs:21-38 |
| 브로드캐스트 | 발신자 제외 room broadcast | handlers/signaling.rs:35-38 |
| 중복 Join 방지 | 기존 멤버면 PeerJoined 재발행 안 함 | handlers/room.rs:44-66 |
| 방 정리 | timeout 스케줄러 | main.rs:43-50 |
연결 협상과 파일 전송을 분리한다
시그널링 서버가 파일 바이트까지 중계하기 시작하면 서버 비용과 책임이 급격히 커진다. PonsWarp에서 시그널링의 입력은 room_id, peer identity, Offer·Answer·ICE 같은 협상 메시지이고, 출력은 올바른 상대에게 전달된 이벤트다. 파일 payload는 연결이 성립한 뒤 DataChannel로 흐른다.
textpeer A ── JoinRoom ──► signaling room ◄── JoinRoom ── peer B peer A ◄─ Answer/ICE ─┘ └─ Offer/ICE ─► peer B └──────────── WebRTC DataChannel ────────────┘ file bytes
방은 room_id → peer set이라는 수명주기 상태를 가진다. 중복 Join은 같은 peer를 두 번 세지 않아야 하고, 정원 초과는 새 peer를 조용히 끼워 넣지 말고 명시적으로 거부해야 한다. target 메시지는 특정 peer에게만, broadcast는 현재 방의 대상 전체에게 전달한다. 이 라우팅 규칙이 섞이면 연결 협상은 되더라도 엉뚱한 브라우저가 Offer를 받는다.
그냥 서버에서 파일을 받아 보내면 안 되나?
서버 중계는 NAT 문제를 단순화하지만 파일 크기와 동시 접속 수에 비례해 저장·대역폭 비용이 늘고, 서버가 평문을 볼 수 있는 설계가 된다. P2P DataChannel은 모든 환경에서 직접 연결된다는 보장은 없으므로 TURN fallback이 필요하지만, 기본 경로와 fallback을 분리하면 시그널링 서버는 연결 소개 역할에 머문다.
시그널링은 신뢰 채널이 아니다. 메시지 type, sender, target, room membership를 검증해야 하고, 연결이 끝난 peer의 늦은 ICE를 폐기해야 한다. 방을 나간 뒤에도 broadcast 대상에 남아 있으면 정보가 새어 나간다.
| 책임 | 시그널링 | WebRTC |
|---|---|---|
| 상대 소개 | 담당 | 비담당 |
| Offer·Answer·ICE | 중계 | 생성·처리 |
| 파일 payload | 비담당 | 담당 |
| 방 membership | 관리 | 참조 |
| 연결 종료 | 상태 정리 | channel close |
재구축 시에는 방 상태 전이와 파일 전송 상태를 별도 상태 머신으로 둔다. JOINED라고 해서 DataChannel이 OPEN인 것은 아니며, 파일 전송은 양쪽 채널이 열리고 capability가 확인된 뒤에만 시작해야 한다.
라우팅 실패를 다루는 방법
target peer가 이미 방을 나갔거나 아직 등록되지 않았다면 메시지를 버퍼에 무기한 쌓지 않는다. 송신자에게 대상 부재를 알리고, 방 상태를 다시 읽은 뒤 재시도 여부를 결정한다. broadcast도 발신자 자신을 포함할지, system event를 별도 타입으로 보낼지 계약에 넣어야 한다. 메시지 라우터가 단순 map lookup처럼 보여도 이 수명주기 규칙이 실제 안정성을 결정한다.
한 문장 정리
PonsWarp 시그널링 서버는 방과 피어를 관리하며 Offer·Answer·ICE만 전달하고, 파일 데이터는 WebRTC가 직접 운반하게 한다.
관련 개념: [개념 14 — DataChannel] · [구현 13 — 서버가 하는 일과 안 하는 일]
연결 협상과 파일 전송을 분리한다
시그널링 메시지는 파일 데이터가 아니다. Offer/Answer는 SDP 협상 정보이고 ICE candidate는 가능한 네트워크 경로다. 이 정보가 교환되면 두 브라우저가 직접 DataChannel을 만들고, 서버의 파일 역할은 끝난다.
방 상태 전이
text연결 없음 → WebSocket connected → JoinRoom → RoomUsers/PeerJoined → Offer/Answer/ICE → DataChannel connected → 파일 전송
중복 JoinRoom을 무시하는 이유도 중요하다. 재연결 과정에서 같은 피어가 두 번 등록되면 sender가 중복 peer를 만들고 signaling 이벤트가 두 번 전달될 수 있다.
target과 broadcast의 책임
target이 있는 메시지는 특정 peer에게만 보내고, target이 없으면 발신자 외 room 전체에 broadcast한다. 이 구분이 없으면 개인 SDP가 다른 peer에게 노출되거나, 모든 peer가 같은 상태 전이를 반복 처리한다.
방 timeout과 페이로드 상한은 signaling 서버가 파일을 운반하지 않는 대신 책임지는 운영 경계다.