Algorithm
X기술적 고민
[발견한 버그]
⇒ 위 문제들 local말고 직접 우리끼리도 링크들어가서 다시 테스트 해보기
STUN 서버는 외부 네트워크 주소를 얻는데 사용됩니다.
TURN 서버들은 직접(P2P) 연결이 실패할 경우 트래픽을 중계하는데 사용됩니다.
https://www.html5rocks.com/ko/tutorials/webrtc/infrastructure/
[실험]
Algorithm
XAlgorithm
X
// Overworld.js 539~541 line
//Draw Game Objects
Object.values(charMap)
.sort((a, b) => {
return a.y - b.y;
})
.forEach((object) => {
object.sprite.draw(ctx, cameraPerson);
**const objectNicknameContainer = document.querySelector(
`.${object.id}`
);**
Algorithm
X공부 진행 상황
(Receive)
기본적으로 Send와 동일하나 몇가지 차이점이 있다.
SendTransport 대신 RecvTransport를 사용한다.
produce를 한후 consume을 해야한다. RecvTransport를 통해 consume이 가능하다.
consume을 하게되면 미디어 데이터를 받아오게된다.
getRtpCapabilities
콜백 실행
createDevice
실행
createSendTransport
실행
createSendTransport
에서 createWebRtcTransport
콜백 실행
connectSendTransport
실행
producer = await producerTransport.produce(params)
이 함수를 통해 미디어 스트림을 다른 유저에게 전송
producerTransport
의 connect 이벤트 실행
producerTransport
의 produce 이벤트 실행
getProducers
실행
getProducers
콜백 실행
자신을 제외한 모든 유저들의 producer.id가 들어있는 배열을 파라미터로 받음.
이 안에 forEach 로 signalNewConsumerTransport
를 여러번 호출함.
socket.emit('getProducers', producerIds => {
console.log("getProducers 콜백 실행")
console.log(producerIds)
// for each of the producer create a consumer
// producerIds.forEach(id => signalNewConsumerTransport(id))
producerIds.forEach(signalNewConsumerTransport)
})
signalNewConsumerTransport
실행
signalNewConsumerTransport
에서 createWebRtcTransport
콜백 실행
connectRecvTransport
실행 (device.connectRecvTransport(params) 아님!)
consumerTransport
의 connect 이벤트 실행
이 이벤트 실행 타이밍이 특이하다.
connectRecvTransport에서 이벤트를 등록하고 다음 함수에서 이 이벤트가 불렸을 때 작동하는 것으로 보인다.
&&&&&connect&&&&& 얘는 connect 내부의 콘솔 로그
그리고 마지막에 paintpeerstream
실행
Algorithm
완전탐색(하)마무리기술적 고민
// app.js
const createWebRtcTransport = async (router) => {
return new Promise(async (resolve, reject) => {
try {
// https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
const webRtcTransport_options = {
listenIps: [
{
**ip: '172.20.10.2', // replace with relevant IP address
// announcedIp: '10.0.0.115',**
}
],
enableUdp: true,
enableTcp: true,
preferUdp: true,
}
const createWorker = async () => {
worker = await mediasoup.createWorker({
rtcMinPort: 2000,
rtcMaxPort: 2020, // 4명까지 됨. 인원 증가시 이 부분을 수정
}) // 2100 으로 하면 100개의 전송 수를 받을 수 있게 됨
// overworld.js 수정
// import mediasoupClient from "mediasoup-client";
const mediasoupClient = require('mediasoup-client')
두 명이서 만나면 한 쪽은 connect 후 getProducers()를 실행하고, 한 쪽은 아예 connect도 실행되지 않는다.
user2가 이미 접속해있는 user1을 만났을 때 아무런 실행을 하지 않는 것이 문제.
우선 워커는 서버를 실행시키자 마자 생성 된다.
그림에 쓰여진 원인 2개와 추가로 생각해볼 것 몇개 더
user1이 입장하자마자 sendtransport
가 잘 실행되는지
아니면 user2의 receiver transport가 있어야 실행되는지 → 정상 작동 확인
원인추측 1 : 라우터가 2개(?)다.
- 꼭 2개여야 하는지? 그리고 2개여서 최초 user1과 user2간의 전달이 안되고 있는건지?
- 우선 우리 코드에서 라우터는 createRoom
함수에서 딱 1개씩 만들어지고 있다.
- 그러면 CreateRoom
은 언제 호출되냐?
- 프론트 socket.on("accept_join",
안의 socket.emit('getRtpCapabilities'
에서
백 socket.on('getRtpCapabilities'
로 받고 실행된다.
- 그러면 유저 accept_join시 마다 라우터가 생성되겠네? 콘솔로그로 확인해보자.
→ 그렇다. 유저 1명이 추가 입장 후 기존의 유저를 만날 때 최초 1회 라우터가 생성된다. 즉, 유저 명수 = 라우터 갯수
이게 문제가 되려나?
원인추측 2 : user2가 거쳐가는 라우터에서 Consumer를 못 보내는(?)건지
- 서버에서 Producertransport.produce(params)
를 호출할 때, params의 파라미터 안에 든 video track을 track = getVideotrack 으로 가져오는데, 이 때 트랙이 제대로 추가가 안된 것은 아닐지?
- 빠진 코드 발견
// server informs the client of a new producer just joined
socket.on('new-producer', ({ producerId }) => signalNewConsumerTransport(producerId))
멀다 수정필요 → 클라이언트1 만 전송받는게 아니라, 같은 groupName 에게 전송
Algorithm
이진탐색(하) - 두번째로 입장한 user2의 콘솔창
```jsx
// app.js
const createRoom = async (roomName, socketId) => {
let router1
let peers = []
if (rooms[roomName]) { // let rooms = { roomName1: { Router, rooms: [ sicketId1, ... ] }, ...}
router1 = rooms[roomName].router
peers = rooms[roomName].peers || []
} else {
router1 = await worker.createRouter({ mediaCodecs, })
console.log('~~~Router1 생성 완료다냥~~~');
}
```
if (producersExist) getProducers()
// producersExist -> producer.len>1 ? true : false
// see client's socket.emit('transport-recv-connect', ...)
socket.on('transport-recv-connect', async ({ dtlsParameters, serverConsumerTransportId }) => {
console.log(`DTLS PARAMS: ${dtlsParameters}`)
const consumerTransport = transports.find(transportData => (
transportData.consumer && transportData.transport.id == serverConsumerTransportId
)).transport
await consumerTransport.connect({ dtlsParameters })
})
5차 테스트) 정상 작동!
성능 비교 테스트
성능 비교 방법
성능테스트 회의 내용
[참고사항]
1기 네이버 선배님들이 조언해줬던 것처럼 사진/동영상으로 남기기
클라이언트 cpu 사용량, 서버 cpu 사용량, 트래픽 / 부하
- 한 방에 몇 명까지?
- 3개 방 ⇒ 10명 x 3개 방 = 30명 동시 접속, 동시 화상 통화 가능한지?
- Mesh는 클라이언트니까 상관없음
- SFU일때만 고려하기
[성능 테스트]
한 서버당 몇개의 그룹을 지원하는지? (EC2 기준)
1그룹당 10명 기준. 몇 그룹까지 한 서버로 처리할 수 있는지?
우리가 체크할 것
cpu 사용량? : 인원이 n명일 때 cpu를 얼마나 사용하는지?
- 트래픽
- 서버는 부하. 비용적인 부분은 챌린징이나 선택이유 이야기 시, 업로드/다운로드 속도
- 인원수 증가 → 트래픽 증가 → 서버 여러개 사용 → 비용 증가
- 클라이언트는 업로드/다운로드 속도 부분, CPU사용률
테스트 어떻게할지 정확하게 루틴(?) 정하고, 뭘로 테스트할지 정하자!
SFU+FE 머지해서 브랜치 따로 만들어놓고,
테스트할 사람들 있을 때!
현재 돌아가는 코드 브랜치명
mesh방식 : msworks -
sfu방식 : SFU
서버3개 aws기준 서버 사람 아무도 없어야 돈을 안낸다
그럼 한 서버에 가능한한 많은 사람을 넣어야..
비용 최소화하려면 모든 서버끼리는 룸과 관계없이 서로 통신할 수 있게 되어야함
왜냐면 그룹1, 2가 있을 때 여기 사람들이 순서대로 그룹 1에 1명, 그룹 2에 1명 있고 순서대로 들어올 때 서버에 집어넣으려면 무조건 한 서버에 들어온 순서대로 넣어야 뒤의 서버들을 안쓰게되니까 이런식으로 비용 최소화하려면 스케쥴링을 따로 해줘야한다
서버끼리의 통신 필요. 같은 서버인 것 처럼 보이게 하는게 어려움
[고려사항]
방법 (각각 SFU, Mesh)
그래프로 그려서 1→ 10명, y축 평균 CPU사용량
방법 (각각 SFU, Mesh)
예상
PM2.io
맥 터미널 활성상태보기
사용자 수 10만 테스트 (auxillery)
예시 - 아무거나 붙인거임
Algorithm
이분탐색(하), (중)기술적 고민
socket.emit("reject_join")
의 호출을 받고 있는 함수가 없음을 발견했고, 이 부분에서 max인원이 되었을 때 입장을 거절하는 게 아니라 SFU를 호출하는 방법으로 바꾸면 어떨까? 하는 아이디어.// overworld.js
socket.on("accept_join", async (groupName) => {
await initCall();
switch (connect) {
case "mesh":
const length = userObjArr.length;
if (length === 1) {
return;
}
for (let i = 0; i < length - 1; ++i) {
const newPC = await createConnection(
userObjArr[i].socketId,
userObjArr[i].nickname
);
const offer = await newPC.createOffer();
await newPC.setLocalDescription(offer);
socket.emit(
"offer",
offer,
userObjArr[i].socketId,
userObjArr[i].nickname
);
}
case "SFU":
socket.on("change_SFU", function(data) {
socket.emit('getRtpCapabilities', groupName, (data) => {
console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`)
// we assign to local variable and will be used when
// loading the client Device (see createDevice above)
rtpCapabilities = data.rtpCapabilities
// once we have rtpCapabilities from the Router, create Device
createDevice()
})
})
}
});
socket.on("change_SFU")
함수를 따로 만들어서 이 안에 1~3번째 유저의 connection 변경사항을 조건식으로 포함해 구현해보기로 했다.// overworld.js
socket.on("accept_join", async (userObjArr) => {
try {
// Mesh코드~
const length = userObjArr.length;
if (length === 1) {
return;
}
for (let i = 0; i < length - 1; ++i) {
const newPC = await createConnection(
userObjArr[i].socketId,
userObjArr[i].nickname
);
const offer = await newPC.createOffer();
await newPC.setLocalDescription(offer);
socket.emit(
"offer",
offer,
userObjArr[i].socketId,
userObjArr[i].nickname
);
}
} catch (err) {
console.error(err);
}
});
socket.on("change_SFU", async (userObjArr, groupName) => {
try {
// SFU
await initCall();
// connection 변경
if (userObjArr.length >= 4) {
console.log("4명 이상입니다.")
socket.emit("disconnect")
console.log("disconnect. 현재 명수: ", userObjArr.length)
// socket.emit("change_SFU", groupName)
}
console.log("SFU connect를 시작합니다.")
socket.emit('getRtpCapabilities', groupName, (data) => {
console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`)
// we assign to local variable and will be used when
// loading the client Device (see createDevice above)
rtpCapabilities = data.rtpCapabilities
// once we have rtpCapabilities from the Router, create Device
createDevice()
});
} catch (err) {
console.error(err);
}
});
socket.on("accept_join", async (userObjArr) => {
try {
if (userObjArr.length < 4) {
// Mesh코드~
const length = userObjArr.length;
if (length === 1) {
return;
}
for (let i = 0; i < length - 1; ++i) {
const newPC = await createConnection(
userObjArr[i].socketId,
userObjArr[i].nickname
);
const offer = await newPC.createOffer();
await newPC.setLocalDescription(offer);
socket.emit(
"offer",
offer,
userObjArr[i].socketId,
userObjArr[i].nickname
);
}
} else {
socket.emit("change_SFU". groupName)
}
} catch (err) {
console.error(err);
}
});
socket.on("change_SFU", async (userObjArr, groupName) => {
try {
// SFU
await initCall();
// connection 변경
if (userObjArr.length >= 4) {
console.log("4명 이상입니다.")
socket.to(userObjArr.socketId).emit("disconnect")
console.log("disconnect. 현재 명수: ", userObjArr.length)
// socket.emit("change_SFU", groupName)
}
console.log("SFU connect를 시작합니다.")
socket.emit('getRtpCapabilities', groupName, (data) => {
console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`)
// we assign to local variable and will be used when
// loading the client Device (see createDevice above)
rtpCapabilities = data.rtpCapabilities
// once we have rtpCapabilities from the Router, create Device
createDevice()
});
} catch (err) {
console.error(err);
}
});
host, guest의 그룹 번호가 같습니다.
이것이 caller의 닉네임이다 coco
1 1
host, guest의 그룹 번호가 같습니다.
이것이 caller의 닉네임이다 coco
1 1
host, guest의 그룹 번호가 같습니다.
이것이 caller의 닉네임이다 coco
1 1
host, guest의 그룹 번호가 같습니다.
이것이 caller의 닉네임이다 coco
1 1
host, guest의 그룹 번호가 같습니다.
이것이 caller의 닉네임이다 coco
1 1
socket.on("accept_join", async (userObjArr) => {
try {
if (userObjArr.length < 4) {
// Mesh코드~
const length = userObjArr.length;
if (length === 1) {
return;
}
for (let i = 0; i < length - 1; ++i) {
const newPC = await createConnection(
userObjArr[i].socketId,
userObjArr[i].nickname
);
const offer = await newPC.createOffer();
await newPC.setLocalDescription(offer);
socket.emit(
"offer",
offer,
userObjArr[i].socketId,
userObjArr[i].nickname
);
}
} else {
socket.emit("change_SFU". groupName)
}
} catch (err) {
console.error(err);
}
});
socket.on("change_SFU", async (userObjArr, groupName) => {
try {
// SFU
await initCall();
// connection 변경
if (userObjArr.length >= 4) {
console.log("4명 이상입니다.")
socket.to(userObjArr.socketId).emit("disconnect")
console.log("disconnect. 현재 명수: ", userObjArr.length)
// socket.emit("change_SFU", groupName)
}
console.log("SFU connect를 시작합니다.")
socket.emit('getRtpCapabilities', groupName, (data) => {
console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`)
// we assign to local variable and will be used when
// loading the client Device (see createDevice above)
rtpCapabilities = data.rtpCapabilities
// once we have rtpCapabilities from the Router, create Device
createDevice()
});
} catch (err) {
console.error(err);
}
});