1. 개요

두 브라우저가 통신해야하는 경우 일반적으로 통신을 조정하고 둘 사이에 메시지를 전달하기 위해 그 사이에 서버가 필요합니다. 그러나 중간에 서버가 있으면 브라우저 간의 통신이 지연됩니다.

이 예제에서는 브라우저와 모바일 애플리케이션이 실시간으로 서로 직접 통신 할 수 있도록 하는 오픈 소스 프로젝트 인 WebRTC에 대해 알아 봅니다 . 그런 다음 두 HTML 클라이언트간에 데이터를 공유하기 위해 피어-투-피어 연결을 생성하는 간단한 애플리케이션을 작성하여 작동하는 모습을 볼 수 있습니다.

클라이언트를 구축하기 위해 웹 브라우저에 내장 된 WebRTC 지원과 함께 HTML, JavaScript, WebSocket 라이브러리를 사용할 것입니다. 그리고 WebSocket을 통신 프로토콜로 사용하여 Spring Boot로 Signaling 서버를 구축 할 것입니다. 마지막으로이 연결에 비디오 및 오디오 스트림을 추가하는 방법을 살펴 보겠습니다.

2. WebRTC의 기본 및 개념

WebRTC가없는 일반적인 시나리오에서 두 브라우저가 어떻게 통신하는지 살펴 보겠습니다.

두 개의 브라우저가 있고 브라우저 1브라우저 2에 메시지를 보내야 한다고 가정 합니다. 브라우저 1은 먼저 서버로 보냅니다 .

 

서버 는 메시지를 수신 한 후 이를 처리하고 Browser 2를 찾은 다음 메시지를 보냅니다.

 

서버는 메시지를 브라우저 2로 보내기 전에 처리해야하기 때문에 거의 실시간으로 통신이 이루어 집니다. 물론 우리는 그것이 실시간 으로 이루어지기  바랍니다.

WebRTC는 두 브라우저 사이에 직접 채널 을 생성하여이 문제를 해결 하므로 서버가 필요하지 않습니다 .

 

결과적으로 메시지가 발신자에서 수신자로 직접 라우팅되므로 한 브라우저에서 다른 브라우저로 메시지를 전달하는 데 걸리는 시간이 크게 단축됩니다 . 또한 서버에서 발생하는 무거운 작업과 대역폭을 제거하고 관련된 클라이언트간에 공유되도록합니다.

3. WebRTC 및 내장 기능 지원

WebRTC는 Chrome, Firefox, Opera 및 Microsoft Edge와 같은 주요 브라우저와 Android 및 iOS와 같은 플랫폼에서 지원됩니다.

WebRTC는 솔루션이 브라우저와 함께 기본 제공되므로 브라우저에 외부 플러그인을 설치할 필요가 없습니다.

또한 비디오 및 오디오 전송을 포함하는 일반적인 실시간 응용 프로그램에서 우리는 C ++ 라이브러리에 크게 의존해야하며 다음과 같은 많은 문제를 처리해야합니다.

  • 패킷 손실 은닉
  • 에코 제거
  • 대역폭 적응성
  • 동적 지터 버퍼링
  • 자동 이득 제어
  • 소음 감소 및 억제
  • 이미지 "청소"

그러나 WebRTC는 이러한 모든 문제를 내부적 으로 처리하여 클라이언트 간의 실시간 통신을 더 간단하게 만듭니다.

4. 피어 투 피어 연결

서버에 대해 알려진 주소가 있고 클라이언트가 이미 통신 할 서버의 주소를 알고있는 클라이언트-서버 통신과 달리 P2P (피어 투 피어) 연결에서는 어떤 피어에도 직접 주소가 없습니다. 다른 피어에게 .

피어-투-피어 연결을 설정하려면 클라이언트가 다음을 수행 할 수 있도록 몇 가지 단계를 수행해야합니다.

  • 의사 소통을 할 수 있도록
  • 서로를 식별하고 네트워크 관련 정보를 공유
  • 관련된 데이터, 모드 및 프로토콜의 형식을 공유하고 동의합니다.
  • 데이터 공유

WebRTC는 이러한 단계를 수행하기위한 API 및 방법론 세트를 정의합니다.

클라이언트가 서로를 검색하고 네트워크 세부 정보를 공유 한 다음 데이터 형식을 공유하기 위해 WebRTC는 신호 라는 메커니즘을 사용합니다 .

5. 시그널링

시그널링은 네트워크 검색, 세션 생성, 세션 관리 및 미디어 기능 메타 데이터 교환과 관련된 프로세스를 나타냅니다.

이는 클라이언트가 통신을 시작하기 위해 서로를 미리 알아야하기 때문에 필수적입니다.

이 모든 것을 달성하기 위해 WebRTC는 시그널링에 대한 표준을 지정하지 않고 개발자의 구현에 맡깁니다. 따라서 이는 모든 기술 및 지원 프로토콜이있는 다양한 장치에서 WebRTC를 사용할 수있는 유연성을 제공합니다.

5.1. 시그널링 서버 구축

시그널링 서버의 경우 Spring Boot를 사용하여 WebSocket 서버를 빌드합니다 . Spring Initializr 에서 생성 된 빈 Spring Boot 프로젝트로 시작할 수 있습니다 .

구현에 WebSocket을 사용하기 위해 pom.xml에 의존성을 추가하겠습니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.4.0</version>
</dependency>

Maven Central 에서 사용할 최신 버전을 항상 찾을 수 있습니다 .

시그널링 서버의 구현은 간단합니다. 클라이언트 애플리케이션이 WebSocket 연결로 등록하는 데 사용할 수있는 엔드 포인트를 만들 것입니다.

Spring Boot에서이를 수행하기 위해 WebSocketConfigurer 를 확장 하고 registerWebSocketHandlers 메서드를 재정의 하는 @Configuration 클래스를 작성해 보겠습니다 .

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketHandler(), "/socket")
          .setAllowedOrigins("*");
    }
}

다음 단계에서 빌드 할 클라이언트에서 등록 할 URL로 / socket식별했습니다 . 또한 SocketHandleraddHandler 메서드에 대한 인수로 전달했습니다. 이것은 실제로 다음에 만들 메시지 처리기입니다.

5.2. 시그널링 서버에서 메시지 핸들러 생성

다음 단계는 여러 클라이언트로부터받을 WebSocket 메시지를 처리하기위한 메시지 핸들러를 만드는 것입니다.

이는 직접 WebRTC 연결을 설정하기 위해 서로 다른 클라이언트 간의 메타 데이터 교환을 지원하는 데 필수적 입니다.

여기서는 간단하게하기 위해 클라이언트로부터 메시지를받을 때 자신을 제외한 다른 모든 클라이언트에게 메시지를 보냅니다.

이를 위해 Spring WebSocket 라이브러리에서 TextWebSocketHandler 확장 하고 handleTextMessage 및  afterConnectionEstablished 메서드를 모두 재정 할 수 있습니다 .

@Component
public class SocketHandler extends TextWebSocketHandler {

    List<WebSocketSession>sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
      throws InterruptedException, IOException {
        for (WebSocketSession webSocketSession : sessions) {
            if (webSocketSession.isOpen() && !session.getId().equals(webSocketSession.getId())) {
                webSocketSession.sendMessage(message);
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }
}

afterConnectionEstablished  메소드 에서 볼 수 있듯이 모든 클라이언트를 추적 할 수 있도록 수신 된 세션을 세션 List에 추가합니다.

그리고 handleTextMessage 에서 볼 수 있듯이 클라이언트로부터 메시지를 받으면 List의 모든 클라이언트 세션을 반복하고 보낸 사람의 세션 ID를 비교하여 보낸 사람을 제외한 다른 모든 클라이언트에게 메시지를 보냅니다. List의 세션.

6. 메타 데이터 교환

P2P 연결에서 클라이언트는 서로 매우 다를 수 있습니다. 예를 들어 Android의 Chrome은 Mac의 Mozilla에 연결할 수 있습니다.

따라서 이러한 장치의 미디어 기능은 매우 다양 할 수 있습니다. 따라서 통신에 사용되는 미디어 유형 및 코덱에 동의하는 피어 간의 핸드 셰이크가 필수적입니다.

이 단계에서 WebRTC는 SDP (Session Description Protocol)를 사용하여 클라이언트 간의 메타 데이터에 동의합니다. 

이를 달성하기 위해 시작 피어는 다른 피어가 원격 설명 자로 설정해야하는 오퍼를 작성합니다. 또한 다른 피어는 시작 피어가 원격 설명 자로 수락하는 응답을 생성합니다.

이 프로세스가 완료되면 연결이 설정됩니다.

7. 클라이언트 설정

시작 피어와 원격 피어 역할을 모두 수행 할 수 있도록 WebRTC 클라이언트를 만들어 보겠습니다.

우리는 HTML 파일이라는 만들어 시작하겠습니다 index.html을 하고 자바 스크립트라는 이름의 파일 client.js  index.html을가  사용됩니다.

시그널링 서버에 연결하기 위해 WebSocket 연결을 생성합니다. 우리가 구축 한 Spring Boot 시그널링 서버가 http : // localhost : 8080 에서 실행되고 있다고 가정하면 연결을 만들 수 있습니다.

var conn = new WebSocket('ws://localhost:8080/socket');

신호 서버로 메시지를 보내기 위해 다음 단계에서 메시지를 전달하는 데 사용할 send 메서드를 만듭니다 .

function send(message) {
    conn.send(JSON.stringify(message));
}

8.  간단한 RTCDataChannel 설정

client.js 에서 클라이언트를 설정 한 후 RTCPeerConnection 클래스에 대한 객체를 만들어야합니다 .

configuration = null;
var peerConnection = new RTCPeerConnection(configuration);

이 예제에서 구성 개체의 목적은 STUN (Session Traversal Utilities for NAT) 및 TURN (Traversal Using Relays around NAT) 서버 및이 사용방법(예제)의 후반부에서 논의 할 기타 구성을 전달하는 것입니다. 이 예에서는 null 을 전달하는 것으로 충분합니다 .

이제 메시지 전달에 사용할 dataChannel만들 수 있습니다 .

var dataChannel = peerConnection.createDataChannel("dataChannel", { reliable: true });

그 후 데이터 채널에서 다양한 이벤트에 대한 리스너를 만들 수 있습니다.

dataChannel.onerror = function(error) {
    console.log("Error:", error);
};
dataChannel.onclose = function() {
    console.log("Data channel is closed");
};

9. ICE와의 연결 설정

WebRTC 연결을 설정하는 다음 단계는 ICE (Interactive Connection Establishment) 및 SDP 프로토콜을 포함하며, 여기서 피어의 세션 설명이 두 피어에서 교환되고 수락됩니다.

신호 서버는 피어간에이 정보를 전송하는 데 사용됩니다. 여기에는 클라이언트가 신호 서버를 통해 연결 메타 데이터를 교환하는 일련의 단계가 포함됩니다.

9.1. 제안 생성

먼저 오퍼를 생성하고 이를 peerConnection 의 로컬 설명으로 설정합니다 . 그런 다음 제안 을 다른 피어 에게 보냅니다 .

peerConnection.createOffer(function(offer) {
    send({
        event : "offer",
        data : offer
    });
    peerConnection.setLocalDescription(offer);
}, function(error) {
    // Handle error here
});

여기서 send 메소드는 오퍼 정보 를 전달하기 위해 시그널링 서버를 호출합니다 .

서버 측 기술로 send 메소드의 로직을 자유롭게 구현할 수 있습니다.

9.2. ICE 후보자 처리

둘째, ICE 후보자를 처리해야합니다. WebRTC는 ICE (Interactive Connection Establishment) 프로토콜을 사용하여 피어를 검색하고 연결을 설정합니다.

peerConnection 에 로컬 설명을 설정하면 icecandidate 이벤트가 트리거됩니다 .

이 이벤트는 원격 피어가 원격 후보 세트에 후보를 추가 할 수 있도록 후보를 원격 피어로 전송해야합니다.

이를 위해 onicecandidate 이벤트에 대한 리스너를 만듭니다 .

peerConnection.onicecandidate = function(event) {
    if (event.candidate) {
        send({
            event : "candidate",
            data : event.candidate
        });
    }
};

icecandidate의 모든 후보가 수집 될 때 이벤트는 빈 후보 문자열을 다시 트리거합니다.

이 후보 객체도 원격 피어에 전달해야합니다. 이 빈 후보 문자열을 전달하여 원격 피어가 모든 icecandidate 객체가 수집 되었음을 알 수 있도록합니다 .

또한 동일한 이벤트가 다시 트리거되어 ICE 후보 수집이 이벤트 에서 null로 설정된 후보 객체 값으로 완료되었음을 나타냅니다 . 이것은 원격 피어로 전달할 필요가 없습니다.

9.3. ICE 후보자 받기

셋째, 다른 피어가 보낸 ICE 후보를 처리해야합니다.

후보 를 수신 한 원격 피어 는 후보를 후보 풀에 추가해야합니다.

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.4. 제안 받기

그 후 다른 피어가 오퍼를 수신하면 이를 원격 설명으로 설정해야합니다 . 또한 응답을 생성해야 하며 이는 시작 피어로 전송됩니다.

peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
peerConnection.createAnswer(function(answer) {
    peerConnection.setLocalDescription(answer);
        send({
            event : "answer",
            data : answer
        });
}, function(error) {
    // Handle error here
});

9.5. 답변 받기

마지막으로, 시작하는 피어는 응답을 받고 원격 설명 으로 설정합니다 .

handleAnswer(answer){
    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

이를 통해 WebRTC는 성공적인 연결을 설정합니다.

이제 시그널링 서버없이 두 피어간에 직접 데이터를주고받을 수 있습니다 .

10. 메시지 보내기

이제 연결을 설정 했으므로 dataChannelsend 메서드를 사용하여 피어간에 메시지를 보낼 수 있습니다 .

dataChannel.send(“message”);

마찬가지로 다른 피어에서 메시지를 수신하려면 onmessage 이벤트에 대한 리스너를 생성 해 보겠습니다 .

dataChannel.onmessage = function(event) {
    console.log("Message:", event.data);
};

데이터 채널에서 메시지를 수신하려면 peerConnection 객체 에 콜백을 추가 해야 합니다.

peerConnection.ondatachannel = function (event) {
    dataChannel = event.channel;
};

이 단계를 통해 우리는 완전한 기능을 갖춘 WebRTC 데이터 채널을 만들었습니다. 이제 클라이언트간에 데이터를 보내고받을 수 있습니다. 또한 여기에 비디오 및 오디오 채널을 추가 할 수 있습니다.

11. 비디오 및 오디오 채널 추가

WebRTC가 P2P 연결을 설정하면 오디오 및 비디오 스트림을 직접 쉽게 전송할 수 있습니다.

11.1. 미디어 스트림 얻기

먼저 브라우저에서 미디어 스트림을 가져와야합니다. WebRTC는이를위한 API를 제공합니다.

const constraints = {
    video: true,audio : true
};
navigator.mediaDevices.getUserMedia(constraints).
  then(function(stream) { /* use the stream */ })
    .catch(function(err) { /* handle the error */ });

constraints 객체를 사용하여 비디오의 프레임 속도, 너비 및 높이를 지정할 수 있습니다.

제약 객체는 또한 모바일 장치의 경우에 사용되는 카메라를 지정할 수 있습니다.

var constraints = {
    video : {
        frameRate : {
            ideal : 10,
            max : 15
        },
        width : 1280,
        height : 720,
        facingMode : "user"
    }
};

또한, 후방 카메라를 활성화하고자한다면 faceMode 의 값을 "user" 대신 "environment" 로 설정할 수 있습니다 .

11.2. 스트림 보내기

둘째, WebRTC 피어 연결 개체에 스트림을 추가해야합니다.

peerConnection.addStream(stream);

피어 연결에 스트림을 추가하면 연결된 피어 에서 addstream 이벤트가 트리거됩니다 .

11.3. 스트림 받기

셋째, 원격 피어에서 스트림을 수신하기 위해 listener를 만들 수 있습니다 .

이 스트림을 HTML 비디오 요소로 설정해 보겠습니다.

peerConnection.onaddstream = function(event) {
    videoElement.srcObject = event.stream;
};

12. NAT 문제

현실 세계에서 방화벽과 NAT (Network Address Traversal) 장치는 우리 장치를 공용 인터넷에 연결합니다.

NAT는 로컬 네트워크 내에서 사용할 IP 주소를 장치에 제공합니다. 따라서이 주소는 로컬 네트워크 외부에서 액세스 할 수 없습니다. 공개 주소가 없으면 동료들은 우리와 소통 할 수 없습니다.

이 문제를 해결하기 위해 WebRTC는 두 가지 메커니즘을 사용합니다.

  1. 충격
  2. 회전

13. STUN 사용

STUN은이 문제에 대한 가장 간단한 접근 방식입니다. 네트워크 정보를 피어와 공유하기 전에 클라이언트는 STUN 서버에 요청을합니다. STUN 서버의 책임은 요청을 수신 한 IP 주소를 반환하는 것입니다.

따라서 STUN 서버를 쿼리하여 공용 IP 주소를 얻습니다. 그런 다음이 IP 및 포트 정보를 연결하려는 피어와 공유합니다. 다른 피어도 동일한 작업을 수행하여 공용 IP를 공유 할 수 있습니다.

STUN 서버를 사용하려면 RTCPeerConnection 객체 를 생성하기 위해 구성 객체에 URL을 전달하면됩니다 .

var configuration = {
    "iceServers" : [ {
        "url" : "stun:stun2.1.google.com:19302"
    } ]
};

14. TURN 사용

반대로 TURN은 WebRTC가 P2P 연결을 설정할 수 없을 때 사용되는 대체 메커니즘입니다. TURN 서버의 역할은 피어간에 직접 데이터를 릴레이하는 것입니다. 이 경우 실제 데이터 스트림은 TURN 서버를 통해 흐릅니다. 기본 구현을 사용하여 TURN 서버는 STUN 서버로도 작동합니다.

TURN 서버는 공개적으로 사용 가능하며 클라이언트는 방화벽이나 프록시 뒤에있는 경우에도 액세스 할 수 있습니다.

그러나 TURN 서버를 사용하는 것은 중간 서버가 있기 때문에 진정한 P2P 연결이 아닙니다.

참고 : TURN은 P2P 연결을 설정할 수없는 경우 마지막 수단입니다. 데이터가 TURN 서버를 통해 흐르기 때문에 많은 대역폭이 필요하며이 경우 P2P를 사용하지 않습니다.

STUN과 유사하게 동일한 구성 객체에 TURN 서버 URL을 제공 할 수 있습니다.

{
  'iceServers': [
    {
      'urls': 'stun:stun.l.google.com:19302'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=udp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=tcp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    }
  ]
}

15. 결론

이 예제에서는 WebRTC 프로젝트가 무엇인지 논의하고 기본 개념을 소개했습니다. 그런 다음 두 HTML 클라이언트간에 데이터를 공유하는 간단한 애플리케이션을 구축했습니다.

또한 WebRTC 연결 생성 및 설정과 관련된 단계에 대해서도 논의했습니다.

또한 WebRTC가 실패 할 때 대체 메커니즘으로 STUN 및 TURN 서버를 사용하는 방법을 조사했습니다.

GitHub 에서이 기사에 제공된 예제를 확인할 수 있습니다  .