TJEdu 교육용 서버 · ctrlcv.kr · WebRTC / WSS

WebRTC · WebSocket 사용 설명서

포털로 돌아가기

교육생 사이트에서 채팅, 알림, 화상·음성, 화면 공유를 만들 때 공용 WebSocket 브로커를 시그널링 채널로 사용합니다. 영상·음성 데이터는 서버를 거치지 않고 브라우저끼리(P2P) 주고받습니다.

한 줄 요약

  • 페이지는 반드시 https://아이디.ctrlcv.kr (또는 개인 도메인 HTTPS)
  • 시그널링: wss://아이디.ctrlcv.kr/ws (같은 사이트면 상대경로 /ws)
  • 채널 이름: 아이디-방이름 (다른 교육생과 겹치지 않게)
  • 카메라·마이크는 HTTP·파일 열기(file://)에서 동작하지 않습니다

권장 엔드포인트 wss://아이디.ctrlcv.kr/ws

포털 공통 wss://ctrlcv.kr/ws

개인 도메인 wss://구매도메인/ws (HTTPS 발급된 경우. Cloudflare는 DNS only — 주황 구름 프록시면 WSS가 404·끊김)

사용 금지 ws://ctrlcv.kr:6001 (외부 직접 접속 차단, HTTPS와 혼합되면 브라우저가 막음)

TURN 서버 미제공 · 학원 LAN 안 P2P는 보통 되고, 집↔학원·모바일망은 STUN만으로 실패할 수 있음

1. 교실에서 되는 것 / 안 되는 것

가능

  • 같은 강의실 PC끼리 화상·화면 공유 (P2P)
  • 채팅·접속자 알림 등 WebSocket 메시지
  • HTTPS 학생 사이트·개인 도메인에서 카메라/마이크
  • 여러 팀이 채널 이름만 다르게 동시 실습

주의 · 제한

  • 서버가 영상을 중계하지 않음 (TURN 없음)
  • 집 PC ↔ 학원 PC, LTE/5G는 ICE 실패 가능
  • 채널에 인증이 없음 → 이름 충돌·엿듣기 가능
  • PHP만으로는 WebSocket 서버를 띄울 수 없음 (공용 브로커 사용)

2. HTTPS · 브라우저 권한

3. WebSocket 프로토콜

브로커는 JSON 텍스트만 받습니다. 먼저 채널에 입장(join)한 뒤 메시지를 브로드캐스트합니다. 보낸 사람에게는 다시 전달되지 않고, 같은 채널의 다른 접속자에게만 갑니다.

// 1) 연결 (학생 사이트에서 같은 출처 권장)
const ws = new WebSocket(
  (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'
);

// 2) 채널 입장 — 반드시 본인 아이디를 접두어로
ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'join', channel: '아이디-room1' }));
});

// 3) 수신
ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(ev.data);
  // { type: 'joined', channel, peers }
  // { type: 'message', channel, data }
  console.log(msg);
});

// 4) 송신 (join 이후)
ws.send(JSON.stringify({
  type: 'message',
  data: { kind: 'chat', from: '아이디', text: 'hello' }
}));
보내는 type 필드 응답
join channel (최대 80자) joined + peers(나 제외 인원)
message data (임의 JSON) 다른 클라이언트에 type: message
ping 없음 pong (연결 유지 확인)

누가 들어왔는지는 서버가 알려 주지 않습니다. 입장 후 data.kind = 'hello'를 한 번 보내 서로 존재를 알리세요. 브로커에 로그인 세션은 없습니다. 비밀번호·토큰을 채널로 보내지 마세요.

4. 연결 테스트

이 페이지에서 공용 브로커(/ws)에 접속해 시그널링이 살아 있는지 확인합니다. 카메라 권한은 쓰지 않습니다.

대기
아직 실행하지 않았습니다.

5. WebRTC 시그널링 예제

WebRTC 미디어는 P2P입니다. 서버는 offer / answer / ICE candidate JSON만 전달하면 됩니다. 같은 채널에 두 브라우저(두 탭)를 열고, 한 쪽에서만 polite = false(먼저 offer)로 시작하세요.

const CHANNEL = '아이디-room1';
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' }
  ]
});

pc.onicecandidate = (e) => {
  if (e.candidate) {
    ws.send(JSON.stringify({
      type: 'message',
      data: { kind: 'ice', candidate: e.candidate }
    }));
  }
};

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'join', channel: CHANNEL }));
});

ws.addEventListener('message', async (ev) => {
  const msg = JSON.parse(ev.data);
  const d = msg.data;
  if (!d) return;
  if (d.kind === 'offer') {
    await pc.setRemoteDescription(d.sdp);
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    ws.send(JSON.stringify({
      type: 'message',
      data: { kind: 'answer', sdp: pc.localDescription }
    }));
  } else if (d.kind === 'answer') {
    await pc.setRemoteDescription(d.sdp);
  } else if (d.kind === 'ice' && d.candidate) {
    await pc.addIceCandidate(d.candidate);
  }
});

async function call() {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  stream.getTracks().forEach((t) => pc.addTrack(t, stream));
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  ws.send(JSON.stringify({
    type: 'message',
    data: { kind: 'offer', sdp: pc.localDescription }
  }));
}

화면 공유는 navigator.mediaDevices.getDisplayMedia({ video: true })로 트랙을 얻습니다. 완벽한 1:1 상태 기계(glare 처리)는 수업 예제에서 단계적으로 다루면 됩니다. 위 코드는 시그널링 경로 확인용입니다.

6. STUN · NAT · TURN

7. 자주 나는 오류

증상 원인 · 대처
NotAllowedError / 카메라 없음 HTTP 또는 권한 거부. HTTPS로 열고 자물쇠에서 허용
WebSocket 404 / 301 wss://호스트/ws 인지 확인. :6001 직접 접속은 사용하지 않음. 개인 도메인은 Cloudflare 주황 구름을 끄세요
mixed content 로 WS 차단 HTTPS 페이지에서 ws:// 사용. 반드시 wss://
join은 되는데 상대가 안 보임 채널 철자가 다름. 아이디-room1로 통일. 두 탭 모두 join 후에 offer
시그널링은 되고 영상은 검정 ICE 실패(다른 망·TURN 없음). 같은 강의실에서 재시도. iceconnectionstate 로그 확인
잠시 뒤 소켓이 끊김 서버가 ping을 보냅니다. 그래도 끊기면 onclose에서 재연결

관련 문서: 데이터베이스 API · Git REST API