Skip to content

Commit

Permalink
Merge pull request #59 from jjikky/refactor/socket
Browse files Browse the repository at this point in the history
시그널링 로직 리팩토링
  • Loading branch information
jjikky authored Sep 10, 2024
2 parents 962abef + c4e45c4 commit f3b51f2
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const fs = require('fs');
const https = require('https');
const config = require('./src/common/config');
const expressLoader = require('./src/common/modules/express');
const socketIoLoader = require('./src/common/modules/socket');
const socketIoLoader = require('./src/socket.loader');
const logger = require('./src/common/modules/logger');

const app = express();
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/common/constants/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ const ErrorMessage = Object.freeze({
NODE_ENV_WRONG:
'NODE_ENV가 올바르게 설정되지 않았습니다. [ production / development / local 중 하나로 설정되어야 합니다.\n 명시되지 않음으로 인해 development로 설정 됩니다.',
});

module.exports = ErrorMessage;
7 changes: 7 additions & 0 deletions src/common/utils/time.util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function getResponseTimeMs(startTime) {
const diff = process.hrtime(startTime);
// 초 단위를 밀리초로 변환하여 소수점 두 자리까지 표시
return (diff[0] * 1e3 + diff[1] * 1e-6).toFixed(2);
}

module.exports = { getResponseTimeMs };
132 changes: 132 additions & 0 deletions src/handler/socket.handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const logger = require('../common/modules/logger');
const redisCli = require('../common/modules/redis');
const config = require('../common/config');
const { getResponseTimeMs } = require('../common/utils/time.util');

const maximum = config.maximumConnection || 9;

const handleObjetJoin = async (io, socket, data, socket_id) => {
const { objet_id, nickname, user_id, profile_image } = data;

const startTime = process.hrtime();
const objetKey = `objet:${objet_id}`;
const socketKey = `socket:${socket_id}`;

logger.info(
`[참여 요청] - 사용자: ${nickname} (ID: ${user_id})가 오브제 ID: ${objet_id}, 소켓 ID: ${socket_id}에 참여 시도, x-request-id: ${socket.xRequestId}`
);

const usersInObjet = await redisCli.lRange(objetKey, 0, -1);

const isUserExist = usersInObjet.map((user) => JSON.parse(user)).find((user) => user.user_id === user_id);

if (isUserExist) {
logger.warn(
`[참여 거부] - 이미 채팅에 참여 중인 사용자: ${nickname} (ID: ${user_id}), 소켓 ID: ${socket_id}, x-request-id: ${socket.xRequestId}`
);
socket.emit('error_message', {
error: { status: 400, message: '이미 음성채팅에 참가중입니다.' },
});
socket.disconnect(true);
return;
}

if (usersInObjet.length >= maximum) {
logger.warn(
`[참여 거부] - 오브제 ID: ${objet_id}, 소켓 ID: ${socket_id}, 채팅방이 가득 찼습니다, x-request-id: ${socket.xRequestId}`
);
socket.emit('error_message', {
error: { status: 403, message: '음성 채팅방이 가득 찼습니다.' },
});
socket.disconnect(true);
return;
}

await redisCli.rPush(objetKey, JSON.stringify({ socket_id, nickname, user_id, profile_image }));
await redisCli.set(socketKey, objet_id);

socket.join(objet_id);
const responseTime = getResponseTimeMs(startTime);
logger.info(
`[참여 성공] - 사용자: ${nickname} (ID: ${user_id})가 오브제 ID: ${objet_id}, 소켓 ID: ${socket_id}에 참여, x-request-id: ${socket.xRequestId}, 응답 시간: ${responseTime}ms`
);

const usersInThisObjet = usersInObjet
.map((user) => JSON.parse(user))
.filter((user) => user.socket_id !== socket_id);

logger.info(`[현재 사용자] - 오브제 ID: ${objet_id}에 있는 사용자 목록, x-request-id: ${socket.xRequestId}`);
logger.info(usersInThisObjet);

io.to(socket_id).emit('all_users', usersInThisObjet);
};

const handleOffer = (socket, data) => {
logger.info(
`[제안] - SDP 제안이 ${data.offerSendID}에서 ${data.offerReceiveID}로 전송됨, x-request-id: ${socket.xRequestId}`
);
socket.to(data.offerReceiveID).emit('getOffer', {
sdp: data.sdp,
offerSendID: data.offerSendID,
offerSendNickname: data.offerSendNickname,
offerSendProfileImage: data.offerSendProfileImage,
});
};

const handleAnswer = (socket, data) => {
logger.info(
`[응답] - SDP 응답이 ${data.answerSendID}에서 ${data.answerReceiveID}로 전송됨, x-request-id: ${socket.xRequestId}`
);
socket.to(data.answerReceiveID).emit('getAnswer', {
sdp: data.sdp,
answerSendID: data.answerSendID,
});
};

const handleCandidate = (socket, data) => {
logger.info(
`[후보] - ICE 후보가 ${data.candidateSendID}에서 ${data.candidateReceiveID}로 전송됨, x-request-id: ${socket.xRequestId}`
);
socket.to(data.candidateReceiveID).emit('getCandidate', {
candidate: data.candidate,
candidateSendID: data.candidateSendID,
});
};

const handleDisconnect = async (socket, socket_id) => {
const socketKey = `socket:${socket_id}`;
const objet_id = await redisCli.get(socketKey);

if (objet_id) {
const objetKey = `objet:${objet_id}`;
let usersInObjet = await redisCli.lRange(objetKey, 0, -1);

usersInObjet = usersInObjet.filter((user) => {
const parsedUser = JSON.parse(user);
return parsedUser.socket_id !== socket_id;
});

await redisCli.del(socketKey);
if (usersInObjet.length > 0) {
await redisCli.del(objetKey);
for (const user of usersInObjet) {
await redisCli.rPush(objetKey, user);
}
} else {
await redisCli.del(objetKey);
}

socket.to(objet_id).emit('user_exit', { socket_id });
logger.info(
`[연결 종료] - 오브제 ID: ${objet_id}에서 사용자 ${socket_id} 연결 종료, x-request-id: ${socket.xRequestId}`
);
}
};

module.exports = {
handleObjetJoin,
handleOffer,
handleAnswer,
handleCandidate,
handleDisconnect,
};
15 changes: 15 additions & 0 deletions src/middleware/handshake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('../common/modules/logger');

module.exports = (socket, next) => {
let xRequestId = socket.handshake.headers['x-request-id'];

if (!xRequestId) {
xRequestId = uuidv4();
logger.info(`x-request-id 생성 : ${xRequestId}`);
}

socket.xRequestId = xRequestId;
logger.info(`x-request-id: ${xRequestId}로 웹소켓 연결 설정`);
next();
};
86 changes: 86 additions & 0 deletions src/socket.loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const axios = require('axios');
const https = require('https');

const config = require('./common/config');
const logger = require('./common/modules/logger');
const { getResponseTimeMs } = require('./common/utils/time.util');
const handshakeMiddleware = require('./middleware/handshake');
const {
handleObjetJoin,
handleOffer,
handleAnswer,
handleCandidate,
handleDisconnect,
} = require('./handler/socket.handler');

module.exports = socketIoLoader = (io) => {
io.use(handshakeMiddleware);

io.on('connection', async (socket) => {
const socket_id = socket.id;
const { token, lounge_id } = socket.handshake.query;

if (token && lounge_id) {
const startTime = process.hrtime();

try {
const response = await axios.post(
`${config.springServerUrl}/objets/signaling`,
{ lounge_id },
{
headers: {
Authorization: `Bearer ${token}`,
'x-request-id': socket.xRequestId,
},
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
}
);
const responseTime = getResponseTimeMs(startTime);
logger.info(
`[연결 성공] - 라운지 ID: ${lounge_id}, 사용자 ID: ${response.data.data}, 소켓 ID: ${socket_id}, x-request-id: ${socket.xRequestId}, 응답 시간: ${responseTime}ms`
);
} catch (err) {
const responseTime = getResponseTimeMs(startTime);
logger.error(
`[연결 오류] - 라운지 ID: ${lounge_id}, 소켓 ID: ${socket_id}, 오류 내용: ${
err.response?.data || err.message
}, x-request-id: ${socket.xRequestId}, 응답 시간: ${responseTime}ms`
);
socket.emit('error_message', {
error: err.response?.data || '알 수 없는 오류',
});
socket.disconnect(true);
return;
}
} else {
logger.warn(`[연결 실패] - 토큰 또는 라운지 ID가 없습니다. 소켓 ID: ${socket_id}`);
socket.emit('error_message', {
error: '토큰 또는 라운지 ID가 없습니다',
});
socket.disconnect(true);
return;
}

socket.on('join_objet', async (data) => {
await handleObjetJoin(io, socket, data, socket_id);
});

socket.on('offer', (data) => {
handleOffer(socket, data);
});

socket.on('answer', (data) => {
handleAnswer(socket, data);
});

socket.on('candidate', (data) => {
handleCandidate(socket, data);
});

socket.on('disconnect', async () => {
await handleDisconnect(socket, socket_id);
});
});
};

0 comments on commit f3b51f2

Please sign in to comment.