11. 웹소켓실시간데이터전송
11. 웹소켓으로 실시간 데이터 전송하기
11.1 웹소켓 이해하기
웹소켓
HTML5 새로 추가된 스펙
실시간 양방향 데이터 전송을 위한 기술
WS라는 프로토콜을 사용함. HTTP 프로토콜 아님. 브라우저와 서버가 WS를 지원하면 사용이 가능하다
ws, Socket.IO 패키지를 통해 웹소켓 사용 가능
웹소켓이 나오기전에는 HTTP기술을 사용하여 실시간 데이터 전송 구현
폴링 : HTTP가 단방향이므로, 서버의 데이터 변경상태를 확인하기 위해 지속적인 요청을 통해 확인하는 방식
한번 연결이 되면 연결을 유지 하여 지속적인 업데이트가 가능하도록 함
Socket.IO
는 웹소켓을 편리하게 사용할 수 있도록 도와주는라이브러리
IE9는 웹소켓지원 안됨 -> 폴링방식으로 구현
웹소켓이 끊기면, 자동으로 재연결 시도
SSE(Server Sent Event)기술의 등장
한번 연결이 되면 서버 -> 클라이언트로 데이터를 지속적으로 보냄
(웹소켓과 다른점)
: 클라이언트 -> 서버 전송은 불가
11.2 ws 모듈로 웹소켓 사용하기
서버에서만 웹소켓 코딩한다고 되는게 아님. 클라이언트 쪽에서도 코딩이 필요함. (양방향 통신이기 때문) ws모듈 + @ = socket.io 모듈
웹소켓 구조도
package.json
{ "name": "gif-chat", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "start": "nodemon app", "test": "echo \"Error : no test specified\" && exit 1 " }, "author": "DevS", "license": "ISC", "dependencies": { "connect-flash": "^0.1.1", "cookie-parser": "^1.4.3", "dotenv": "^5.0.1", "express": "^4.16.2", "express-session": "^1.15.6", "morgan": "^1.9.0", "pug": "^2.0.0-rc.4" }, "devDependencies": { "nodemon": "^1.14.11" } }
# 프로젝트 만들기
$ npm i
# 웹 소켓
$ npm i ws
# socket.io 설치
$ npm i socket.io
.env
COOKIE_SECRET=gifchat
app.js
```javascript const express = require("express"); const path = require("path"); const morgan = require("morgan"); const cookieParser = require("cookie-parser"); const session = require("express-session"); const flash = require("connect-flash"); require("dotenv").config();
const indexRouter = require('./routes'); // 생략시 index.js 명시적으로 코딩하기 const webSocket = require('./socket'); // socket.js, socket2.js .. 파일명 const app = express();
app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); app.set('port', process.env.PORT || 8005);
app.use(morgan()); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); app.use(express.urlencoded({extended: false})); app.use(cookieParser(process.env.COOKIE_SECRET)); app.use(session({ resave: false, saveUninitialized: false, secret: process.env.COOKIE_SECRET, cookie: { httpOnly: true, secure: false } })); app.use(flash());
app.use('/', indexRouter);
app.use((req, res, next) => { const err = new Error('Not Found'); err.status = 404; next(err); });
app.use((err, req, res) => { res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; res.status(err.status || 500); res.render('error'); });
const server = app.listen(app.get('port'), () => { console.log(app.get('port'), '번 포트에서 대기 중 '); });
webSocket(server);
- routes/index.js
```javascript
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.render('index'); // 렌더링 페이지 : routes
});
module.exports = router;
socket.js
const WebSocket = require('ws'); module.exports = (server) => { // 웹소켓 서버 const wss = new WebSocket.Server({server}); // wss (웹소켓서버)에 리스너(ws.on)을 등록. 이벤트 기반 // connection -> 클라이언트가 서버와 웹소켓 연결을 맺을 때 발생 wss.on('connection', (ws, req) => { /* 클라이언트의 IP를 알아내는 방법 (자주사용 되므로 알아두기) 크롬 로컬 접속 : IP -> ::1 cf. IP 알아내는 방법 : express -> proxy-addr 패키지를 활용 하기도함 */ const ip = 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 */ const interval = 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
... const webSocket = require('./socket2'); // socket2.js 로 변경 ...
routes/index.js
... router.get('/', (req, res) => { res.render('index2'); // index -> index2 로 변경 }); ...
socket2.js
```javascript const SocketIO = require('socket.io');
module.exports = (server) => {
// 웹소켓 서버, 옵션(서버에 관한 여러가지 설정 가능) const io = SocketIO(server, {path: '/socket.io'}); // path 옵션만 사용
io.on('connection', (socket) => { const req = socket.request;
/*
클라이언트의 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.to(소켓아이디).emit(이벤트, 데이터)
나를 제외한 전체에게 메시지 보내기
socket.broadcast.emit(이벤트, 데이터)
socket.broadcast.to(방아이디).emit(이벤트, 데이터)
필요한 모듈 설치
$ npm i mongoose multer axios color-hash
.env 추가
COOKIE_SECRET=gifchat MONGO_ID=root MONGO_PASSWORD=nodejsbook
routes/index.js
```javascript const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs');
// 추가 const Room = require('../schemas/room'); const Chat = require('../schemas/chat'); const router = express.Router();
// router.get('/', (req, res) => { // res.render('index2'); // });
fs.readdir('uploads', (error) => { if (error) { console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.'); fs.mkdirSync('uploads'); } });
const upload = multer({ storage: multer.diskStorage({ destination(req, file, cb) { cb(null, 'uploads/'); }, filename(req, file, cb) { const ext = path.extname(file.originalname); cb(null, path.basename(file.originalname, ext) + new Date().valueOf() + ext); } }), limits: {filesSize: 5 1024 1024} });
router.post('/room/:id/gif', upload.single('gif'), async (req, res, next) => { try { const chat = new Chat({ room: req.params.id, user: req.session.color, gif: req.file.filename });
await chat.save();
req.app.get('io').of('chat').to(req.params.id).emit('chat', chat);
res.send('ok');
} catch (e) { console.error(e); next(e); } });
router.get('/', async (req, res, next) => { try { const rooms = await Room.find({}); res.render('main', {rooms, title: 'GIF 채팅방', error: req.flash('roomError')}); } catch (e) { console.error(e); next(e); } });
router.get('/room', (req, res) => { res.render('room', {title: 'GIF 채팅방 생성'}); });
router.post('/room', async (req, res, next) => { try { const room = new Room({ title: req.body.title, max: req.body.max, owner: req.session.color, password: req.body.password }); const newRoom = await room.save(); const io = req.app.get('io'); io.of('/room').emit('newRoom', newRoom); res.redirect(/room/${newRoom._id}?password=${req.body.password}
); } catch (e) { console.error(e); next(e); } });
router.get('/room/:id', async (req, res, next) => { try{ const room = await Room.findOne({_id: req.params.id}); const io = req.app.get('io'); if (!room) { req.flash('roomError', '존재하지 않는 방입니다.'); return res.redirect('/'); } const {rooms} = io.of('/chat').adapter; if (rooms && rooms[req.params.id] && room.max < rooms[req.params.id].length) { req.flash('roomError', '허용 인원이 초과하였습니다.'); return res.redirect('/'); }
// 추가
const chats = await Chat.find({room: room._id}).sort('createdAt');
return res.render('chat', {
room,
title: room.title,
chats,
user: req.session.color
});
} catch (e) { console.error(e); return next(e); } });
router.post('/room/:id/chat', async (req, res, next) => { try { const chat = new Chat({ room: req.params.id, user: req.session.color, chat: req.body.chat }); await chat.save(); // 챗 저장 req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat); // 챗 전송 모두에게 res.send('ok'); } catch (e) { console.error(e); next(e); } });
router.delete('/room/:id', async (req, res, next) => { try { await Room.remove({_id: req.params.id}); await Chat.remove({room: req.params.id}); res.send('ok'); setTimeout(() => { req.app.get('io').of('/room').emit('removeRoom', req.params.id); }, 2000); } catch (e) { console.error(e); next(e); } });
module.exports = router;
- schemas/chat.js
```javascript
const mongoose = require('mongoose');
const {Schema} = mongoose;
const {Types: {ObjectId}} = Schema;
const chatSchema = new Schema({
room: {
type: ObjectId, // Room 컬렉션의 ObjectId
required: true,
ref: 'Room' // Room과 연결
},
user: {
type: String,
required: true,
},
chat: {
type: String,
gif: String,
createdAt: {
type: Date,
default: Date.now
}
},
});
module.exports = mongoose.model('Chat', chatSchema);
schemas/room.js
```javascript
const mongoose = require('mongoose');
const {Schema} = mongoose; const roomSchema = new Schema({ title: { type: String, required: true }, max: { type: Number, required: true, defaultValue: 10, min: 2, }, owner: { type: String, required: true }, password: String, createdAt: { type: Date, default: Date.now } });
module.exports = mongoose.model('Room', roomSchema);
- schemas/index.js
```javascript
const mongoose = require('mongoose');
const {MONGO_ID, MONGO_PASSWORD, NODE_ENV} = process.env;
const MONGO_URL = `mongodb://${MONGO_ID}:${MONGO_PASSWORD}@localhost:27017/admin`;
module.exports = () => {
const connect = () => {
if (NODE_ENV !== 'production') {
mongoose.set('debug', true);
}
mongoose.connect(MONGO_URL, {
dbName: 'gifchat'
}, (error) => {
if (error) {
console.log('몽고디비 연결 에러', error);
} else {
console.log('몽고디비 연결 성공');
}
});
};
connect(); //
mongoose.connection.on('error', (error) => {
console.error('몽고디비 연결 에러', error);
});
mongoose.connection.on('disconnect', () => {
console.error('몽고디비 연결이 끊겼습니다. 연결을 재시도 합니다.');
});
require('./chat'); // ??
require('./room'); // ??
};
views
```pug
// main.pug (채팅방 목록)
extends layout
block content h1 GIF 채팅방 fieldset legend 채팅방 목록 table thead tr th 방 제목 th 종류 th 허용 인원 th 방장 tbody for room in rooms tr(data-id=room._id) td= room.title td= room.password ? '비밀방' : '공개방' td= room.max td(style='color:' + room.owner)= room.owner -var password = room.password ? 'true' : 'false'; td: button(data-password=password data-id=room._id).join-btn 입장 .error-message= error a(href='/room') 채팅방 생성 script(src='/socket.io/socket.io.js') script. var socket = io.connect('http://localhost:8005/room', { path: '/socket.io' });
// 방 생성 이벤트 등록
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);
});
// room.pug (채팅방 생성 뷰) extends layout
block content fieldset legend 채팅방 생성 form(action='/room' method='post') div: input(name='title' placeholder='방제목') div: input(name='max' type='number' placeholder='수용 인원 (최소 2명)' min='2' value='10') div: input(name='password' type='password' placeholder='비밀번호(없으면 공개방)') div: button(type='submit') 생성
1. 비행기 테이블, 좌석 테이블 관계
- 비행기 테이블
비행기 번호 :
```SQL
CREATE TABLE 비행기(
비행기번호 NUMBER PRIMARY KEY,
생산년도 DATE NOT NULL
);
- 좌석
```SQL
CREATE TABLE 좌석(
좌선번호 NUMBER,
좌석등급 VARCHAR2(10) NOT NULL,
비행기번호 NUMBER,
CONSTRAINT 좌석_PK PRIMARY KEY (좌선번호, 비행기번호),
CONSTRAINT 좌석_FK FOREIGN KEY (비행기번호) REFERENCES 비행기
);
CREATE TABLE 학생(
학번 NUMBER PRIMARY KEY ,
이름 VARCHAR2(20) not null ,
멘토학번 NUMBER CONSTRAINT 학생_FK REFERENCES 학생
);
```SQL
CREATE TABLE 공급자(
공급자번호 NUMBER PRIMARY KEY ,
공급자이름 VARCHAR2(20) NOT NULL
);
CREATE TABLE 부품(
부품번호 NUMBER PRIMARY KEY ,
부품이름 VARCHAR2(50) NOT NULL
);
CREATE TABLE 부서(
부서번호 NUMBER PRIMARY KEY ,
부서이름 VARCHAR2(100) NOT NULL
);
CREATE TABLE 계약(
부품번호 NUMBER CONSTRAINT 계약_부품_FK REFERENCES 부품,
부서번호 NUMBER CONSTRAINT 계약_부서_FK REFERENCES 부서,
공급자번호 NUMBER,
CONSTRAINT 계약_PK PRIMARY KEY (부품번호, 부서번호)
);
Last updated
Was this helpful?