프레임워크/Express

[2차 정리]node.js 웹소켓

빨대도둑 2023. 9. 7. 01:07

HTTP와 AJAX

HTTP는 Hyper Test Transfer Protocol의 약자로 오늘날 통신에 가장 널리 사용되고 있는 일종의 약속이자 형식입니다.

URL과 Header 같은 부가 정보를 포함하여 사용자가 원하는 데이터를 정확히 주고 받을 수 있도록 해줍니다.

HTTP통신은 클라이언트 요청 한번에 응답 한 번을 보내고 통신을 끝내게 됩니다.

따라서 페이지의 일부분만 갱신하고 싶어도 응달을 다시 보내야 합니다.

AJAX는 Asynchronous JavaScript XML 의 약자로 XMLHttpRequest 라는 자바스크립트 객체를 이용해 서버와 비동기 방식으로 통신하여 DOM을 조작해 문서의 일부분만 갱신하는 것을 가능하게 합니다.

AJAX는 페이지의 일부분만 동적으로 생성하기 때문에 페이지 전체를 갱신하는 HTTP통신보다 렌더링 속도가 빠르고 오버헤드가 적습니다.

또한 비동기 방식으로 통신을 하기 때문에 클라이언트가 불필요하게 대기하는 시간이 줄어듭니다.

따라서 HTTP보다 AJAX를 사용하는 경우는 이메일 중복 체크나 비밀번호 확인 등 페이지 이동이 없는 경우와 빠른 렌더링을 보장하기 위한 경우에 사용합니다.

하지만 AJAX도 HTTP통신이기 때문에 여전히 클라이언트가 요청을 보내지 않으면 통신을 시작할 수 없다는 한계가 있습니다.

 


 

웹 소켓

웹 소켓은 클라이언트와 서버 간의 양방향 통신이 가능하도록 지원하는 프로토콜 입니다.

‘WS: WebSocket Protocol’이 생겨나면서 클라이언트가 요청을 먼저 보내지 않아도 서버 측에서 데이터를 보낼 수 있습니다.

WS는 특히 실시간 서비스를 궁극적으로 발전시킨 원동력이 되었습니다.

클라이언트가 세번의 요청을 보낸다고 하면 HTTP방식은 세번의 통신 연결이 필요하지만 웹 소켓을 사용하면 딱 한번만 연결을 맺고 양방향 통신이 가능합니다.

또한 단순한 API로 구성되어 있어 설계나 구현도 간결하고, 기존에 사용하던 http 통신으로 구현한 express 서버와 포트도 공유가 가능합니다.

XMLHttpRequest 객체와 Server-Sent Events를 조합해서 사용하는 것도 가능합니다.

*XMLHttpRequest: 통신을 비동기적으로 가능하게 해주는 객체

*Server-Sent Events: 단방향 통신 연결을 통해 http를 사용해 데이터를 푸시할 수 있도록 해주는 통신 모델, EventSource 객체를 사용해서 구현한다.

 

다음 명령어로 ws 모듈을 설치합니다.

npm install ws
//socket.js
const WebSocket = require('ws'); // npm install -g ws@7.4.3

module.exports = (server) => {
		// 클라이언트가 요청을 보내면 new WebSocket으로 우베 소켓 객체의 인스턴스를 생성해서 was 변수어 대입합니다. 
    const wss = new WebSocket.Server({ server });

    wss.on('connection', (ws, req) => { // Connection Generate
				//Connection이 생성되면, req.header와 req.connection.remoteAddress를 통해 사용자의 IP를 알아냅니다. 
        const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
        console.log('New Client : ', ip);
        ws.on('message', (message) => { // 클라이언트로부터 메세지, IP와 클라이언트가 보낸 메시지를 서버 콘솔에 띄움. 
            console.log(message);
        });
        ws.on('error', (err) => { // 에러처리
            console.error(err);
        });
        ws.on('close', () => { // 종료
            console.log('클라이언트 접속 해제', ip);
            clearInterval(ws.interval);
        });

				// wetInterval 함수로 서버도 클라이언트에게 3초마다 메세지를 보냄. 
        ws.interval = setInterval(() => { // 서버에서 메세지
						// 비동기 처리로 인해 혹시 연결이 되지 않은 상태에서 메시지를 보내는 것을 막기 위해 ws.readyState를 한번 체크함. 
            if (ws.readyState === ws.OPEN) {
                ws.send('Message From Server.');
            }
        }, 3000);
    });
};
// ws.readyState에는 OPEN(연결 성공), CLOSE(연결 닫힘), CLOSING(닫는 중), CONNECTING(연결 중)이 있습니다. 
// 만약에 종료되었다면 해당 Interval은 clearInterval로 제거해주어야 합니다. 

 

<!--클라이언트 코드-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket</title>
  </head>
  <body>
    <div>ws 모듈로 웹 소켓을 알아봅시다.</div>
    <script>
      const webSocket = new WebSocket("ws://localhost:8080"); // ws protocol
			<!--클라이언트가 WebSocket 객체의 인스턴스를 생성함.-->
      webSocket.onopen = function () {<!--웹 소켓과 연결에 성공하면 onopen 핸들러임.  -->
        console.log("Web Socket Connected");
      };
      webSocket.onmessage = function (event) {<!--메시지를 보낼 때 실행되는 onmessage 핸들러임.  -->
        console.log(event.data);
        webSocket.send("This Message From Client");
      };
    </script>
  </body>
</html>

 

// 서버 코드

const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const express = require('express');
const app = express();

const webSocket = require('./socket.js');

/* 포트 설정 */
app.set('port', process.env.PORT || 8080);

/* 공통 미들웨어 */
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser('wsExample'));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: 'wsExample',
    cookie: {
        httpOnly: true,
        secure: false
    }
}));

/* 라우터 설정 */
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

/* 404 에러처리 */
app.use((req, res, next) => {
    const error = new Error(`${req.method} ${req.url} 해당 주소가 없습니다.`);
    error.status = 404;
    next(error);
});

/* 에러처리 미들웨어 */
app.use((err, req, res, next) => {
    res.locals.message = err.message;
    res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
    res.status(err.status || 500);
    res.send('error Occurred');
})

/* 서버와 포트 연결.. */
const server = app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 서버 실행 중 ..')
});

// 서버 파일의 역할은 url로 접속 시 index.html 파일을 보내주고, WebSockt 서버와 http를 사용하는 express 서버가 포트를 공유할 수 있도록 해줌. 
webSocket(server); // ws와 http 포트 공유

 


 

실시간 채팅(테스트 버전)

websocket을 사용할 수 있는 방법에는 ws 모듈 말고 socket.io라는 것이 있습니다.

socket.io는 ws 모듈의 메시지를 주고 받는 기능을 확장한 패키지 입니다.

사용자를 그릅화해서 메시지를 보낼 수 도 있고 특정 사용자에게만 메시지를 보내는 기능을 쉽게 만들 수 있어 주로 채팅 기능을 구현할 때 사용합니다.

다음 명령어로 socket.io 모듈을 설치합니다.

npm install socket.io
// socket.io 모듈 불러오기
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const express = require('express');
const app = express();

const webSocket = require('./socket.js');

/* 포트 설정 */
app.set('port', process.env.PORT || 8080);

/* 공통 미들웨어 */
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser('wsExample'));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: 'wsExample',
    cookie: {
        httpOnly: true,
        secure: false
    }
}));

/* 라우터 설정 */
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

/* 404 에러처리 */
app.use((req, res, next) => {
    const error = new Error(`${req.method} ${req.url} 해당 주소가 없습니다.`);
    error.status = 404;
    next(error);
});

/* 에러처리 미들웨어 */
app.use((err, req, res, next) => {
    res.locals.message = err.message;
    res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
    res.status(err.status || 500);
    res.send('error Occurred');
})

/* 서버와 포트 연결.. */
const server = app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 서버 실행 중 ..')
});

webSocket(server); // ws와 http 포트 공유

 

// SocketIO 인스턴스 생성
const SocketIO = require("socket.io");

module.exports = (server) => {
		// SocketIO 객체의 인스턴스를 생성하고 변수 io에 넣어줌. 
		// 두번째 인자로 path를 넣어주어야 하는데, 아무것이나 지정해도 상관은 없지만 index.html파일과 동일하게 설정해야 함. 
    const io = SocketIO(server, { path: "/socket.io" }); // index.js의 path와 동일하게

		// io.on()을 통해 Connection을 생성하고 socket.on()을 통해 이벤트를 감지함. 
    io.on("connection", (socket) => {
        const req = socket.request;
				// io.on()으로 Connection을 생성한 뒤 콜백으로 넘겨지는 socket은 내부에 request를 가지고 있어 이를 이용해 IP 주소를 알아냄. 
        const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
				// socket.id라는 것이 있는데, 이는 각 소켓에 고유한 ID를 부여해주는 것으로 이를 이용하여 특정 사용자에게만 메시지를 모낸다든가 하는 기능을 구현할 수 있음. 
        console.log(
            `New Client : ${ip}, socket.id : ${socket.id}`
        );

        socket.on("disconnect", () => {
            console.log(`Client Out : ${ip}, socket.id : ${socket.id}`);
            clearInterval(socket.interval);
        });

        socket.on("error", (error) => { });

        socket.on("from client", (data) => { // 클라이언트가 넘긴 데이터
            console.log(data);
        });

        socket.interval = setInterval(() => { // send 대신 emit으로 메세지를 보냄
						// ws 모듈에서 메시지를 보내는 함수가 send()였다면 socket.io에서는 emit()를 사용함. 
						// io.emit()은 연결된 모든 소켓에게 이벤트를 보내는 것이고, socket.emit()은 특정 소켓에게만 이벤트를 보내는 메서드임. 
            socket.emit("from server", "Message From Server");
        }, 3000);
    });
};

 

  • ws 모듈과 비슷하게 동작하는 것 중 딱 두개만 기억해야 한다면 send()가 아닌 emit()으로 메시지를 보내고 받는 것 과, emit()의 인자에는 '이벤트명'과 '메시지'를 전달해주어야 한다는 점 이다. emit( )에서 보내주는 이벤트명은 이벤트를 식별하는 역할을 하며 각 이벤틈다 메시지를 다르게 보낼 수 있다.
  • socket.emit(”from server”~)으로 보내준 “from server” 라는 이벤트명을 index.html에서 socket.on(”from server”~)로 받아주게 되며, 이젠 이벤트마다 여러 리스너를 만들 수 있게 된다.
<!--클라이언트 파일-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Socket.io</title>
  </head>
  <body>
    <div>socket.io 모듈로 웹 소켓을 알아봅시다.</div>
    <script src="/socket.io/socket.io.js"></script><!-- 'io'라는 변수로 SocketIO 객체를 사용할 수 있게 하는 스크립트이다. -->
    <!-- io 제공 script -->
    <script>
			// io.connect()인자에 ws 프로토콜이 아닌 http 프로토콜이 들어간다. 
			// websocket으로 바로 연결되는 것이 아니라 http의 폴링을 한 번 시도한 후 websocket으로 연결한다. 
			// websocketㅇ르 지원하지 않는 브라우저가 있을 수 있기 때문이다. 
			// index.html에서 io.connect()에 http 프로토콜을 넣고, Status 101은 웹 소켓 객체를 사용할 수 있다는 의미이다. 
      const socket = io.connect("<http://localhost:8080>", {
        path: "/socket.io",
        transports: ["websocket"], // polling 생략, IE9을 제외한 모든 브라우저는 websocket을 지원한다. 
      });
			// socket.io에서 자체적으로 json을 문자열 객체로 변환하는 JSON.stringify와 문자열 객체를 json 객체로 변환하는 json.parse가 내부적으로 실행되기 때문. 
      socket.on("from server", function (data) {
        // 이벤트 리스너, 여러개 할당 가능
        console.log(data);
        socket.emit("from client", "Message From Client"); // 이벤트 이름, 데이터
      });
    </script>
  </body>
</html>

 


 

클라이언트가 2명 이상 접속해서 채팅하는 상황

// http, express 모듈을 불러와서 http.Server(app)을 통해 연결함. 
const http = require("http");
const express = require("express");
const app = express();

app.use(express.static(__dirname)); 

const server = http.Server(app);
const io = require("socket.io")(server);
// 연결한 server을 socket.io와 포트를 공유해서 io변수에 담아줌
let users = [];

server.listen(8080, () => {
    console.log("8080포트에서 서버 실행 중...");
});

// '/'라우터에는 res.sendFile()을 통해 index.html을 보내줌. 
app.get("/", (req, res) => {
    res.sendFile(__dirname + "/index.html");
});

// socket.io를 사용해서 Connection을 만듬
// 총 3개의 이벤트를 생성함. 
io.on("connection", (socket) => {
    let name = "";
    socket.on("has connected", (username) => { // 이벤트 : has connected: 사용자가 접속하면 users 리스트에 사용자를 넣고 emit()을 통해 username과 userList를 연결함
        name = username;
        users.push(username);
        io.emit("has connected", { username: username, usersList: users });
    });

    socket.on("disconnect", () => { // 이벤트 : has disconnected: 사용자가 접속을 끊으면 users 리스트에서 splice 함수를 통해 사용자를 제거함. 
        users.splice(users.indexOf(name), 1);
        io.emit("has disconnected", { username: name, usersList: users });
    })

    socket.on("new message", (data) => { // 이벤트 : new message: 새로운 메시지가 클라이언트로부터 오면 on()을 통해 이를 받고 해당 메시지를 다시 emit()을 통해 모든 소켓에 전송. 
        io.emit("new message", data); // 모든 소켓에 메세지를 보냄
    });
});