WebSocket

·2022년 7월 11일
0

etc.

목록 보기
1/1
post-thumbnail

WebSocket

http 프로토콜과 ws 프로토콜

기존의 http 프로토콜은 stateless를 지향합니다. 요청이 계속 연결되는 경우, 비용이 상당하기 때문입니다. 실제 웹사이트에 방문하는 경우를 생각해보시죠. 초기페이지를 요청과 응답이 서로 이루어지고 난 후, 다음 사용자의 이벤트까지 클라이언트와 서버의 통신은 끊기게 되죠.

이번엔 채팅을 생각해볼까요?? 우리는 상대로부터 언제 답장이 올지 알 수있나요?? 매번 요청과 응답을 받기엔 비용소모도 크게 되니, 기존의 http 프로토콜 운영방식으로는 채팅 프로그램을 만들기 비효율적이겠죠.

따라서 WebRTC, WebSocket은 stateless와는 다른 연결방식을 취하게 됩니다. 기존의 http 프로토콜과 서로간의 통신을 하는 것은 분명하나, 지속적으로 연결이 되는 상태를 가지게 됩니다. 서버나 클라이언트 이제 더이상 요청이 응답을 반드시 거쳐야하는게 아닌, 필요할 때 데이터를 보내고, 업데이트 하는게 가능해지죠 리얼타입 앱을 구축해지는 게 가능해진겁니다.

WebSocket 클라이언트-서버 연결하기

npm을 통해 먼저 ws 모듈을 설치합시다. 사용방법은 간단합니다. 기존의 http 서버를 생성한 후, 소켓 모듈을 장착시켜 리얼타임 통신이 가능하게 만듭니다. 이후 클라이언트에서 해당 연결된 서버를 호출하면 서버와 클라이언트가 지속적으로 통신이 가능하게 됩니다. 이후 클라이언트 행동에 따라, 서버가 반응하고, 서버의 행동에 따라 클라이언트가 반응하는 이벤트를 실행할 수 있게 됩니다.

// server - server.js  ///////////
/////////////////////////////////
////////////////////////////////
import http from "http";
import WebSocket from "ws";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log(`Listening on http://localhost:3000`);

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// function handleConnection(socket) {
//   console.log(socket);
// }

wss.on("connection", (socket) => {
  // 클라이언트가 통신이 중단되면 (브라우저가 꺼지면) 서버 콘솔에 출력합니다.
  socket.on("close", () => {
    console.log("Disconnected from the Browser");
  });
  // 클라이언트가 메시지를 보내면 서버 콘솔에 메시지를 출력합니다.
  socket.on("message", (message) => {
    console.log(message.toString());
  });
  
  // 서버에서 클라이언트에게 메시지를 보냅니다.
  socket.send("hello");
});

// app.listen(3000, handleListen);
server.listen(3000, handleListen);
// client - app.js //////////////
/////////////////////////////////
/////////////////////////////////
const socket = new WebSocket(`ws://${window.location.host}`);

// 서버와 연결되면, 브라우저 콘솔에 출력합니다.
socket.addEventListener("open", () => {
  console.log("Connected to Server 😂");
});

// 서버가 메시지를 보냈다면, 브라우저 콘솔에 메시지를 출력합니다.
socket.addEventListener("message", (message) => {
  console.log("New message : ", message.data, "from the server");
});

// 서버와 통신이 중단되면, 브라우저 콘솔에 출력합니다.
socket.addEventListener("close", () => {
  console.log("disConnected to server ✌");
});

// 10초후 서버에 데이터를 보냅니다.
setTimeout(() => {
  socket.send("hello from the browser!");
}, 10000);

1:1 채팅, 클라이언트 A - 서버 - 클라이언트 B 연결하기

클라이언트와 서버 간 양방향 연결을 통해 서로 메시지를 주고 받을 수 있다는 것을 확인하였습니다. 다만, 우리가 실제 쓰고 있는 채팅 프로그램은 클라이언트 - 서버가 아닌 클라이언트와 또다른 클라이언트 간의 대화이죠.

따라서 서버를 기반으로 각각의 클라이언트에 대한 socket 객체를 생성하고, 각자의 메시지가 화면의 렌더링 되는 클라이언트 - 서버 - 클라이언트 형태의 1:1 채팅 기능을 구현해봅시다.

// server - server.js  ///////////
/////////////////////////////////
////////////////////////////////
import http from "http";
import WebSocket from "ws";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log(`Listening on http://localhost:3000`);

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// function handleConnection(socket) {
//   console.log(socket);
// }

// socket 객체를 넣어둘 임시 데이터 베이스
const sockets = [];

wss.on("connection", (socket) => {
  // 소켓이 새로 생성되면, sockets 배열에 담는다.
  sockets.push(socket);
  // 따로 닉네임을 설정하지 않으면 갖게 되는 default 값
  socket.nickname = "Anonymous";
  console.log("Connected to Browser 😂");
  socket.on("close", () => {
    console.log("Disconnected from the Browser");
  });
 
  socket.on("message", (msg) => {
    const message = JSON.parse(msg);
    switch (message.type) {
      // 만약 객체의 "type" 키의 값이 new_message면 메시지를 보내준다.
      case "new_message":
        sockets.forEach((aSocket) => {
          aSocket.send(`${socket.nickname} : ${message.payload}`);
        });
        break;
      // 만약 객체의 "type" 키의 값이 nickname이면 해당 소켓 객체의 닉네임을 새롭게 설정한다.
      case "nickname":
        socket.nickname = message.payload;
        break;
    }
    // socket.send(message);
  });
  // socket.send("hello");
});

// app.listen(3000, handleListen);
server.listen(3000, handleListen);
// client - app.js //////////////
/////////////////////////////////
/////////////////////////////////
const messageList = document.querySelector("ul");
const messageForm = document.querySelector("#message");
const nickForm = document.querySelector("#nickname");
const socket = new WebSocket(`ws://${window.location.host}`);

socket.addEventListener("open", () => {
  console.log("Connected to Server 😂");
});

socket.addEventListener("message", (message) => {
  const li = document.createElement("li");
  li.innerText = message.data;
  messageList.append(li);
  // console.log("New message : ", await message.data.text());
});

socket.addEventListener("close", () => {
  console.log("disConnected to server ✌");
});

// 일반객체를 JSON 형태로 만들어주는 함수
function makeMessage(type, payload) {
  const msg = { type, payload };
  return JSON.stringify(msg);
}

// 메시지 객체를 보내는 함수
function handleSubmit(event) {
  event.preventDefault();
  const input = messageForm.querySelector("input");
  socket.send(makeMessage("new_message", input.value));
  const li = document.createElement("li");
  li.innerText = `You : ${input.value}`;
  messageList.append(li);
  input.value = "";
}

// 닉네임 객체를 보내는 함수
function handleNickSubmit(event) {
  event.preventDefault();
  const input = nickForm.querySelector("input");
  socket.send(makeMessage("nickname", input.value));
}

messageForm.addEventListener("submit", handleSubmit);
nickForm.addEventListener("submit", handleNickSubmit);

socket.io

실시간, 양방향, 이벤트 기반의 통신을 제공하는 프레임워크입니다. websocket과 비슷하다고 느낄 수 있으나, 둘은 전혀 다른 개념입니다. websocket은 일종의 통신 규약이고, socket.io는 통신을 제공하는 라이브러리 로써, websocket을 하나의 방법으로 적용됩니다. 즉 websocket을 해당 브라우저가 지원하지 않거나, websocket의 연결이 끊기다 하더라도, 대체할 수 있는 여러 기능을 가지고 있다는 뜻이 되겠네요

socket.io를 이용해서 서버와 클라이언트 환경을 맞춰봅시다.

// server.js
import http from "http";
// import WebSocket from "ws";
import SocketIO from "socket.io";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log(`Listening on http://localhost:3000`);

const server = http.createServer(app);
const wsServer = SocketIO(server);

wsServer.on("connection", (socket) => {
  console.log(socket);
});

// app.listen(3000, handleListen);
server.listen(3000, handleListen);
// front- app.js
const socket = io();
// views-home.pug
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 Noom
    link(rel="stylesheet", href="https://unpkg.com/mvp.css")
  body
    header 
      h1 Noom!
    main
    script(src="/socket.io/socket.io.js") 
    script(src="/public/js/app.js") 

webSocket 과 socket.io의 차이

socket.io에서는 send는 사라지고 emit을 통해 데이터를 보냅니다. websocket에서는 데이터를 보낼때 JSON으로 변화시켜 보내야 했지만 이제는, 어떤 타입으로든 보내는 것이 가능해집니다. socket.io가 중간 과정에서 알아서 변화시켜 보냅니다.

키 이름을 어떠한 걸 넣어도 됩니다. websocket에서는 message라는 문자열을 반드시 첫번째 이름으로 넣어줬다면, 이제는 마음대로 개발자가 정해서 넣을 수 있습니다. 서버와 클라이언트가 같은 키를 가지고 있다면 해당 데이터는 정상적으로 전송이 됩니다.

데이터 갯수에 상관없이 원하는 만큼 보내는 것이 가능합니다. websocket에서는 통신 한번 당 한개의 데이터만 보내지기 때문에 복수의 데이터를 보내려면 하나의 오브젝트로 묶어 보내야 했다면, 이제는 일일이 매개변수로 넣어 보내는 것이 가능해졌습니다.

emit의 매개변수로 함수를 보낼 수 있습니다. 해당 함수가 서버에 보내지면 서버에서 호출하는 것이 가능하며, 서버에서 호출하는 순간, 다시 클라이언트에서 함수가 실행이 되게 하는 것이 가능합니다.
함수를 매겨변수로 사용할 때 유의점은 emit의 맨 마지막 매개변수로 넣어야 한다는 것 입니다.

// client
const socket = io();
const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");

function backendDone(msg) {
  console.log("The back-end say:", msg);
}

function handleRoomSubmit(event) {
  event.preventDefault();
  const input = form.querySelector("input");
  socket.emit("enter_room", input.value, backendDone);
  input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);
import http from "http";
import SocketIO from "socket.io";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const handleListen = () => console.log(`Listening on http://localhost:3000`);

const server = http.createServer(app);
const wsServer = SocketIO(server);

wsServer.on("connection", (socket) => {
  socket.on("enter_room", (roomName, done) => {
    console.log(roomName);
    setTimeout(() => {
      done("hello");
    }, 15000);
  });
});

room 생성하기

매우 간단합니다. socket.join()을 사용하며 원하는 룸에 들어가도록 할 수 있습니다. 기존에 socket에 없던 룸이면 새롭게 룸을 만들고, 있던 룸이라면 그 룸에 참여하게 됩니다. 룸에서 나가는 함수는 socket.leave() 를 씁니다.

wsServer.on("connection", (socket) => {
  // socket.onAny((event) => {
  //   console.log(`Socket Event:${event}`);
  // });
  socket.on("enter_room", (roomName, done) => {
    socket.join(roomName);
    done();
    socket.to(roomName).emit("welcome");
  });

adaptor

admin UI

profile
새로운 것에 관심이 많고, 프로젝트 설계 및 최적화를 좋아합니다.

0개의 댓글