psj2867
webrtc 간단한 설명 본문
계기
웹을 공부하면 실시간 통신에 관해서 욕망이 안 생길수가 없다.
websocket 으로 충족하고 싶지만 서버에 생기는 부담이 과하게 느껴진다.
찾다가 webrtc 가 있었지만 아직 안정화가 덜 되고 복잡해보여 넘겼다.
여유가 생기고 대규모 데이터 구현을 하고싶다 -> 대규모 데이터는 보통 미디어다 media server -> webrtc 같은 신기한 생각으로 구현을 시작했다.
보통 웹켐등의 데이터를 사용하는데 구현 편의를 위해 데이터로는 canvas를 사용했다.
webrtc 설명
아마 다른 글을 이미 보고 왔겠지만 webrtc 스트림 데이터를 서버없이 또는 단순 에코 서버를 통하여 n:n udp 통신이 목적이다.
실제로는 다양한 옵션이 있지만 간단한 설명을 위해 넘긴다.
n:n 통신이 목적이지만 실제로는 1:n 일 경우 1이 n개의 연결을 가진다.
각자 n:n일 경우 각자 n-1개의 통신을 가지면 n:n이다.
실제 코드에서 RTCPeerConnection 을 통신 수만큼 생성한다.
간단한 동작 과정
webrtc 의 목표는 미디어 등의 스트림 데이터를 전송하는 것이다.
이 프로토콜은 크게 두가지 목표가 있는데 어떤 데이터를 보낼 것이고 어떻게 보낼 것인가 이다.
첫째, 어떤 데이터를 보낼 것인가는 미디어 전송을 하는 대부분의 프로토콜이 그렇듯 아래의 과정을 거친다.
- 나는 어떤 미디어 타입을 전송할 수 있고
- 너는 무엇을 받을 수 있니?
- 그럼 가능한 타입을 사용하자
이 협상이 sdp이다.
실제로 코드상에서는 아래와 같이 offer와 answer라고 부르고 각자의 local, remote를 구분해서 저장한다.
const offer = await requestConnection.createOffer();
requestConnection.setLocalDescription(offer);
responseConnection.setRemoteDescription(offer);
const answer = await responseConnection.createAnswer();
responseConnection.setLocalDescription(answer);
requestConnection.setRemoteDescription(answer);
둘째, 어떻게 보낼 것인가는 조금 더 복잡하다.
빠르고 효율적인 통신을 위해서 굳이 서버없이 p2p 통신이 가능하다면 p2p로 통신한다.
대충 우선순위를 말하면 아래와 같다.
- 가능하면 p2p 통신을 한다.
- 클라이언트 둘다 같은 사설 네트워크라면 내부에서 p2p로 통신한다.
- 아니라면 공용 네트워크로 p2p 통신을 한다.
- 안 된다면 에코 서버를 통해서 통신한다.
약간의 네트워크 지식이 필요한데 위를 서로 확인하려면 ip 를 확인하면 된다.
- 자신의 ip를 얻는다, 이는 사설 ip 일 수도 있고 아닐 수도 있다. 이 정보를 상대방에게 보낸다.
- 자신의 공용 ip를 얻는다, 이는 STUN 서버에게 자신의 공용 ip를 물어볼 수 있다. 이 정보를 상대방에게 보낸다.
- 자신의 TURN 서버 ip를 얻는다. TURN 서버는 위의 echo 서버이다. #추후 추가..
위의 내용을 보면 얻어야하는 정보가 많고 비동기적이다.
자신의 사설 ip야 바로 알 수 있지만 공용 ip는 네트워크 요청을 해야하고 TURN 서버 또한 네트워크 요청을 해야한다.
언제 끝날지 모르는 데이터를 묶어서 보내는 것은 비효율적이고 운이 좋다면 사설 ip 만으로 통신이 가능할 수도 있다.
실제로 코드상에서는 candidate로 부르고 얻는 즉시 데이터를 보낸다.
아래 코드는 response 가 직접 받지만 실제 코드에서는 서버를 거쳐서 상대 클라이언트가 받아 addIceCandidate을 실행한다.
requestConnection.onicecandidate = async (e) => {
if (e.candidate) {
responseConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
};
정리하자면 sdp(offer,answer), candidate(양쪽 다수)의 많은 데이터가 상대방에게 sdp는 순차적으로 candidate는 비순차적으로 전달 되어야 한다.
실제로 코드로 보면 아래와 같다.
아래 코드는 같은 화면에서 동작하는 것으로 단순히 localhost로 통신이 이루어진다고 보면 된다.
js 익숙하지 않더라도 대충 await 하면 비동기를 기다리는다는 것만 알면 된다.
// 요청 커넥션, 요청 받을 원격의 커넥션 둘다 생성
const requestConnection = new RTCPeerConnection(configuration);
const responseConnection = new RTCPeerConnection(configuration);
requestConnection.addStream(requestUser.streamSrc);
responseConnection.addStream(responseUser.streamSrc);
// 실제로는 비동기지만 같은 페이지이므로 그때 그때 바로 등록한다.
requestConnection.onicecandidate = async (e) => { // request 에서 생성시에
if (e.candidate) {
// response 에 등록
responseConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
};
responseConnection.onicecandidate = async (e) => { //동일
if (e.candidate) {
requestConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
};
// 연결이 완료되어 mediastream이 확인되면 자신의 video에 추가
requestConnection.ontrack = function (ev) {
if (!requestUser.peers[responseUser.uid]) {
let video = requestUser.addVideo();
video.srcObject = ev.streams[0];
requestUser.peers[responseUser.uid] = video;
}
}
responseConnection.ontrack = function (ev) {
if (!responseUser.peers[requestUser.uid]) {
let video = responseUser.addVideo();
video.srcObject = ev.streams[0];
responseUser.peers[requestUser.uid] = video;
}
}
// request 기준 offer 생성 -> setlocal -> answer 대기 -> setremote
// reponse 기준 offer 대기 -> setremote -> answer 생성 -> setlocal
const offer = await requestConnection.createOffer();
requestConnection.setLocalDescription(offer);
responseConnection.setRemoteDescription(offer);
const answer = await responseConnection.createAnswer();
responseConnection.setLocalDescription(answer);
requestConnection.setRemoteDescription(answer);
유의점
setLocalDescription 등록이 된 후에 자신의 ice 데이터가 수집되면서 onicecandidate 이 호출된다.
setRemoteDescription 이 등록되지 않은 상태에서 candidate가 오면 오류가 난다.
방법은 setLocalDescription 또한 await 할 수 있지만 기다리지 않고 넘어가면 된다.
현제 환경에서 테스트 결과 20ms 안에 처음 onicecandidate가 호출되는데 서버로 정확하게 보장하려면 임시 저장하고 remote 확인 후 저장해야 될 것 같기도 하다.
정리
webrtc를 통하여 데이터가 통신되는 것을 확인가능하고 datachanel 또는 다른 미디어 소스를 추가만 하면 어렵지 않게 연결이 된다.
후기
간단하게 webrtc 동작구조 테스트를 진행해보았는데 생각보다 반응도 빨랐고 만족스러웠다.
추가적으로 TURN 서버 구축이랑 sdp 내용 및 옵션들 더 확인해볼 생각이다.
local 코드는 아래에서
https://github.com/psj2867/webrtc-canvas-sample/blob/master/local-canvas-webrtc.html
server를 통한 통신은 아래에서 확인가능합니다.
https://github.com/psj2867/webrtc-canvas-sample/tree/master
서버는 단순 통신만 해서 아래 코드만 확인해도 어느정도 읽힐 것 같습니다.
https://github.com/psj2867/webrtc-canvas-sample/blob/master/views/index.ejs
'기타' 카테고리의 다른 글
alpine + web component 사용 (1) | 2023.12.21 |
---|---|
printf에 관하여 (0) | 2023.06.15 |
punycode encoding/decoding 알고리즘(bootstring) - 2 (0) | 2023.05.09 |
punycode encoding/decoding 알고리즘(bootstring) (0) | 2023.05.09 |
iptables 정리 (0) | 2023.04.18 |