최근부터 WebRTC를 이용한 스트리밍 서비스를 만들고 있다. 그래서 WebRTC를 들여다보기 시작했고, 현 시점에는 SFU 서버를 통해 몇만명이 들어올 수 있는 스트리밍 서버를 어느정도 완성했다.
WebRTC의 개념에 대해 다룬 글은 많아도, 실질적인 부분을 다룬 글은 찾아보기 힘들었다. 그래서 이 글에서는 기술 사양을 훑는 대신 WebRTC를 진짜 써보기 전에 알아야 할 배경지식들을 적어보고자 한다.
WebRTC란?
우선 WebRTC는 Web + RTC(Real-Time Communication) 이다. 즉, 웹으로 실시간 통신을 하는 방법이다.
과거에는 웹으로 실시간 영상을 송·수신을 하는 방법이 마땅치 않았다. HLS방식을 통해서 약 6~10초, LL-HLS를 이용하여 3.5초 정도의 딜레이를 가진채 방송하는 방법이 있고, 영상 발신이라면 <Canvas>
등을 이용해 Moving-Picture (Motion JPEG 등)을 만든 후 WebSocket등으로 보내는 방법이 전부였다. 하지만 각각 높은 딜레이와 높은 발신 트래픽(대역폭)이 문제였다.
WebRTC를 사용하면 RTP를 이용해서 영상을 수·발신 할 수 있다. 즉, 실시간으로 영상을 송·수신 할 있다는 것이다.
사실 WebRTC를 쓰지 않는 다른 실시간 방송 기법도 많다. 필자는 과거 WebSocket으로 실시간 방송을 구현 한 적이 있다.
이러한 것들은 MediaSource Extension에 의존하는데, iPhone에서는 MSE를 제대로 지원하지 않는다는게 문제이다.
WebRTC에 포함된 프로토콜
WebRTC는 크게 보면 프로토콜의 집합이다. HTTP를 이용해 영상을 주고 받을수도, 채팅을 만들 수도, 롱풀링, SSE 기법등을 통해서 자신만의 독창적인 프로토콜을 만들 수도 있다. WebRTC도 이와 유사하다. 데이터를 주고 받을수도 있고, 영상을 주고 받을 수도 있다.
WebRTC에는 아래와 같은 프로토콜·기술이 포함 또는 관련되어 있다. 목록을 우선 열거하고, 각각의 배경지식은 후술한다.
시그널링
네트워크 세상에서는 서로간 통신하기 위해서는 상대방의 IP 주소 등의 정보를 알아야 한다.
- STUN (Session Traversal Utilities for NAT): 내 공인 IP주소를 알아내기 위해 쓰인다.
- SDP (Session Description Protocol): WebRTC로 서로간 연결하기 위해서는 내 정보와 상대방의 정보를 알아야 한다. SDP라는 형태에 내 정보가 담겨져 있고, 이것을 상대에게 보낸다.
- ICE: “상대방측과 연결하기 위한 실제 로직”이다. 주어진 SDP를 기반으로 상대방측과 연결을 시도한다. 네트워크를 조금 다루신 분들이라면 “홀펀칭을 실시하고 서로간 연결되게 하는 로직” 으로 이해할 수 있다.
WebRTC는 Peer-to-Peer, 즉 상대방과 직접 IP로 연결한다. 공유기 아래에 있어도 “실제 내 공인 IP”를 알아야 한다. STUN을 통해 공인 IP를 구하고, SDP에 “연결 IP”로서 기록한다. 서로간 SDP를 교환하면 (두개의 SDP가 모이면) ICE Agent가 ICE 로직에 따라 실제 연결을 시도한다.
서로간 연결이 된 상태여도 STUN 메세지는 대충 15초마다 주기적으로 발송해서 연결 상태를 확인·유지한다.
P2P 환경에서는 서로간 연결이 불가능한 경우도 있다. 각 클라이언트가 공유기(NAT) 아래에 있는데, 서로가 Symmetric NAT의 경우라면 홀펀칭이 불가능하기 때문이다. 이때 사용되는게 TURN이다
- TURN (Traversal Using Relays around NAT): 서로간 직결로 연결이 불가능 할 경우 중계서버를 통해 서로간 통신하는 방법.
TURN을 사용한다는 것은 “서로간 P2P로 연결 할 수 없는 환경”에서 눈물을 머금고 내 서버를 중계서버로서 데이터를 거치게 한다는 뜻이 된다.
과거 LTE만 있던 시절에서는 KT LTE 망에서 홀펀칭이 되지 않았던 적이 있다. 지금도 그런지는 모르겠지만, KT LTE 아래에 있던 스마트폰을 위해 따로 TURN을 구현했었다.
데이터 전송
시그널링이 끝나면 서로 교신이 가능한 상태이다. TCP위에서 HTTP가 돌아가는 것 처럼, 실제 데이터 송신에는 SCTP 또는 (RTP + RTCP)가 동작한다.
- RTP: 영상 정보를 전송하는데 쓰인다. RTP위에서 H264, VP8, Opus등의 미디어 정보가 전송된다
- RTCP: RTP를 제어하는데 쓰인다. 영상에 깨져서 나오면 키프레임을 요청하기 위해
PLI
,FIR
을 보내거나, 영상의 재생 상태를 피드백용으로 서로 교신한다. - SCTP: 영상을 제외한 텍스트, 데이터를 주고 받는데 쓰인다. (DataChannel에 해당한다)
WebRTC의 예시 코드를 보면 WebRTC 연결을 만들고 나서 DataChannel
또는 RTPStream
을 만든다. DataChannel
또는 RTPStream
을 위해 내부적으로 별개의 프로토콜이 동작한다. 후술하겠지만 브라우저 또는 서버는 패킷내 식별번호
를 통해서 SCTP이면 DataChannel로, RTP(RTCP)면 영상 처리로 분기한다.
UDP는 Stream 방식이 아니기 때문에 엄밀하게는 “연결되어 있는 상태”라고 할 수는 없다.
대신 “서로간 UDP로 메세지를 주고 받을 수 있음직한” 상태이다.
데이터 암호화 계층
HTTP에서는 HTTPS라는 이름으로 데이터가 암호화 된다. HTTPS 기저에서 SSL, TLS라는것이 실제 암호화를 담당한다. WebRTC도 DTLS/SRTP로 통신 암호화를 진행한다.
- DTLS: Datagram TLS이다. 즉, UDP 기반의 SSL이다. Data Channel 사용시 DTLS가 사용된다.
- SRTP: Secure RTP이다. 즉, RTP를 암호화 한 통신이다. 영상 데이터는 SRTP를 통해 데이터가 전송된다.
시그널링 (STUN/SDP/ICE)까지는 암호화 없이 동작한다.
ICE가 끝나고 상대와 실제로 연결된 시점에 Client Hello, Server Hello를 보내서 인증서와 비밀키를 교환하고 암호화 통신을 시작한다. 키 교환 이후 AES128 또는 AES256이 사용된다.
이해하기 쉬운 예시
HTTP에 맞춰서 생각해 보자. 처음에 STUN
을 사용해서 내 실제 IP를 알아낸다. SDP
에 내 정보를 담아서 상대에게 보내고, 상대에게서도 SDP
정보를 받는다. (이것을 중계해 주는게 시그널 서버이다.) 이 정보를 토대로 서로간 교신을 시도한다. 교신 시도는 ICE
라는 이름으로 기술된 순서(로직)에 따라 이루어진다.
일단 연결되면 L4(UDP/TCP단)에서 통신이 되는 상태이다. 이제 암호화 통신을 위해 DTLS
/SRTP
를 준비한다. DTLS
/SRTP
는 SSL(TLS) 통신과 유사하게 Client Hello-Server Hello를 비롯한 Handshake를 거친다.
DTLS
/SRTP
가 열린 위에서 SCTP
/RTP
(RTCP
)로 실제 데이터를 수·발신한다. TCP – SSL(TLS) – HTTP와 같이 레이어화 되어 있다.
홀 펀칭이란
필자가 네트워크에 관심이 있다고, 네트워크를 배우고 싶다고 하는 사람에게 꼭 하는 말이 있다. “우선 UDP 홀펀칭과 HTTP 서버를 Socket을 직접 여는것 부터 시작해서 처음부터 짜봐라”는 것이다. 그만큼 홀펀칭·NAT 개념은 중요하다. 이것이 무엇인지 알아야 STUN의 필요성과 TURN 서버의 필요성이 파악된다.
일반적인 클라이언트-서버 구조를 생각해 보자. 서버는 어떤 방법이든 클라이언트가 접속할 길을 열어둔다. 공인 ip를 가지고 있을수도, 포트포워딩을 할 수도, Cloudflare Tunnel와 같은 방법을 사용할 수도 있다.
그러나 P2P에서는 상대방으로의 접근이 보장되지 않는다. 서로가 스마트폰이고, 공유기(NAT) 아래에 있다고 생각해 보자. 일반적인 상황이라면 포트포워딩을 해야지 접속이 가능할 것이다. 그러나 사용자에게 포트포워딩을 강요할 수 없다.
그래서 나온것이 홀펀칭이다. 전체를 설명하면 너무 길어지니까 요약을 하자면, 외부로 접속을 시도하면 공유기(NAT)에서 “내 사설 IP:내 포트
” -> “상대편 IP:상대편 포트
“로 접속했다고 저장한다. 그래야지 돌아오는 응답을 어느 컴퓨터·휴대폰으로 넘겨줄 지 알 수 있다.
휴대폰 -- [192.168.0.10:8888 => 121.144.252.70:80] --> 공유기
공유기 -- [111.222.111.222:8888 => 121.144.252.70:80] --> 실제 서버
* SNAT으로 source를 실제 IP로 변환하고, NAT Entry에 추가함
공유기는 돌아올 응답 패킷을 위해 데이터를 저장함 (stateful conntrack)
(1) 공유기에 111.222.111.222:8888로 들어오는 데이터는 모두 192.168.0.10:8888에 들어가도록 저장함: Full Cone
(2) 121.144.252.70 에서 111.222.111.222:8888로 보내는 데이터는 모두 192.168.0.10:8888에 들어가도록 저장함: Restricted Cone
(3) 121.144.252.70:80 에서 111.222.111.222:8888로 보내는 데이터는 모두 192.168.0.10:8888에 들어가도록 저장함: Port Restricted Cone
실제 서버 -- [121.144.252.70:80 => 111.222.111.222:8888] --> 공유기
공유기 --[111.222.111.222:8888 ==> 192.168.10:8888] --> 휴대폰
이 개념을 활용하면 “일단 되든 말든 상대측 IP:Port
에 STUN 을 쏴봐”가 된다. 내가 상대에게 보낸SDP
에 나의 IP주소(STUN Server Reflective IP):Port
가 있고, 상대가 나한테 보내준 SDP
에 상대 IP주소:Port
가 있다. 주소를 서로 알기 시작하면 서로가 상대측 주소로 연결이 뚫릴때 까지 UDP를 쏜다. 그러면 서로간의 공유기(NAT)에 “내 Port
로 상대측 IP:Port
패킷이 오면 지정된 휴대폰으로 보내야겠다” 라는 정보가 새겨진다.
서로간의 SDP는 사전에 HTTPS 또는 Websocket 등으로 먼저 교환한다. 상대의 주소를 알아야 P2P 연결을 할 수 있기 때문이다.
이 방법으로 공유기(NAT)에 서로간 통신할 수 있는 구멍을 뚫는 것을 홀펀칭
이라고 한다. 서로가 “상대방 서버로 접속하고 싶어요”라고 UDP를 보내면 공유기(NAT)에서 상대 주소를 받아드릴수 있게 된다.
단, 서로간 Symmetric NAT
라면 작동되지 않는다.
조금 주제가 벗어나서 부록으로 적어보자면, 왠만한 NAT는 Source Port를 그대로 사용한다. 즉, 패킷의 출발지 주소가 192.168.0.3:1234
라면 왠만해선 공인 IP만 바꾼 111.222.111.222:1234
를 사용한다. 그리고 1234
포트를 192.168.0.3
에 할당해 준다.
그렇다면 이미 다른 컴퓨터가 해당 포트를 사용한다면 어떨까? 192.168.0.2:1234
가 먼저 포트를 선점했다고 하자. 그러면 192.168.0.3:1234
는 111.222.111.222:1235
로 연결될 가능성이 크다.
중요한 포인트는, 홀펀칭은 NAT가 출발지 IP주소:포트
만 같다면 동일한 외부 포트를 할당해 줄 것이라는 것을 기저에 깔고 진행된다는 것이다. 출발지 IP주소:포트
-> 121.144.252.70:80
이든 출발지 IP주소:포트
-> 118.112.119.111:8080
상관없이, 출발지 IP주소:포트
가 같기 때문에 포트가 변환됐을지라도 두개 모두 동일한 SRC 포트를 쓸 것이라는 기대를 하고 만든 방법이다.
그렇지 않고 출발지 IP주소:포트
가 같아도 상대측 주소(121.144.252.70:80
, 118.112.119.111:8080
)에 따라 다른 SRC 포트를 할당하는 NAT를 Symmetric NAT
이라고 하며, 한쪽이라도 Symmetric NAT
를 가지고 Port Restricted Cone
이상의 제약이 있다면 서로간 직결이 불가능하다. 그때는 TURN으로 패킷 중계를 해야할 수 밖에 없다.
왠만한 NAT는 P2P를 고려하기 때문에 Symmetric NAT
를 쓰지 않는다. Symmetric NAT
를 쓰면 사용자가 P2P로 통신하는 게임·프로그램이 작동하지 않는다고 불만을 표할 가능성이 매우 높다. 그래서라도 왠만한 공유기(NAT)는 출발지 IP주소:포트
가 같다면 동일한 외부 포트를 제공할 가능성이 높다. 그럼에도 만에 하나라도 Symmetric NAT
라면 TURN을 써서 통신 중계를 해줘야 한다.
NAT가 가능한 동일한 소스 포트를 쓰려는 성질을 Port Preservation
이라고 한다.
포트 충돌시(Port Collision
)에도 내부 규칙에 따라서 새로운 포트를 할당하려고 한다. HashMap에서 Bucket 충돌시 이를 피하려는 움직임을 생각하면 좋다.
NAT는 특정 시간이 지나면 연결 정보를 삭제한다. 무한정 포트를 할당해 줄 수는 없기 때문이다. 시간은 NAT(공유기) 마다 다르다.
약 15초마다 STUN을 발신하는 이유에 NAT 정보를 유지하려는 것도 있다.
Server Reflexive Candidate (srflx)
나한테 연결할 때 쓸 수 있을만한 IP주소:포트
를 Candidate (후보) 라고 한다. 할당된 IP 주소가 많을수록 Candidate는 많을 것이다. 그 중, 외부 서버가 확인해준 IP가 기제된 후보를 Server Reflexive Candidate라고 한다.
인터넷에서 상대가 나를 접속하려면 공인 IP를 알아야 한다. 그러나 내 컴퓨터는 나에게 할당된 사설 IP밖에 알지 못한다. 그래서 외부 STUN 서버에 “내 IP가 뭐에요?” 라고 물어본다. 그러면 STUN 서버는 “너의 공인 IP
는 111.222.111.222
이고, 너의 SRC 포트
는 1234
이다” 라고 답해준다.

STUN에는 자체는 Request/Response 밖에 없다. Binding Request
속에 “내 IP가 뭐에요?”라고 물어보면 (MAPPED-ADDRES
) 내 IP를 응답해 준다. 없다면 응답은 없다.
WebRTC에서 상대와 연결하기 위해 보내는 경우에는 MAPPED-ADDRESS
요소를 넣지 않는다. 대신 아래에 후술할 Credential이 포함된다.
이때, STUN 서버에서 보인 IP, 포트를 (STUN) Server reflexive address
라고 한다. 한글로 직번역 하면 “서버에서 비춰진(보여진) IP주소” 이다.
공유기 아래에 … 공유기 아래에 … 공유기 아래에 … n개의 공유기가 있더라도 STUN 서버로 패킷이 나가면서 각자의 NAT에 기록이 생긴다. 그렇기 때문에, 최종적으로 나온 공인 IP 주소:포트번호
로 패킷을 보내면 역방향으로 돌아가서 내 PC에 들어온다.
인트라넷 속에서도 NAT(공유기)가 있다면 STUN이 필요하긴 하다. 엄밀하게 따지면 STUN 서버가 관측한 IP가 공인 IP는 아닐 수 있다.
SDP
네트워크 IP가 여러개라면, 나에게 접속하는데 쓸 수 있는 IP 주소:포트
도 여러개 일 것이다. 이것을 후보라고 하고, 후보가 여러개이므로 후보군
이 된다. 상대측에게 IP 주소:포트
목록을 넘겨줄 때 사용되는 형식이 SDP이다.
[SDP 구조 작성예정]
SDP에는 후보군 설명만 있는것이 아니다. 연결에 관한 다른 정보도 있다. 예를 들어, DataChannel
로 데이터를 주고 받는다고 하자. SDP내에 DataChannel
에 관한 정보가 쓰여져 있다. 영상·음성도 마찬가지이다.
즉, SDP는 실제 통신을 하기위해 필요한 정보를 교환하는 데이터 구조라고 생각하면 된다. 이것을 파싱해서 연결에 필요한 정보, 데이터 송·수신에 필요한 트랙·스트림 정보를 서로간 교환한다.
SDP에는 영상 코덱정보도 들어간다. SDP없이 데이터를 쏜다면 이 트랙이 뭘 의미하는지 등등도 파악할 수 없다.
m-line, a-line
[글 작성 예정]
내부 NAT 테이블
연결 이후에도 STUN이 발송되는데는 이유가 있다. 우선 UDP는 TCP와 달리 ACK를 기대할 수 없다. 즉, 연결이 끊겼는지 여부를 판단할 수 없다. 마지막 STUN이 언제인지만으로 연결이 끊겼는지를 판단할 수 있다.
간접적으로는 효율적인 처리를 위해 내부적으로 EndPoint와 WebRTC 연결 객체를 이어주는데 쓰이기 때문이다. 리눅스 커널이 5-Tuple을 가져서 정확한 fd에 데이터를 넣어주는 것 처럼 우리도 어플리케이션 단에서 5-Tuple을 유지할 필요가 있다.
// Candidate가 여러개일 수 있다. 그러므로 N:1로 구성해야 한다.
HashMap<(srcIpAddr, srcPort, dstIpAddr, dstIpPort, Protocol), WebRTC_ClientId>
HashMap<WebRTC_ClientId, WebRTC_Client>
위와 같이 5-tuple을 관리하는 객체를 만들어야 대량의 처리를 효율적으로 받을 수 있다. 이때, STUN과 최초 SDP에서 교환한 Candidate가 근거가 된다. STUN이 heartbeat 처럼 계속 온다면 연결된 객체를 유지하고, n초 이상 오지 않는다면 객체를 정리하면 된다.
물론 TCP를 안쓰고 UDP만 사용하며 + 서버의 인터페이스 하나만을 사용한다면 2-Tuple로도 구성 가능 할 것이다.
새로운 IP로 STUN이 오는 경우가 있을 수 있다. 예를 들어, LTE, 5G 같은 네트워크에서 통신사의 외부 IP가 바뀔 수 있다. 이 경우에는 SDP의 Candidate에 해당 IP가 있을 경우에만 5-Tuple Table에 등록해야 한다.
SDP는 재교섭이 가능하다. 이에 대한 대비를 해 놓자. (맨 아래 모바일 네트워크 섹션 참고)
STUN의 ufrag
STUN은 상대방에게 “나 계속 접속하고 싶어요”를 알리는 용도라고 했다.그렇다면 STUN은 어떻게 믿을 수 있을까? 나쁜 공격자가 IP를 위조해서 UDP를 쏠 수도 있다.
STUN에는 ufrag와 ice-password라는것이 있다. 각각은 최초 SDP 교환시에 서로 교환한다. ufrag
는 “사용자 ID”이고, ice-pwd
는 메세지 검증을 위한 HMAC Key로 생각하면 된다. 예시 코드로 보자면 다음과 같이 데이터를 보내는 셈이다
var myUfrag = "Ufrag12345";
var remoteUfrag = "aabbccdd";
var remoteIcePwd = "asdf12345";
var transactionManager = new TranscationManager();
var stunMessage = StunMessage.builder().MessageType(type)
// 상대입장에서 Local : Remote 형식으로 배치돼야 함.
// 그러려면 우리가 Remote:Local로 보내야 함.
.addAttribute("USERNAME", remoteUfrag + ":" + myUfrag).
.addAtrribute(" ~~~ ", ~~~ )
.addTransactionId(transactionManager.generate());
stunMessage.addAtrribute(
"MESSAGE-INTEGRITY",
HMAC_SHA1(stunMessage.data(), remoteIcePwd)
);
ufrag는 공개되지만, MESSAGE-INTEGRITY
는 누군가가 위조할 수 없어야 한다. 그렇기에 처음에 SDP를 교환할 때 ice-pwd
라는 항목으로 서로 자신의 HMAC 키를 상대에게 알려준다.
정리하자면 SDP에 단순히 트랙정보, 연결 후보(Candidate) 뿐 아니라 ufrag와 ice-pwd도 있다.
그렇기에 SDP 교환은 안전한 HTTPS 등등 위에서 이루어져야 한다. 최초 SDP는 MITM이 안된다는 전제하에 WebRTC가 쓰여져있다.

createOffer
로 각각 SDP를 생성할 때 ice-pwd
, ice-ufrag
라는 이름으로 ufrag와 hash salt를 알려줌을 볼 수 있다.당연하게도 상대측 SDP에도 상대측의 ufrag
와 ice-pwd
가 담겨있다. ufrag
와 ice-pwd
를 합쳐서 Credential
이라고 종종 부르고, 내껏과 상대의 것 각각을 Local Crendtial (또는 local ufrag)
, Remote Credential
이라고 부른다. 상대측이 보낸 STUN 메세지가 제대로 됐는지를 remote credential
(상대 SDP에 적인 ufrag와 ice-pwd)를 이용해서 반드시 확인하자.
DTLS
UDP라고 해도 데이터는 암호화 되어 전송돼야 한다. 그렇다면 TLS/SSL Handshake는 어떻게 시작할까? 이것 또한 SDP에서 부터 시작한다. 아래 사진과 같이 SDP에 a=fingerprint:sha256
이라는 항목이 있다. 이것이 DTLS 인증서의 fingerprint 이다.

우선 한가지 알고 가야하는것이 있다. DTLS는 Self-Signed 인증서를 허용한다. 실제로도 모두가 Self-Signed 인증서를 쓰고있다.
Self-Signed 인증서의 문제는 “상대방이 생성한 인증서가 맞는가?”를 검증할 수 없다는데 있다. 악의적인 사용자가 MITM을 해서 자신의 인증서를 끼어넣을 수 있기에 브라우저들이 막는다. 그러나, SDP에 인증서의 Figerprint가 기제되므로서 상대방이 생성한 인증서가 맞음을 확인할 수 있다.
그래서 Self-Signed 안전함이 확보된 이상 문제가 없다. 오히려 매번 키가 바뀜으로서 더 안전하다고 까지 한다. 인증서의 유효기간 문제도 있기에, 많은 클라이언트들이 DTLS용 인증서를 요청이 온 순간순간 만들어서 쓴다.
보안적으로는 forward secrecy 유지가 된다는 장점이 있다.
매번 Self-Signed 인증서를 생성하는게 보안상 더 낫고, 유효기간 문제가 있음에도 미리 키를 생성할 생각이라면 다음 명령어를 사용하자. 주의할 것은 CN=WebRTC
를 설정해야 호환성 문제가 없다는 것과, 인증서 유효 기간을 잘 관리해야 한다는 점이다.
# 1. Generate RSA 2048-bit private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out dtls.key
# 2. Generate a unique serial number
SERIAL=$(openssl rand -hex 16)
# 3. Create self-signed certificate
openssl req -new -x509 -sha256 \
-key dtls.key \
-out dtls.crt \
-days 7 \
-subj "/CN=WebRTC" \
-set_serial 0x$SERIAL
실제 Handshake 과정은 TLS의 Handshake와 동일하다. 패킷이 궁금하면 다음 섹션[(버그) Firefox는 DTLS의 Out-of-order Arrival이 지원되지 않음]의 Wireshark 패킷 캡쳐를 확인하자.
패킷 단위 암호화
WebRTC는 UDP를 기본으로 사용한다. TCP와 달리 UDP는 더더욱 패킷 단위이다. 데이터를 수신받을때도 패킷 크기만큼 읽히고, 보낼때도 데이터의 크기만큼의 패킷이 생성된다.
TCP에서는 한번의 read()
시 버퍼에 쌓여있는 데이터가 한번에 들어오지만, UDP에서는 패킷 단위로 읽힌다. write()
시에도 TCP는 데이터를 wmem에 쌓아두고 softirq가 적절한 크기의 패킷으로 짤라서 알아서 발송한다.
UDP 소켓에서 데이터를 읽고 쓰는 것은 하부에서 recvmsg
, sendto
가 돌아간다는 뜻이다. 한번의 syscall시 packet 크기 만큼만 읽히고 쓰인다.
하지만 UDP에서는 “순서 보장이 없기 때문에” read()
시 패킷 크기만큼만 읽힌다. All-or-Nothing 처럼 무조건 Packet단위로는 읽힌다는 제약덕에 UDP 소켓을 실제 쓸 수 있다. 만약 버퍼가 있다면 한번 읽었을때 순서가 뒤바뀐채 데이터 구분이 안될 것이다. write()
시에도 무조건 데이터 크기만큼이 보내진다. (단, IP의 Fragment는 현재 고려하지 않는다.)
RTP의 max packet size가 1200인것도 패킷단위 수·발신이 기본이며, MSS(MTU) 이상의 데이터는 전송 불가하다는 제약 때문에 존재한다.
그렇기 때문에, 암호화도 패킷 단위로 발생한다. TCP였으면 1GB파일 전송시 1GB를 블록으로 암호화 하면 됐다. 그러나 WebRTC에서는 근본적으로 불가능하기 때문에 패킷 단위로 암호화를 해야한다. 1500 MTU (=1480 MSS) 기준으로 6800번 정도의 암호 연산이 발생한다.
사실 이것이 WebRTC 서버가 CPU 지원을 많이 필요로 하는 이유가 된다. 영상을 전송한다면 초당 300~500 패킷은 발송된다. 뷰어가 1천명만 있다 해도 초당 400 * 1000의 암호화가 필요한 셈이다. 그 외, RTCP 응답 복호화 등등이 발생하면 더 많은 CPU가 소모된다.
TCP는 프로토콜(L4)단에서 Out-of-order 재조립이 된다. 그래서 HTTP 등과 상관없이 암호화를 통째로 보내도 된다.
그러나 UDP L4단에서는 그런게 없다.대신 어플리케이션 단에서 Reliable-UDP를 구현할 수 있다. 실제로 WebRTC에서는 DataChannel
에서 Ordered
, RTP
은 Sequence No
로 순서보장을 한다.
버그) Firefox는 DTLS의 Out-of-order Arrival이 지원되지 않음
UDP는 순서보장이 되지 않는다. 매우 중요하고 놓치기 쉬운 특성이다. Chrome에서는 DTLS Handshake 패킷 순서가 섞여서 들어와도 문제가 없다. 그러나 Firefox에서는 순서가 섞이면 DTLS 수립에 실패한다.

크롬은 위와 같이 Server Hello, Server Certification, Server Hello Done이 섞여서 들어와도 된다.

그러나 파이어폭스는 이들 중 하나의 순서만 바뀌어도 DTLS 수립이 되지 않는다. Firefox뿐 아니라 다른 간단한 WebRTC 클라이언트들도 동일한 문제가 있을 것이다. 그러므로, DTLS의 경우 Jitter 이상의 간격을 두고 Handshake 패킷을 전송하는 로직이 추가로 필요하다.
Firefox 같이 큰 프로그램에서도 UDP의 Out-of-order 처리가 미흡한 경우가 생긴다.
직접 WebRTC 서버를 만든다면 반드시 Out-of-order에 대한 고려를 해야한다.

위와같이 순서 보장을 넣으면 Firefox에서도 잘 작동한다.
SFU vs M2U vs P2P
이까지 읽은 분들이라면 이제 SFU vs M2U vs P2P라는 그림에서 벗어날 수 있다. 그리고 벗어나야 한다. 어쨋든 WebRTC는 서로간 직접 연결할 수 있는 밑바탕이다. 그리고 그 위에 RTP를 이용해서 영상을 송수신 하거나, SCTP를 통해 데이터를 송수신 할 수 있다.
이때 서버측이 지정한 RTP 스트림을 원하는 상대에게 쏘면 개념상 SFU가 된다. 여러개의 RTP 스트림을 서버에서 합성한 뒤 상대에게 쏘면 M2U가 된다. 브라우저끼리 연결되어 있다면 P2P이다. WebRTC 라는 근간은 같다. 그리고 RTP에 무엇을 쏠 것인지만 생각하면 된다. 일부 영상은 합성해서 쏘고 일부 영상은 그대로 forward(전달)만 할 수도 있다. 어쨋든 뷰어가 보는것은 RTP 스트림이다.
그러므로, SFU, M2U, P2P에 집착하지 말고 본질에 대해 조금 더 생각하자. 그러면 더 나에게 맞는 구조를 채택할 수 있다.
~ 더 나아가기 ~
WebRTC 자체의 개념 정립은 어느정도 됐을 것이다. 이제 WebRTC와 연결된 것들과 실제 운영시 참고할 것들을 알아야 한다.
최적화
sendmmsg, recvmmsg
[글 작성 예정]
ethtool (nic) queue
[글 작성 예정]
ifconfig queue
[글 작성 예정]
wmem, rmem
[글 작성 예정]
일련의 순서
[글 작성 예정 : nic queue -> irq (irq temp disable) -> sofirq (NAPI / net.core.netdev_budget_usecs / net.core.netdev_budget -> rmem / 역방향]
모바일 네트워크 (LTE, 5G)
괴담이 있다. LTE, 5G 네트워크는 조금만 움직여도 IP가 계속 바뀐다는 것이다. 필자는 큰 지역(광역시)을 벗어나면 IP가 바뀌지 않을까 내심 걱정했었다. 몇일간 테스트 해 본 결과에 따르면, 큰 걱정은 할 필요 없다는 것이 필자의 판단이다.
일반적인 HTTP 기반의 Stateless 통신의 경우 IP 변경이 큰 문제가 아니다. HLS 기반 스트리밍도 “요청”이라는 기준이 존재하기 때문에, IP가 바뀌어도 다음 요청을 쓰면 된다.
KT에서는 (1) 부산에서 살다가 (2) 부산-서울을 KTX를 타고 와서 (3) 서울(노량진)에서 지내고 (4) 홍대쪽에 출·퇴근을 하는동안 IP가 동일했다. 옛날 같았으면 각 지역별 모바일 백본의 경계를 넘어갔을때 IP가 바뀌었을지 모르겠지만, 이제는 그렇지 않다.
LG에서는 서울 전역을 돌아다녀도 IP가 바뀌지 않음을 확인 할 수 있었다. 테스트 구간에 지하철을 포함해서 [홍대입구 -(2호선)- 당산역 -(9호선)- 노량진역 – 용산]을 업무차 몇번 왕복 했는데 동일한 IP가 유지 됐다.
단, KT와 LG U+모두 LTE를 껏다켜면 새 IP가 할당되었다.
다들 KTX나 고속도로에서도 모바일 게임을 잘 하지 않는가. 온라인 게임 중 stateful한 게임도 꽤 있을것인데 영향이 없다는것은 걱정할 필요가 없다는 반증이다.
다만, KTX 및 고속도로에서는 기지국 Handover 과정에서 수초간 네트워크 상태가 좋지 못했다. HLS나 버퍼 관리가 가능한 곳에서는 buffer time을 길게 가져가면 되는데 비해, WebRTC는 버퍼기간을 직접 관리하기 힘들다. 이에 대한 유저 불만은 조심하자.
Handover에서도 IP가 유지되는 대신에 IPv4/v6 모두 TCP MSS가 1378b 밖에 되지 않았다. 일반적으로 쓰이는 1460 MSS (=1500 MTU) 보다 훨씬 낮을 뿐더러 UDP이니 패킷 크기 관리를 잘 해야 한다.
Handover 중에는 Packet Drop도 발생했다. IP-Endpoint가 유지된다 수준으로만 받아드리자.
다만, SKT에 한해서 큰 지역을 벗어나면 IP가 바뀌는 현상이 있긴 했다. 홍대 입구역에서 파주로 넘어가기 위해 강변북로를 올라타자 IP의 끝자리가 바뀌었다. 반대로 파주에서 강변북로를 타고 들어올 때 합정역 즈음에서 IP의 끝자리가 바뀌었다.
[홍대] 111.222.111.10 --> [강변북로] 111.222.111.40 --> [파주] 111.222.111.40
[파주] 111.222.111.40 --> [합정역 부근] 111.222.111.10
그래도 바뀌는 IP는 매번 동일하다. 매일 출퇴근을 해도 계속해서 동일한 IP로 바뀌는것으로 보아 SKT는 “지역별 백본·IP” 같은것이 있는것 같다. 아마 서울내 지역별 IP와 고양·파주쪽 IP가 분리되어 있는것 같다.
LTE를 껏다켜도 동일한 IP가 유지되었다. 지역별 IP가 존재한다는 가설에 힘을 붙인 이유이다. 또는, 적어도 IP 부여 규칙이 있는것 같다.
IP가 바뀌면?
STUN으로 새 IP를 보낸다고 되지 않는다. 상대측에게 내 IP를 알려줘야 한다. 보안을 위해서라도 후보군(Candidate
)에 등록되지 않은 IP로 메세지가 온다면 무시할 수 밖에 없다.
앞서 설명한 것 처럼, 연결에 필요한 정보들은 SDP에 기록되어 있다. 그러므로, 새 IP가 적혀있는 후보군 목록을 SDP로 포장해서 다시 상대방에게 전달해 줘야 한다.
// ICE Restart가 있으면 다시 처음부터 재교섭을 한다.
const offer = await rtc.createOffer({ iceRestart: true });
// Offer를 나한테 적용하고 상대에게도 보내서 re-negotiation을 진행한다.
await rtc.setLocalDescription(offer);
sendToRemote(offer);
계속된 연결로 인한 데이터 과금 주의
LTE를 쓰다가 와이파이에 접속한 환경을 생각해 보자. HTTP라면 다음 요청부터는 LTE에서 Wi-Fi로 요청이 나갈 것이다. 요청 단위마다 새 연결이 가능하기 때문이다. HLS일때에도 .mkv
, .ts
파일을 새로 받아올 때 Wi-Fi로 전환될 것이다. 그러나 WebRTC는 처음 접속했을때 썻던 LTE를 그대로 계속 사용한다.
Wifi를 켜도 마찬가지이다. 인텔리젼스 Wi-Fi의 “Wi-Fi 연결시에도 LTE 켜기” 기능을 끈 상태임에도 불구하고, (1) LTE에서 WebRTC를 연결한 상태에서 (2) Wi-Fi에 접속도 (=) LTE를 통해 WebRTC 통신이 이어져나갔다.
사용자 입장에서는 Wi-Fi를 켰음에도 데이터가 소진되는 경험을 하게 된다. 화면 상단의 LTE 신호세기 아이콘도 사라져서 더 알아체기 힘들다. 심지어는 필자도 “데이터 사용량 안내 문자”가 와서 알았다.
테스트 기기는 [삼성 겔럭시 Z폴드 3] 이다.
LTE를 이용해서 WebRTC를 접속했다면, Wi-Fi 네트워크에 연결해도 WebRTC는 LTE로 계속 연결된다. 사용자 입장에서는 끊김이야 없겠다만은 데이터 사용료가 계속 발생할 수 있다. 이 부분을 충분히 사용자에게 알려주자.
답글 남기기