constWebSocket=require('ws');module.exports= (server) => {// 웹소켓 서버constwss=newWebSocket.Server({server});// wss (웹소켓서버)에 리스너(ws.on)을 등록. 이벤트 기반// connection -> 클라이언트가 서버와 웹소켓 연결을 맺을 때 발생wss.on('connection', (ws, req) => {/* 클라이언트의 IP를 알아내는 방법 (자주사용 되므로 알아두기) 크롬 로컬 접속 : IP -> ::1 cf. IP 알아내는 방법 : express -> proxy-addr 패키지를 활용 하기도함 */constip=req.headers['x-forwarded-for'] ||req.connection.remoteAddress;/* 3개의 이벤트 리스너 등록 - message : 클라이언트 -> 메세지 요청이 왔을 때 실행 - error : 웹소켓 연결 중 문제 발생시 실행 - close : 클라이언트와 연결이 끊겼을 때 실행 */console.log('새로운 클라이언트 접속', ip);ws.on('message', (message) => {console.log(message); });ws.on('error', (error) => {console.error(error); });ws.on('close', () => {console.log('클라이언트 접속 해재', ip); });/* 3초마다 연결된 모든 클라이언트에 메시지를 보내는 이벤트 4가지 상태 존재 - OPEN, CONNECTION, CLOSING, CLOSED */constinterval=setInterval(() => {if (ws.readyState ===ws.OPEN) {ws.send('서버에서 클라이언트로 메시지를 보냅니다.'); } },3000);ws.interval = interval;});};
index.pug
doctype
html
head
meta(charset='utf-8')
title GIF 채팅방
body
div F12를 눌러 console 탭과 network 탭을 확인하세요.
script.
var webSocket = new WebSocket("ws://localhost:8005");
webSocket.onopen = function () {
console.log('서버와 웹 소켓 연결 성공 !');
};
webSocket.onmessage = function (event) {
console.log(event.data);
webSocket.send('클라이언트에서 서버로 답장을 보냅니다.');
};
11.3 socket.io 사용하기
ws 모듈 : ws프로토콜 사용
socket.io 모듈 : http 프로토콜 사용. 약간 Vue.js 양방향 통신의 이벤트 기반과 프로그래밍 패러다임이 비슷
클라 path 옵션 -> 서버의 path 옵션과 동일해야 통신이 가능
socket.io는 최초 폴링 방식으로 접근. 브라우저가 웹소켓을 지원하면 웹소켓으로 업그레이드하여 사용하고, 지원하지 않는 경우 폴링방식으로 유지.
app.js
...constwebSocket=require('./socket2'); // socket2.js 로 변경...
routes/index.js
...router.get('/', (req, res) => {res.render('index2'); // index -> index2 로 변경});...
/*
클라이언트의 IP를 알아내는 방법 (자주사용 되므로 알아두기)
크롬 로컬 접속 : IP -> ::1
cf. IP 알아내는 방법 : express -> proxy-addr 패키지를 활용 하기도함
*/
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
/*
3개의 이벤트 리스너 등록
*/
console.log('새로운 클라이언트 접속', ip, socket.id, req.ip);
socket.on('reply', (data) => { // 사용자 정의 이벤트, ws 모듈과 다른점
console.log(data);
});
socket.on('error', (error) => {
console.error(error);
});
socket.on('disconnect', () => {
console.log('클라이언트 접속 해재', ip, socket.id);
});
/*
3초마다 연결된 모든 클라이언트에 메시지를 보내는 이벤트
*/
socket.interval = setInterval(() => {
socket.emit('news', 'Hello Socket.IO'); // emit(이벤트 이름, 데이터)
}, 3000);
}); };
- index2.pug
```pug
doctype
html
head
meta(charset='utf-8')
title GIF 채팅방
body
div F12를 눌러 console 탭과 network 탭을 확인하세요.
script(src='/socket.io/socket.io.js')
script.
var socket = io.connect("http://localhost:8005", {
path: '/socket.io'
}
);
socket.on('news', function (data) { // 서버로 부터 데이버 받기 (news 이벤트 등록 및 정의)
console.log(data);
socket.emit('reply', 'Hello Node.JS'); // 서버로 데이터 보내기 (서버에 reply 이벤트 등록 필요)
});
11.4 실시간 GIF 채팅방 만들기
GIF 파일을 올릴 수 있는 채팅방 만들기
몽고디비
몽구스
multer, axios : 이미지 업로드 -> 서버에 Http요청
네임스페이스 : URL 접근시. RESTful api 같은 .. 처럼 처리
socket.request.headers.referer로 URL 정보추출. url 정보에서 방 아이디를 추출 하여 구분
// 방 생성 이벤트 등록
socket.on('newRoom', function (data) {
var tr = document.createElement('tr');
var td = document.createElement('td');
td.textContent = data.title;
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.password ? '비밀방' : '공개방';
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.max;
tr.appendChild(td);
td = document.createElement('td');
td.style.color = data.owner;
td.textContent = data.owner;
tr.appendChild(td);
td = document.createElement('td');
var button = document.createElement('button');
button.textContent = '입장';
button.dataset.password = data.password ? 'true' : 'false';
button.dataset.id = data._id;
button.addEventListener('click', addBtnEvent);
td.appendChild(button);
tr.appendChild(td);
tr.dataset.id = data._id;
document.querySelector('table tbody').appendChild(tr);
});
// 방 삭제 이벤트 등록
socket.on('removeRoom', function (data) {
Array.prototype.forEach.call(document.querySelectorAll('tbody tr'), function (tr) {
if (tr.dataset.id === data) {
tr.parentNode.removeChild(tr);
}
});
});
function addBtnEvent(e) {
if (e.target.dataset.password === 'true') {
const password = prompt('비밀번호를 입력하세요!');
location.href = '/room/' + e.target.dataset.id + '?password=' + password;
} else {
location.href = '/room/' + e.target.dataset.id;
}
}
Array.prototype.forEach.call(document.querySelectorAll('.join-btn'), function (btn) {
btn.addEventListener('click', addBtnEvent);
});
// layout.pug (block : jsp include 같은 기능) doctype html head meta(charset='utf-8') title=title link(rel='stylesheet' href='/main.css') body block content
// chat.pug (채팅 뷰, 핵심) extends layout
block content h1= title a#exit-btn(href='/') 방 나가기 fieldset legend 채팅 내용
#chat-list
for chat in chats
if chat.user === user
.mine(style='color:' + chat.user)
div= chat.user
if chat.gif
img(src='/gif/' + chat.gif)
else
div= chat.chat
else if chat.user === 'system'
.system
div= chat.chat
else
.other(style='color:' + chat.user)
div= chat.user
if chat.gif
img(src='/gif/' + chat.gif)
else
div= chat.chat
form#chat-form(action='/chat' method='post' enctype='multipart/form-data')
label(for='gif') GIF 올리기
input#gif(type='file' name='gif' accept='image/gif')
input#chat(name='chat')
button(type='submit') 전송
script(src='/socket.io/socket.io.js')
script.
var socket = io.connect('http://localhost:8005/chat', {
path: '/socket.io'
});
socket.on('join', function (data) {
var div = document.createElement('div');
div.classList.add('system');
var chat = document.createElement('div');
div.textContent = data.chat;
div.appendChild(chat);
document.querySelector('#chat-list').appendChild(div);
});
socket.on('exit', function (data) {
var div = document.createElement('div');
div.classList.add('system');
var chat = document.createElement('div');
div.textContent = data.chat;
div.appendChild(chat);
document.querySelector('#chat-list').appendChild(div);
});
socket.on('chat', function (data) {
var div = document.createElement('div');
if (data.user === '#{user}') {
div.classList.add('mine');
} else {
div.classList.add('other');
}
var name = document.createElement('div');
name.textContent = data.user;
div.appendChild(name);
if (data.chat) {
var chat = document.createElement('div');
chat.textContent = data.chat;
div.appendChild(chat);
} else {
var gif = document.createElement('img');
gif.src = '/gif/' + data.gif;
div.appendChild(gif);
}
div.style.color = data.user;
document.querySelector('#chat-list').appendChild(div);
});
document.querySelector('#chat-form').addEventListener('submit', function (e) {
e.preventDefault();
if (e.target.chat.value) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.status === 200) {
e.target.chat.value = '';
} else {
console.error(xhr.responseText);
}
};
xhr.open('POST', '/room/#{room._id}/chat');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ chat: this.chat.value }));
}
});
document.querySelector('#gif').addEventListener('change', function (e) {
var formData = new FormData();
var xhr = new XMLHttpRequest();
console.log(e.target.files);
formData.append('gif', e.target.files[0]);
xhr.onload = function () {
if (xhr.status === 200) {
e.target.file = null;
} else {
console.error(xhr.responseText);
}
};
xhr.open('POST', '/room/#{room._id}/gif');
xhr.send(formData);
});