diff --git a/wsServer.js b/wsServer.js new file mode 100644 index 0000000000000000000000000000000000000000..3f303b5db4c2b4aadd965b7adead567e547a4efc --- /dev/null +++ b/wsServer.js @@ -0,0 +1,296 @@ +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