[ZoomCloneCode] SocketIO를 알아보자

junghan·2023년 1월 17일
0

zoom클론코딩

목록 보기
3/4
post-thumbnail

이번에는 아주 쉽게 실시간 기능을 만들어주는 framework인 socketIO를 사용해볼 것 입니다.

What is socketIO?

SocketIO는 이전에 사용했던 websocket과 마찬가지로 socket IO는 실시간, 양방향, event 기반의 통신을 가능하게 해줍니다.

하지만 socketIO와 websocket은 엄연히 다릅니다.
Socket IO는 실제로 가능한 경우 전송을 위해 WebSocket을 사용하기도 하지만 각 패킷에 추가 메타데이터를 추가하기 때문에 WebSocket 클라이언트가 Socket.IO 서버에 성공적으로 연결할 수 없습니다. 마찬가지로 Socket.IO 클라이언트도 일반 WebSocket 서버에 연결할 수 없겠죠.

WebSocket 연결을 설정할 수 없는 경우 연결은 HTTP long-polling으로 대체됩니다.

이 기능은 WebSockets에 대한 브라우저 지원이 아직 초기 단계였기 때문에 프로젝트가 생성된 지 10년이 넘었을 때 사람들이 Socket.IO를 사용한 가장 큰 이유였다고 말합니다.

이러한 특징말고도 자동 재연결이나 패킷버퍼링과 같은 기능들이 특징입니다.

sockeIO docs (https://socket.io/docs/v4/#http-long-polling-fallback)

그렇다면 직접 socketIO를 사용하며 socketIO가 얼마나 간편한지 알아보겠습니다.


socketIO 세팅하기

1. socketIO 패키지 설치

$ npm i socket.io

2. SocketIO server생성

import http from "http";
import SocketIO from "socket.io";
import express from "express";

//소켓 IO추가할것

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");

// app.listen(3000, handleListen);
//express환경에서 http와 ws를 둘다 돌리는 작업
//필수사항은 아님 여기서는 동일한 포트에서 두가지 작업을 처리하기 위해 설정
const httpServer = http.createServer(app); //ws를 쓰기위해 express로부터 http서버를 생성
const wsServer = SocketIO(server); //socket io 서버 생성


//브라우저와 연결된 소켓
httpServer.listen(3000, handleListen);

서버에 socketIO서버를 띄우게 되면 socket.io.js라는 API가 제공되고, 클라이언트에서는 이 파일을 이용해 socket.io를 사용할 수 있게 됩니다.
일전에 말했듯이 socketIO와 websocket은 호환이 되지 않기 때문에(socket.io가 더 많은 기능이 있어서) 클라이언트는 더 이상 브라우저에서 제공하는 websocket API를 통한 소통이 불가합니다.

3. 클라이언트에 socket.io적용

pug파일에 socket.io.js import를 해줍니다.

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 Zoom
        link(rel="stylesheet" href="https://unpkg.com/mvp.css@1.12/mvp.css")
    body 
        header 
            h1 zoom
        main 

        script(src="/socket.io/socket.io.js")
        script(src="/public/js/index.js") 

script(src="/socket.io/socket.io.js")으로 socket.io.js를 삽입하게되면 클라이언트에서도 socket.io를 사용할 수 있게 됩니다.

4. 클라이언트와 서버 socket.io 연결하기

클라이언트에서 import받은 socket.io.js 통해 io()를 사용할 수 있게 되는데,

index.js

const socket = io()

자동적으로 서버의 socket.io와 연결해주는 함수인 io()를 호출하면 연결이 완료됩니다.


서버의 connection이벤트를 통해 연결을 확인해보면

server.js

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

위와 같이 연결됨을 확인할 수 있습니다.
websocket을 사용할 경우 연결이 된 것을 확인한 뒤, 소켓을 직접 저장하여 관리해야했지만 socket.io의 경우 socket을 자동으로 저장하여 관리하기 때문에 별도의 처리가 필요하지 않습니다.



Socket.io 기본 통신

1. 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 Zoom
        link(rel="stylesheet" href="https://unpkg.com/mvp.css@1.12/mvp.css")
    body 
        header 
            h1 zoom
        main 
            div#welcome
                form    
                    input(placeholder="room name", required, type="text")
                    button Enter room
        script(src="/socket.io/socket.io.js")
        script(src="/public/js/index.js") 

2. socket.io emit/on

socket.io에서는 emit함수로 이벤트를 전송하고 on함수로 이벤트를 수신합니다.

index.js

const socket = io();

const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");

function handleRoomSubmit(event) {
  event.preventDefault();
  const input = form.querySelector("input");
  console.log(input.value);
  socket.emit("enter_room", { payload: input.value }, (msg) => {
    console.log("server is done! server:", msg);
  }); //1. 특정한 event를emit가능하고, 2. object를 전송가능함 추가적으로 3. 서버에서 호출할 수 있는 클라이언트 function을 넣을 수 있음. 심지어 함수에 argument도 넣을 수 있음.
  // 아래 설명할 것

  socket.emit("checker", { payload: input.value }, "hello", 42, true); //여러 argument를 동시에 보내는 것이 가능해짐
  // 아래 설명할 것

  input.value = "";
}

form.addEventListener("submit", handleRoomSubmit);

server.js


import http from "http";
import SocketIO from "socket.io";
import express from "express";

//소켓 IO추가할것

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 httpServer = http.createServer(app); //ws를 쓰기위해 express로부터 http서버를 생성
const wsServer = SocketIO(httpServer); //socket io 서버 생성
//브라우저 상에서 websocket의 open이벤트로 감지가능

wsServer.on("connection", (socket) => {
  socket.on("enter_room", (msg, done) => {
    console.log(msg);
    setTimeout(() => {
      done("finished work!"); //클라이언트로 부터 전달받은 함수를 호출하면 서버에서 호출하지만 실행은 클라이언트에서 됨.
    }, 10000); // 즉 클라이언트로부터 전달받은 argument를 토대로 오래걸리는 작업을 한 뒤, 함수호출을 통해 작업종료 확인하는 등 여러 방법으로 사용가능
  });
  socket.on("checker", (a, b, c, d) => {
    console.log(a, b, c, d);
  });
  //서버 재연결 설명
});

httpServer.listen(3000, handleListen);

socket.io에서 emit은 기존 websocket에 비하여 많은 기능들이 추가가 되었습니다.

1. 고정적으로 보내던 message 이벤트 대신 특정 이벤트로 커스텀이 가능해졌습니다.
2. argument로 하나의 string으로 보냈던 websocket과 달리 object형태나 자유로운 형태의 자료형으로 전달가능해졌을 뿐만 아니라, 여러개의 argument가 전달가능합니다.
3. argument의 마지막 위치에 콜백함수를 전달하여 서버에서 클라이언트의 함수를 호출이 가능해졌습니다.

3번 케이스의 경우, 클라이언트로부터 전달받은 argument를 토대로 오래 걸리는 작업을 한 뒤, 함수호출을 통해 작업종료를 확인하는 등 여러 방법으로 사용가능하기에 네트워크 소통비용을 줄일 수 있습니다. 또한 서버는 클라이언트의 콜백함수에게로 argument를 전달할 수도 있습니다.

client

socket.emit("enter_room", { payload: input.value }, (msg) => {
    console.log("server is done! server:", msg);
  });

server

socket.on("enter_room", (msg, done) => {
    console.log(msg);
    setTimeout(() => {
      done("finished work!"); // 서버 -> 클라이언트로 argument전달
    }, 10000); 
  });



socket.io Namespace

https://socket.io/docs/v4/server-api/#namespace

경로 이름(예: /chat)으로 식별되는 지정된 범위 아래에 연결된 socket 풀을 나타냅니다.

namespace로 정의된 특정 노드끼리만 연결하여 특정 페이지에서 소켓이 보내주는 실시간메시지를 구분하여 받을 수 있어 불필요한 낭비를 줄일 수 있습니다.

다시 말하자면, namespace는 기본적으로 '/'에 신호를 전송하고 수신했지만, 다른 경로를 만들어서 같은 namespace에 있는 소켓끼리만 소통할 수 있는 것입니다.



Socket.io Socket

socket.io에는 서버와 클라이언트를 위한 다양한 API가 제공됩니다.

https://socket.io/docs/v4/server-api/#socket

socket 클래스는 브라우저 클라이언트와 상호 작용하기 위한 기본 클래스인데, 특정 namespace(기본적으로 /)에 속하며 기본 클라이언트를 사용하여 통신합니다.

이 socket은 실제 기본 TCP/IP 소켓과 직접 관련이 없으며 클래스의 이름일 뿐이라는 점에 유의해야 합니다.

각 namespace 내에서 소켓이 가입하고 떠날 수 있는 임의의 채널(room이라고 함)을 정의할 수도 있습니다. 이는 socket 그룹에 브로드캐스팅하는 편리한 방법을 제공합니다.

Room

Room은 Namespace의 하위 개념으로써 namespace안에 있는 소켓들을 room으로 쪼개나눈 것입니다.

client

socket.emit("enter_room", "room1", (msg) => {
    console.log("server is done! server:", msg);
  }); 

server

  socket.on("enter_room", (roomName, done) => {
    console.log(socket.id);
    console.log(socket.rooms); 
    socket.join(roomName); // room을 새로 생성하고 들어감
    console.log(socket.rooms);
    socket.leave(roomName); // room을 나감
    console.log(socket.rooms); 

아래의 출력결과를 보면 client에서 전달한 msg를 받아 server에서 socket.join을 통해 room을 생성하였고, rooms()를 통해 확인 가능한 것을 볼 수 있습니다. leave를 하면 소속된 room이 사라지겠죠.

또한 socket의 id도 rooms()에 소속되어 있는 것을 볼 수 있는데, 기본적으로 socketIO에서 모든 socket은 private room을 가지고 있기 때문입니다.

profile
42seoul, blockchain, web 3.0

0개의 댓글