-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.js
336 lines (289 loc) · 12.3 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
const express = require('express');
const { createServer } = require('https');
const { WebSocketServer } = require('ws');
const { readFileSync } = require('fs');
const { format, createLogger, transports } = require('winston');
const { randomUUID } = require('crypto');
const { fileURLToPath } = require('url');
const path = require('path');
const { dirname } = require('path');
const compression = require('compression');
const Turn = require('node-turn');
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
const app = express();
const httpsPort = 9443;
const turnPort =3478;
//-------------------- winston 로그 설정 --------------------
// winston 출력 글자 수 제한
const maxLogLength = 100;
// N자 이상은 줄임표로 바꾸는 winston format
const myFormat = format.printf(({ level, message, timestamp }) => {
if (message.length > 100) {
message = message.substring(0, maxLogLength) + '...';
}
return `[${timestamp}]${level}: ${message}`;
});
// winston logger 설정
const logger = createLogger({
level: 'info',
format: format.combine(
format.colorize(),
format.timestamp({
format: 'HH:mm'
}),
myFormat
),
//defaultMeta: { service: 'app' },
transports: [
new transports.Console(),
//new winston.transports.File({ filename: 'app.log' })
]
});
//-------------------- winston 로그 설정 끝 ------------------
// 압축 사용 !!!다른 라우터들보다 먼저 나와야함!!!
app.use(compression({ level: 6 }));
// script,views,asset 폴더 안의 모든 파일에 대한 응답
app.use(express.static(path.join(__dirname, 'dist')));
app.get('/.well-known/assetlinks.json', (req, res) => {
res.sendFile(path.join(__dirname, '/.well-known/assetlinks.json'));
});
// HTTPS 서버 옵션
const options = {
cert: readFileSync('SSL/www.kyj9447.kr-crt.pem', 'utf8'),
key: readFileSync('SSL/www.kyj9447.kr-key.pem', 'utf8')
};
// HTTPS 서버 생성
const httpsServer = createServer(options, app);
// 서버 리스닝 (443)
httpsServer.listen(httpsPort, () => {
logger.info('https server is listening on port '+httpsPort);
});
// 웹소켓 서버 생성
const wss = new WebSocketServer({ server: httpsServer });
// TURN 서버 옵션
const TURNserver = new Turn({
// set options
authMech: 'long-term',
credentials: {
kyj9447: 'kyj0407',
},
listeningPort: turnPort
,debugLevel: 'ERROR'
});
// TURN 서버 시작
TURNserver.start();
logger.info('TURN server is listening on port '+turnPort);
// rooms 리스트
const rooms = [];
const roomMax = 10; // 방 1개당 최대 유저 수
// rooms 리스트 요소 예시
// {
// roomnumber: 'sdfgkgjd1',
// users: [
// ws, // ws.sessionId로 sessionId 사용 가능
// ws, // ws.username로 username 사용 가능
// ws, // ws.room으로 roomnumber 사용 가능
// ws,
// ...
// ]
// },
// {
// roomnumber: '4asdasd',
// users: [
// ws,
// ws,
// ws,
// ...
// ]
// }
// 웹소켓 연결
wss.on('connection', (ws) => {
logger.info('!NEW! (not logged in)');
// sessionId 생성
ws.sessionId = generateSessionId();
// 해당 ws 연결의 room과 user값을 선언
ws.room;
ws.username;
ws.on('message', (message) => {
logger.info(`MESG : ${message}`);
// {type, from, to, data} 형태로 메세지를 보내고 받음
// ex1 login) { type: 'login', from: '', to:'', data: { roomrequest: '1234', username: 'John'} } => to All
// ex2 joined) { type: 'joined', from: '', to:'', data: 'ws.sessionId' } => to me (서버 -> 클라이언트만 발생)
// ex3 err) { type: 'error', from: '', to:'', data: 'error message' } => to me (서버 -> 클라이언트만 발생)
// ex4 offer) { type: 'offer', from: sessionId, to: '', data: 'WebRTC offer' } => to All
// ex5 answer) { type: 'answer', from: sessionId, to: 'ws.sessionId', data: 'WebRTC answer' } => to sessionId (from offer)
// ex6 candidate) { type: 'candidate', from: sessionId, to: 'ws.sessionId', data: 'WebRTC candidate' } => to sessionId (from answer)
// 메세지 파싱
const parsedMessage = JSON.parse(message);
// message의 from(보낸사람)에 sessionId 저장
parsedMessage.from = ws.sessionId;
// 1. login 메세지인 경우
if (parsedMessage.type === 'login') {
// ws에 username 정보 저장
ws.username = parsedMessage.data.username;
logger.info("!NEW USER! username : " + ws.username + " sessionId: " + ws.sessionId);
// rooms 리스트에서 roomnumber==roomrequest인 room을 찾음
// 없으면 undefined, 있으면 해당 room을 반환
const existingRoom = rooms.find((room) => room.roomnumber === parsedMessage.data.roomrequest);
// room이 존재하는 경우
if (existingRoom) {
logger.info("Room 찾음 : " + existingRoom.roomnumber);
// 해당 room의 user수가 가득 찬 경우
if (existingRoom.users.length >= roomMax) {
logger.info("Room 가득 참 : " + existingRoom.roomnumber);
// 에러 메세지 전송
const errorMessage = {
type: 'error',
message: {
errorType: 'full',
errorText: '요청하신 방이 가득 찼습니다.'
}
};
ws.send(JSON.stringify(errorMessage));
return; // ws.on('message') 핸들러 종료 (이후 함수 실행 안함)
}
// 해당 ws객체에 찾은 room 참조
ws.room = existingRoom;
// 해당 room의 users에 user(ws)를 추가
existingRoom.users.push(ws);
}
// room이 존재하지 않는 경우
else {
logger.info("Room 생성 : " + parsedMessage.data.roomrequest);
// 해당 room을 생성하고 해당 room의 users에 user를 추가
const newRoom = {
roomnumber: parsedMessage.data.roomrequest,
users: []
};
newRoom.users.push(ws);
rooms.push(newRoom);
// 해당 ws객체에 새로 만든 room 참조
ws.room = newRoom;
}
// 사용자에게 접속 성공 메세지 전송 (자신의 세션id 전송)
const joinedMessage = {
type: 'joined',
data: ws.sessionId
};
logger.info("SEND : " + JSON.stringify(joinedMessage));
// 본인에게 전송
ws.send(JSON.stringify(joinedMessage));
//해당 room에 존재하는 본인 제외 모든 user에게 login 메세지 전송
sendMessageToAll(ws, parsedMessage);
}
// 2. offer, answer, candidate 메세지인 경우
else if (parsedMessage.type === 'offer' || parsedMessage.type === 'answer' || parsedMessage.type === 'candidate') { // 특정 user에게만 전송
sendMessageToOne(ws, parsedMessage);
}
// 3. 방 번호 중복 체크 메세지인 경우
else if (parsedMessage.type === 'randomCheck') {
const randomroom = rooms.find(room => room.roomnumber === parsedMessage.data)
const randomCheckMessage = {
type: 'randomCheckResult',
data: {
// randomroom이 이미 존재하면 fail, 존재하지 않으면 ok
result: randomroom ? 'fail' : 'ok', // 3항연산자
roomrequest: parsedMessage.data
}
}
// 해당 ws객체는 room,username이 존재하지 않으므로 send 사용
logger.info("SEND : " + JSON.stringify(randomCheckMessage));
ws.send(JSON.stringify(randomCheckMessage));
}
// type이 잘못된 경우
else {
logger.error('Unknown message type : ' + parsedMessage);
}
});
ws.on('close', () => {
// ex6 logout) { type: 'logout', data: {username: ws.username, sessionId: ws.sessionId} } => to All
try { // 정상 종료시 (ws에 sessionId, room, username이 존재)
logger.info('!CLOSE : ' + ws.sessionId);
const logoutMessage = { type: 'logout', data: { username: ws.username, sessionId: ws.sessionId } };
sendMessageToAll(ws, logoutMessage);
// 방 정리
if (ws.room) {
let exitedUser;
// 연결 종료된 사용자의 room에서 index를 찾음
const exitedUserIndex = ws.room.users.findIndex((user) => user.sessionId === ws.sessionId);
// 해당 room의 users에서 해당 user를 삭제
if (exitedUserIndex > -1) {
exitedUser = ws.room.users.splice(exitedUserIndex, 1)[0];
}
// 사용이 끝난 세션 Id 제거
expireSessionId(exitedUser.sessionId)
}
// 모든 rooms 확인 후 해당 room의 user수가 0명인 경우 해당 room을 삭제
rooms.forEach((room, index) => {
if (room.users.length === 0) {
logger.info('Removing room : ' + room.roomnumber);
rooms.splice(index, 1);
}
});
} catch (error) { // 비정상 종료시 (ws연결했으나 방이 가득 찼거나, room 생성에 실패하여 종료하는 등)
logger.error('미등록 사용자 연결 종료됨 : ' + error);
}
});
ws.on('error', (error) => {
logger.error(error);
const errorMessage = { type: 'error', data: error };
ws.send(JSON.stringify(errorMessage));
});
});
// 세션 Id 저장 객체
const UUID = [];
// 랜덤한 세션 Id 생성 함수
function generateSessionId() {
// crypto 라이브러리를 사용하여 랜덤한 세션 Id 생성
const sessionId = randomUUID();
if (UUID.includes(sessionId)) { // UUID에 중복된 세션 Id가 존재하는 경우 재귀호출
return generateSessionId();
}
UUID.push(sessionId); // UUID에 중복된 세션 Id가 없는 경우 UUID에 추가
return sessionId; // 세션 Id 반환
}
// 사용이 끝난 세션 Id 제거 함수
function expireSessionId(SessionId) {
const index = UUID.indexOf(SessionId); // UUID에서 삭제할 세션 Id의 index를 찾음
if (index !== -1) { // UUID에서 삭제할 세션 Id가 존재하는 경우
UUID.splice(index, 1); // UUID에서 삭제할 세션 Id를 삭제
logger.info('expireSessionId: ' + SessionId + ' is expired')
} else {
logger.warn('expireSessionId: SessionId not found'); // 삭제할 UUID가 존재하지 않는 경우 경고
}
}
// 특정 room에 존재하는 모든 user에게 메세지 전송
// !sender는 room 확인용으로만 사용!
function sendMessageToAll(sender, message) {
logger.info('sendMessageToAll : ' + JSON.stringify(message));
const targetRoom = sender.room;
if (targetRoom) {
targetRoom.users.forEach(user => { // 모든 user중
if (user.sessionId !== sender.sessionId) { // 보내는 사람 제외
// 메세지 전송
user.send(JSON.stringify(message));
}
});
}
else {
logger.warn('sendMessageToAll : Room not found');
}
}
// 특정 room에 존재하는 특정 user에게 메세지 전송
// !sender는 room 확인용으로만 사용!
function sendMessageToOne(sender, message) {
logger.info('sendMessageToOne : ' + JSON.stringify(message));
const targetRoom = sender.room;
if (targetRoom) {
targetRoom.users.forEach(user => { // 모든 user중
if (user.sessionId === message.to) { // 특정 대상에게만 전송
// 메세지 전송
user.send(JSON.stringify(message));
}
});
}
else {
logger.warn('sendMessageToOne : Room not found');
}
}