교육생 사이트에서 채팅, 알림, 화상·음성, 화면 공유를 만들 때 공용 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 · 브라우저 권한
getUserMedia(카메라/마이크),getDisplayMedia(화면 공유)는 보안 컨텍스트(HTTPS)에서만 됩니다.- 주소창이
https://아이디.ctrlcv.kr/…인지 확인하세요.http://,tjserver.iptime.org, 탐색기에서 HTML을 더블클릭한file://는 실패합니다. - 권한 팝업에서 허용해야 합니다. 한 번 차단하면 주소창 자물쇠 → 사이트 설정에서 다시 허용합니다.
- iframe 안에 넣을 때는
allow="camera; microphone; display-capture"가 필요합니다.
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
- 시그널링만 이 서버의
/ws를 탑니다. 영상 UDP는 브라우저 ↔ 브라우저입니다. - 학원 공유기 안(같은 LAN)에서는 사설 IP 후보로 연결되는 경우가 많습니다.
- 공인망을 건너려면 STUN으로 자신의 공인 주소:포트를 알아냅니다. 위 예제의 Google STUN을 쓰면 됩니다. 이 서버는 STUN/TURN을 직접 제공하지 않습니다.
- 대칭형 NAT(아이피타임 일부 모드, 모바일망)에서는 STUN만으로 실패합니다. 그때는 TURN(중계)이 필요한데 교육 서버에는 TURN이 없습니다. 과제 시연은 같은 강의실에서 하세요.
- 방화벽이 UDP를 막으면 WebRTC 미디어가 붙지 않습니다. 시그널링(WSS, TCP 443)만 되고 화면이 검은 경우가 이 패턴입니다.
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