WebRTC 소개

WebRTC는 웹 브라우저 상에서 실시간 p2p 통신을 지원하는 표준 라이브러리입니다.

WebRTC의 동작 과정

WebRTC의 동작 과정

PeerConnection

피어 간 연결을 위해선 한 쌍의 RTCPeerConnection 객체들이 짝을 이뤄야 합니다. 짝을 이룬 두 객체 중 한 객체는 송신 역할만 가능하고, 다른 객체는 수신 역할만 가능합니다. 이에 대한 역할을 명확하게 구분하고자 새로운 클래스로 재정의하였습니다.

PeerConnection 클래스 계층도

PeerConnection 클래스 계층도

두 클라이언트들이 서로 양방향 통신(bidirectional communication)을 하기 위해선 두 쌍의 PeerConnection이 필요합니다.

양방향 통신(bidirectional communication) 시 PeerConnection들

양방향 통신(bidirectional communication) 시 PeerConnection들

하지만, 클라이언트들의 수가 3개 이상일 때부터 문제가 발생합니다. 클라이언트가 관리하는 LocalPeerConnectionRemotePeerConnection이 어떤 클라이언트와 연결되어 있는지 구분하기 어려워집니다.

세 클라이언트들의 연결

세 클라이언트들의 연결

이를 위해 각 Connection에 대한 식별자를 도입하고, Map 구조를 이용해 관리하는 PeerConnectionManager를 도입했습니다.

export default class PeerConnectionManager {
  private _localId: string = "";
  private _localPeerConnMap: Map<string, LocalPeerConnection>;
  private _remotePeerConnMap: Map<string, RemotePeerConnection>;
  
  // ...
}

Signaling 서버

Signaling 서버는 피어들 사이의 SDP(Session Description Protocol)를 교환해주는 별도의 서버입니다. 일반적으로 HTTP 기반 웹 API로 구현됩니다. 이번에는 FastAPI 기반의 WebSocket 서버를 구현했습니다.

Connection 식별자 관리

앞서 클라이언트들의 Connection에 대한 식별자가 필요하다는 것을 언급했습니다. 이 식별자에 대한 관리는 Signaling 서버에서 담당합니다.

from typing import Any

from fastapi import WebSocket

from managers.exceptions import ConnIdAlreadyExists
from utils.generators import generate_random_digit_char_string

class ConnectionManager:
    def __init__(self):
        self.connections: dict[str, WebSocket] = {}

    def generate_next_id(self):
        ids: list[str] = self.get_conn_ids()
        while True:
            new_id: str = generate_random_digit_char_string()
            if new_id not in ids:
                return new_id

    def get_conn_ids(self):
        return list(self.connections.keys())

    async def connect(self, conn_id: str, websocket: WebSocket):
        if conn_id in self.connections.keys():
            raise ConnIdAlreadyExists()
        self.connections.update({conn_id: websocket})
        await websocket.accept()

    def disconnect(self, conn_id: str):
        self.connections.pop(conn_id)

    async def send_personal_data(self, data: Any, conn_id: str):
        await self.connections.get(conn_id).send_json(data)

    async def broadcast_except_sender(self, data: Any, sender_conn_id: str):
        for [conn_id, conn] in self.connections.items():
            if conn_id == sender_conn_id:
                continue
            await conn.send_json(data)

    async def broadcast(self, data: Any):
        for conn in self.connections.values():
            await conn.send_json(data)

다중 클라이언트 접속을 위한 커스텀 프로토콜

image.png