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?