Skip to content
Snippets Groups Projects
Commit e94800de authored by 심재엽's avatar 심재엽
Browse files

feat: 웹소켓 서버 작성 (#9)

parent 4e62421d
Branches
No related tags found
2 merge requests!31Develop,!11[#9] 번개모임, 채팅 로직 구현
const http = require('http');
const crypto = require('crypto');
// const ChatRoom = require('./models/chatRoom.js');
const mongoose = require('mongoose');
const ChatRoom = require('./models/chatRooms');
// WebSocket 관련 데이터
let clients = [];
let chatRooms = {};
// MongoDB 연결 설정
async function connectMongoDB() {
try {
await mongoose.connect('mongodb://localhost:27017/chat', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB에 성공적으로 연결되었습니다.');
// MongoDB 연결 성공 후 WebSocket 서버 시작
startWebSocketServer();
} catch (err) {
console.error('MongoDB 연결 실패:', err);
process.exit(1);
}
}
// 채팅방 기록 불러오기 함수
async function getChatHistory(chatRoomId) {
const chatRoom = await ChatRoom.findOne({ chatRoomId });
return chatRoom ? chatRoom.messages : [];
}
// WebSocket 서버 생성 및 핸드셰이크 처리
function startWebSocketServer() {
const wsServer = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket server is running');
});
wsServer.on('upgrade', (req, socket, head) => {
const key = req.headers['sec-websocket-key'];
const acceptKey = generateAcceptValue(key);
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey}`
];
socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
// 클라이언트를 clients 배열에 추가
clients.push(socket);
let chatRoomId = null;
let nickname = null;
socket.on('data', async buffer => {
let message;
try {
message = parseMessage(buffer);
const parsedData = JSON.parse(message);
const { type, chatRoomId: clientChatRoomId, nickname: clientNickname, text } = parsedData;
console.log('서버에서 수신한 메시지:', { type, clientChatRoomId, clientNickname, text });
if (type === 'join' || type === 'leave') {
await ChatRoom.updateOne(
{ chatRoomId: clientChatRoomId },
{ $set: { [`isOnline.${clientNickname}`]: type === 'join' } }
);
const statusMessage = {
type: 'status',
chatRoomId: clientChatRoomId,
nickname: clientNickname,
isOnline: type === 'join',
};
clients.forEach(client => {
client.write(constructReply(JSON.stringify(statusMessage)));
});
}
if (type === 'join') {
chatRoomId = clientChatRoomId;
nickname = clientNickname;
console.log("join시 chatRoomId", chatRoomId);
console.log("join시 nickname", nickname);
await ChatRoom.updateOne(
{ chatRoomId },
{
$set: {
[`isOnline.${nickname}`]: true,
[`lastReadLogId.${nickname}`]: null,
},
}
);
if (!chatRooms[chatRoomId]) {
chatRooms[chatRoomId] = [];
}
const chatRoom = await ChatRoom.findOne({ chatRoomId });
console.log("join시 chatRoom", chatRoom);
if (!chatRoom) {
console.error(`ChatRoom을 찾을 수 없습니다: chatRoomId = ${chatRoomId}`);
} else {
console.log(`ChatRoom 조회 성공: ${chatRoom}`);
}
const isAlreadyParticipant = chatRoom.participants.includes(nickname);
if (!isAlreadyParticipant) {
const joinMessage = {
message: `${nickname}님이 참가했습니다.`,
timestamp: new Date(),
type: 'join'
};
chatRooms[chatRoomId].push(joinMessage);
await ChatRoom.updateOne({ chatRoomId }, {
$push: { messages: joinMessage, participants: nickname }
});
clients.forEach(client => {
client.write(constructReply(JSON.stringify(joinMessage)));
});
} else {
console.log(`${nickname}은 이미 채팅방에 참가 중입니다.`);
}
try {
const previousMessages = await getChatHistory(chatRoomId);
if (previousMessages.length > 0) {
socket.write(constructReply(JSON.stringify({ type: 'previousMessages', messages: previousMessages })));
console.log(`이전 메시지 전송: ${previousMessages.length}개`);
}
} catch (err) {
console.error('이전 채팅 기록 불러오기 중 오류 발생:', err);
}
} else if (type === 'message') {
const chatMessage = {
message: text,
timestamp: new Date(),
type: 'message',
sender: nickname
};
chatRooms[chatRoomId].push(chatMessage);
try {
// 새로운 메시지를 messages 배열에 추가
const updatedChatRoom = await ChatRoom.findOneAndUpdate(
{ chatRoomId },
{ $push: { messages: chatMessage } },
{ new: true, fields: { "messages": { $slice: -1 } } } // 마지막 추가된 메시지만 가져옴
);
// 마지막에 추가된 메시지의 _id를 가져오기
const savedMessage = updatedChatRoom.messages[updatedChatRoom.messages.length - 1];
// 새로운 메시지 전송: 클라이언트로 메시지 브로드캐스트
const messageData = {
type: 'message',
chatRoomId,
sender: nickname,
message: text,
timestamp: chatMessage.timestamp,
_id: savedMessage._id // 저장된 메시지의 _id 사용
};
clients.forEach(client => {
client.write(constructReply(JSON.stringify(messageData)));
console.log('채팅 메시지 전송:', messageData);
});
} catch (err) {
console.error('MongoDB 채팅 메시지 저장 오류:', err);
}
} else if (type === 'leave') {
const leaveMessage = { message: `${nickname}님이 퇴장했습니다.`, timestamp: new Date() };
chatRooms[chatRoomId].push(leaveMessage);
await ChatRoom.updateOne(
{ chatRoomId },
{ $set: { [`isOnline.${nickname}`]: false } }
);
await ChatRoom.updateOne({ chatRoomId }, {
$push: { messages: leaveMessage },
$pull: { participants: nickname }
});
clients.forEach(client => {
client.write(constructReply(JSON.stringify(leaveMessage)));
});
clients = clients.filter(client => client !== socket);
}
} catch (err) {
console.error('메시지 처리 중 오류 발생:', err);
}
});
socket.on('close', async () => {
if (nickname && chatRoomId) {
await ChatRoom.updateOne(
{ chatRoomId },
{ $set: { [`isOnline.${nickname}`]: false } }
);
const statusMessage = {
type: 'status',
chatRoomId,
nickname,
isOnline: false,
};
clients.forEach(client => {
client.write(constructReply(JSON.stringify(statusMessage)));
});
}
});
socket.on('error', (err) => {
console.error(`WebSocket error: ${err}`);
clients = clients.filter(client => client !== socket);
});
});
wsServer.listen(8081, () => {
console.log('WebSocket 채팅 서버가 8081 포트에서 실행 중입니다.');
});
}
// Sec-WebSocket-Accept 헤더 값 생성 -> env처리
function generateAcceptValue(key) {
return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64');
}
// WebSocket 메시지 파싱 함수
function parseMessage(buffer) {
const byteArray = [...buffer];
const secondByte = byteArray[1];
let length = secondByte & 127;
let maskStart = 2;
if (length === 126) {
length = (byteArray[2] << 8) + byteArray[3];
maskStart = 4;
} else if (length === 127) {
length = 0;
for (let i = 0; i < 8; i++) {
length = (length << 8) + byteArray[2 + i];
}
maskStart = 10;
}
const dataStart = maskStart + 4;
const mask = byteArray.slice(maskStart, dataStart);
const data = byteArray.slice(dataStart, dataStart + length).map((byte, i) => byte ^ mask[i % 4]);
return new TextDecoder('utf-8').decode(Uint8Array.from(data));
}
// 클라이언트 메시지 응답 생성 함수
function constructReply(message) {
const messageBuffer = Buffer.from(message, 'utf-8');
const length = messageBuffer.length;
const reply = [0x81];
if (length < 126) {
reply.push(length);
} else if (length < 65536) {
reply.push(126, (length >> 8) & 255, length & 255);
} else {
reply.push(
127,
(length >> 56) & 255,
(length >> 48) & 255,
(length >> 40) & 255,
(length >> 32) & 255,
(length >> 24) & 255,
(length >> 16) & 255,
(length >> 8) & 255,
length & 255
);
}
return Buffer.concat([Buffer.from(reply), messageBuffer]);
}
// MongoDB 연결 후 WebSocket 서버 시작
connectMongoDB();
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment