오늘한 것
1. 서버 IP 주소 확인
2. 멀티 플레이 구현 준비
3. 시행착오가 있었던 부분 ( 파싱 관련 )
01. 서버 IP 주소 확인
현재에는 프랑크푸르트 서버로 ( 무료 ) koyeb를 이용하고 있다.
따라서 프랑크 프루트 IP 주소로 DB에 접속 가능하게 해 주었더니 오류가 떴다.
일단은 외부 IP도 접속이 가능하게끔 모든 주소를 허용해 두었지만, 최소한의 보안을 위해
현재 서버의 IP주소를 확인하고, 이 주소에서만 접근할 수 있도록 조치할 예정이다.
두 가지 방법이 있다, 외부 모듈을 설치(axios)하는 방법과, 기본적인 https 모듈을 사용하는 방법.
const axios = require('axios');
const https = require('https');
.then((res) => console.log('🟢 현재 Koyeb 서버의 외부 IP(axios):', res.data.ip))
.catch((err) => console.error('IP 확인 실패:', err));
let data = '';
res.on('data', chunk => { data += chunk; });
res.on('end', () => {
console.log('🟢 현재 Koyeb 서버의 외부 IP(https):', JSON.parse(data).ip);
});
둘 모두 같은 결과가 나온다.
위의 IP 주소를 몽고DB의 Network whiteList에 등록해주면 된다.
* 주의 등록한 후 바로 재배포하면 안될 때가 있다. 3~5분의 시간이 지나고 재배포하자 .
02. 멀티플레이 구현 준비
1. 배열이 가변적이다.
2. undefined하면 기본적으로 falsy 값이 나온다 .
const users = require('../models/User');
const { Server } = require('socket.io');
const { v4: uuidv4 } = require('uuid');
// rooms 안에 들어가는 배열 구조
// {roomId: String, userId1: String, userId2: STring, rank: Number}
let rooms = [];
module.exports = function (server) {
const io = new Server(server);
io.on('connection', (socket) => {
console.log("사용자가 연결되었습니다");
socket.on('joinMatch', (grade, userId) => {
console.log(`${userId}가 매칭을 시작했습니다.`);
const matchedRoom = findMatchingRoom(grade);
if (matchedRoom) {
socket.join(matchedRoom.roomId);
socket.emit('joinRoom', { roomId: matchedRoom.roomId });
//먼저 들어와 있는 사람 id1 뒤에 들어온 사람 id2
socket.to(matchedRoom.roomId).emit('startGame', { userId1: matchedRoom.userId1, userId2: userId });
// 매칭된 방 제거
rooms = rooms.filter(room => room.roomId != matchedRoom.roomId);
}
else {
const roomId = uuidv4();
socket.join(roomId);
rooms.push({ roomId, userId1: userId, userId2: null, grade });
socket.emit('createRoom', { roomId });
}
});
socket.on('sendPlayerInfo', (data) => {
const userData = data.userData;
const roomId = data.roomId;
socket.to(roomId).emit('getPlayerInfo', userData);
});
// 여기서부터는 멀티플레이 관련
socket.on('doPlayer', (index, roomId) => {
console.log(`${index}에 돌을 두었습니다.`);
socket.to(roomId).emit('doOpponent', (index));
});
socket.on('sendGameResult', (userId, result) => {
users.findOne({ userId: userId }).then(user => {
if (result == 1) {
user.win += 1;
}
else if(result == 0){
}
else if(result == -1){
user.lose -=1;
}
user.save();
});
});
//TODO: eixtRoom이랑 endGame emit하는 부분
socket.on('disconnect', () => {
console.log('사용자가 연결을 끊었습니다.');
});
});
}
function findMatchingRoom(userGrade) {
const allowGradediff = 1;
return rooms.find(room => Math.abs(room.rank - userGrade) <= allowGradediff);
}
일단 기본적으로 구현해본 코드.
현재에는 grade( 급수 ) 관련 코드가 들어가 있지 않다.
조금 수정해서 급수 관련 코드를 넣었다.
function GradeUpdate(user, result) {
var limit;
if(user.grade == 18 && user.gradeScore == -3 ) return;
if (user.grade == 1 && user.gradeScore == 10 ) return;
if (user.grade <= 4) {
limit = 10;
}
else if (user.grade <= 9) {
limit = 5;
}
else {
limit = 3;
}
if( result == 1){
user.gradeScore++;
if (user.gradeScore > limit){
user.grade++;
user.gradeScore = 0;
}
}
else if ( result == -1){
user.gradeScore--;
if (user.gradeScore < -limit){
user.grade--;
user.gradeScore = 0;
}
}
}
급수를 업데이트하는 코드를 만들고, 이와 같이 넣어준다.
socket.on('sendGameResult', (userId, result) => {
users.findOne({ userId: userId }).then(user => {
if (result == 1) {
user.win += 1;
}
else if (result == 0) {
user.draw += 1;
}
else if (result == -1) {
user.lose -= 1;
}
GradeUpdate(user, result);
user.save();
});
일단은 여기까지...
추가적으로 유저 스키마도 업데이트 해 주었다.
lose와 draw, profileIndex를 넣어주었다.
const userSchema = new mongoose.Schema({
nickName: { type: String, default: '홍길동'},
userId: { type: String, default: 'abcd' },
userPassword: { type: String, default: '1234' },
coin: { type: Number, default: 0 },
win: { type: Number, default: 0 },
draw: { type: Number, default: 0},
lose: { type: Number, default: 0 },
grade: { type: Number, default: 18},
gradeScore: { type: Number, default: 0},
profileIndex: { type: Number, default: 0}
});
이제 팀원 분들과 소통하며 필요한부분을 찾고 있었다.
1. 유저 정보 바꾸기 . ( 프로필 사진이나 닉네임 변경 )
2. 타임아웃 정보 전달 (상대방에게 승리 정보 전달 )
1. 유저 정보 바꾸기
exports.changeUserInfo = async (req, res) => {
try {
if(!req.session.isAuthenticated) res.stauts(404).send('로그인이 필요합니다.');
var userId = req.body.userId;
var nickName = req.body.nickName;
var profileIndex = req.body.profileIndex;
var user = await users.findOne({ userId: userId });
if (user){
if (nickName != null) {
user.nickName = nickName;
}
if (profileIndex != null){
user.profileIndex = profileIndex;
}
await user.save();
res.status(200).send('사용자 정보가 성공적으로 변경되었습니다.');
}
else{
res.status(404).send('없는 유저 ID입니다.');
}
}
catch(err){
console.log('cannot change the user Information '+ err);
res.status(500).send('사용자 정보 변경 중 오류가 발생했습니다');
}
}
세션도 추가했기에 세션을 통해서 했다.
사실 조금 더 정확하게 하려면 자신의 id의 정보만 바꿀 수 있게 하는 것이 좋겠지만, 일단은 패스
2. 타임 아웃 정보 전달
socket.on('timeOut',(isTimeout,roomId)=>{
console.log('상대방의 시간 초과로 승리했습니다.');
socket.to(roomId).emit('timeOutOpponent', (isTimeout));
})
03. 파싱 관련 문제
처음에 데이터를 전달할 때에
_socket.Emit("joinMatch", new {grade});
이렇게 전달했다.
그런데 계속 전달이 되지 않길래 로그를 찍어봤다.
io.on('connection', (socket) => {
console.log("사용자가 연결되었습니다");
socket.on('joinMatch', (data) => {
console.log('사용자가 매칭을 시작했습니다'+ data);
사용자가 매칭을 시작했습니다 라는 로그도 뜨지 않았다, 그럼 joinMatch가 emit이 되지 않은 것인가 ?
했지만, 사용자가 연결되었습니다. 라는 로그는 잘 뜨길래 데이터 쪽에서 문제가 있다고 판단했다.
그래서 확실히 하기 위해 다음과 같이 바꿔 주었다.
_socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));
이렇게 하면 정상적으로 정보를 잘 전달한다.
Emit으로 joinMatch는 전달이 잘 되었지만, 파싱 실패로 인해서 ( 정상적인 JSON 객체가 전달되지 않아서) 이벤트 콜백이 호출되지 않았던 것이다. ( 비정상 구조/배열/문자열로 래핑된 JSON이면 이벤트를 수신해도 콜백을 실행하지 못함).
즉, 데이터 형식 오류로 인해서 이벤트 콜백 함수가 '무시'된 것 처럼 보인 것이다.
이런 문제가 발생한 이유는 SocketIOUnity가 내부적으로 전달할 데이터를 직렬화 하는데, 이 떄 명확한 JSON Object가 아닌 Object[]로 감싼 구조가 되는 버그/제약이 자주 발생해서 그런 것 같다.
* C#에서 익명 객체를 내부적으로 배열로 직렬화하거나, .NET 내부에서 JSON을 자동 감싸는 특성 때문
GPT의 예시를 빌려오자면,
그렇기 떄문에 명확하게 올바른 객체를 전달하는 것이 좋은 것 같다.
결국 해결한 코드는 다음과 같다.
io.on('connection', (socket) => {
console.log("사용자가 연결되었습니다");
socket.on('joinMatch', (data) => {
console.log('사용자가 매칭을 시작했습니다'+ data);
let parsedData;
if (typeof data === 'string') {
try {
parsedData = JSON.parse(data);
} catch (e) {
console.error('데이터 파싱 실패:', e);
parsedData = { grade: 0 };
}
} else {
parsedData = data;
}
const grade = parsedData.grade; // 파싱된
const matchedRoom = findMatchingRoom(grade);
// 이하 생략
public void RequestMatch(int grade)
{
//_socket.Emit("joinMatch", new {grade});
// _socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));
var matchData = new MatchData { grade = grade };
_socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));
}
**** 추가 ****
public void SendMessage(string roomId, string nickName, string message)
{
_socket.Emit("sendMessage", new { roomId, nickName, message });
}
저번에 했던 것을 찾아봤는데, 익명 객체로 보낸다.
socket.on('sendMessage', function(message) {
console.log('메시지를 받았습니다: ' + message.roomId + ' ' + message.nickName + ' : ' + message.message);
socket.to(message.roomId).emit('receiveMessage', { nickName: message.nickName, message: message.message });
});
중간에 실패했던 코드
socket.on('joinMatch', (grade) => {
console.log('사용자가 매칭을 시작했습니다'+ grade);
public void RequestMatch(int grade)
{
//_socket.Emit("joinMatch", new {grade});
// _socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));
// var matchData = new MatchData { grade = grade };
// _socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));
_socket.Emit("joinMatch", grade);
}
기본적으로 '객체'를 전달하는 방식이 기준이라 단일 int를 전달하는 것은 실행되지 않았다.
또 위에서 익명객체를 보냈을 때 잘 되길래 한 번 실험해봤다.
io.on('connection', (socket) => {
console.log("사용자가 연결되었습니다");
socket.on('joinMatch', function(data) {
console.log('사용자가 매칭을 시작했습니다'+ data);
var grade = data.grade2;
console.log('grade는 : ' + grade);
public void RequestMatch(int grade)
{
string grade2 = grade.ToString();
_socket.Emit("joinMatch", new {grade2});
int형이라 안되는건가 싶어 string으로 바꿔서 보내봤는데도 제대로 작동하지 않는다.
시행 착오 부분에 새롭게 정리했다.
멘토님과 함께 많은 시간을 쏟아부어 정리했다. 내가 너무 데이터 형식에만 매몰되어 있어서 생긴 문제도 있었다.
https://febelo0524.tistory.com/172
Unity < - > Node.js Socket.IO 데이터 교환
문제 : 데이터를 전하는 방식이 될 때도 있고 안될 때도 있음 다음은 내가 강사님에게 질문한 전문이다. 안녕하세요 ! 질문이 생겨 질문 드립니다. 강사님이 알려주셨던 코드 중, 메시
febelo0524.tistory.com
자세한 내용은 다음 글 참고 !