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
.env
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);
socket.js
index.pug
11.3 socket.io 사용하기
ws 모듈 : ws프로토콜 사용
socket.io 모듈 : http 프로토콜 사용. 약간 Vue.js 양방향 통신의 이벤트 기반과 프로그래밍 패러다임이 비슷
클라 path 옵션 -> 서버의 path 옵션과 동일해야 통신이 가능
socket.io는 최초 폴링 방식으로 접근. 브라우저가 웹소켓을 지원하면 웹소켓으로 업그레이드하여 사용하고, 지원하지 않는 경우 폴링방식으로 유지.
app.js
routes/index.js
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;
}); };
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(이벤트, 데이터)
필요한 모듈 설치
.env 추가
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 });
} 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('/'); }
} 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/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);
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' });
// 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 채팅 내용
// 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') 생성
Last updated
Was this helpful?