코딩/Today I Learn

Zoom 클론코딩 #3 SoketIO

코딩쪼렙 2022. 3. 3. 19:27
728x90

설명 및 설정

 

프론트 백엔드 실시간 연결해주는  framework

유연해서 연결이 끊기면 자동으로 재연결을 시도하고 

websocket이 지원이 안되면 다른 방법을 찾아서 연결한다.

socketIO는 webScoket의 부가기능아님!!! webSocket은 브라우저에 api가 기본적으로 있어서 프론트에 설치를 안해도됐지만 socketIo는 기능도 더많고 다르기 때문에 백, 프론트 각각 설치 해줘야함 

 

https://socket.io/docs/v4/

 

설치 npm i socket.io

import SocketIO from "socket.io";

 

 

 

const httpServer= http.createServer(app);

const wss = new WebSocket.Server({httpServer}); 기존 웹소켓 서버 방식

const wsServer= SocketIO(httpServer); 소켓아이오 서버 방식 매우 흡사하다.

 

 

socketio백엔드 연결 준비 

 

server.js 

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

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

 

 

socketio프론트 연결 준비 

pug에 

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

app.js

const socket = io(); //위에 설치하면 프론트에 io라는 기능이 생김 자동으로 실행하고 있는 socketio서버를 찾는기능

 

 

연결확인

front에 console에 이상 없는지 확인,

back-end에 보면 직접 sockets=[]배열에 넣은 webSocket과 다르게 자동으로 접속한 서버 감지 

 

 

pug

            div#welcome
                form 
                    input(placeholder="room name", required, type="text")
                    btton Enter Room 
app.js

webSocket과 달리 socket.send()로 항상 메세지로 보내고 해당 sokect.on("message",)텍스트 메세지로 받아 JSON.Stringify 할 필요없이

event이름과 object을 socket.emit(text,argment1,argument2,argument2)처럼 Json,텍스트,숫자,boolean등으로 여러개의 인자를 보낼 수 있음

마지막인자에 끝날 때 실행되는 함수

 ,server에서 호출하지만 front-end에서 실행되는 function보냄 

 

백엔드에서도 

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

};// 이벤트이름과 함수를 적어주면 됨 
})

app.js

function backendDone(msg){
    console.log("backend says:  ", msg );
}

function hanldeRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room",input.value, backendDone  );
    input.value="";
}// 실행하면 백 콘솔에 rommName 나오고 , front console에 backend says hello 라고 뜸 

 

절대 주의! 마지막 function(done)을 실행시킬 때 프론트엔드function을  백엔드 여기서, 실행하는것이 아니고

프론트에있는 버튼을 대신 눌려주는 거라고 생각. 프론트에서 실행함 

백엔드에서 인자를 같이 보내서 실행 할 수있음. 

 

Room

 

socket.io는 기본적으로 room을 제공.

socket.join("룸이름")

 

socket.onAny() :미들웨어 같은 것 어느 이벤트건간에 console.log하게 해줌 

 

wsServer.on("connection", (socket) =>{
    socket.onAny((event)=>{
        console.log(`Socket event: ${event}`);  //   Socket event: enter_room

    })
    socket.on("enter_room", (roomName, done)=> {

        console.log(socket.id);                //fEHdwQp9pOYtxHfDAAAB 
         console.log(socket.rooms);      //Set { 'fEHdwQp9pOYtxHfDAAAB' }
         socket.join(roomName);
         console.log(socket.rooms);     //Set { 'fEHdwQp9pOYtxHfDAAAB', '1234' }
         setTimeout(()=>{
            done("hello");
         },1000);
       });
})

 

socket.join, leave, 방에다가 이벤트와 메세지를 보낼 수 있다.

 

 

 

방제 정하고 입장하면 

방제 제목 form 사라지고 , msg from 생기고, roomName 나타내기

 

const socket = io();

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

room.hidden=true;

let roomName;

function showRoom(msg){
    room.hidden=false;
    welcome.hidden = true;
    const h3 = room.querySelector("h3");
    h3.innerText= `Room ${roomName}`;
}

function hanldeRoomSubmit(event){
    event.preventDefault();
    const input = form.querySelector("input");
    socket.emit("enter_room",input.value, showRoom  );
    roomName=input.value;
    input.value="";
}

 

server-api/socket-to-room

io.on("connection", (socket) => {

  // to one room
  socket.to("others").emit("an event", { some: "data" });

  // to multiple rooms
  socket.to("room1").to("room2").emit("hello");

  // or with an array
  socket.to(["room1", "room2"]).emit("hello");

  // a private message to another socket
  socket.to(/* another socket id */).emit("hey");

  // WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
  // named `socket.id` but the sender. Please use the classic `socket.emit()` instead.
});

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"); //roomname room에 들어가면 welcome이라는 이벤트 발생
});
})

 

 

app.js

 

function addMessage(msg){ //자주 쓸 내용이라 함수로 따로 만듦
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText=msg;
    ul.appendChild(li);
}

socket.on("welcome", ()=>{

   addMessage("Someone Joined!")

});

 

나를 제외한 모든 socket에  Someone Joined라고 알람이 감 

 

이벤트 

disconnecting

This event is similar to disconnect but is fired a bit earlier, when the Socket#rooms set is not empty yet

완전히 서버와 연결이 끊나기 전에 

 

 

app.js

socket.on("bye",()=>{
    addMessage("Someone Left!")
});

 

server.js

    socket.on("disconnecting",()=>{
        socket.rooms.forEach((room)=> socket.to(room).emit("bye")); //socket.rooms하면 각각의 id 배열이 나옴 
    })

 

disconnect : 연결이 완전히 끊어졌을때 발생하는 이벤트 (room 정보가 비어있음)
disconnecting : 브라우져는 이미 닫았지만 아직 연결이 끊어지지 않은 그 찰나에
발생하는 이벤트 (그래서 room 정보가 살아있음)
--------------------------------------------------------------------

Set(2) { "asdfasdfasdf" , "room" } 에서
Set은 자바스크립트 자료구조 중 하나 입니다.
중복되는 data를 포함할 수 없고,
Index가 적용이 안되고,
forEach 사용이 가능합니다.

 

 

 

내가 메세지 보내기 

 

app.js

 

function showRoom(msg){
    room.hidden=false;
    welcome.hidden = true;
    const h3 = room.querySelector("h3");
    h3.innerText= `Room ${roomName}`;
    const form = room.querySelector("form");
    form.addEventListener("submit", hanldeMessageSubmit);
}

function hanldeMessageSubmit(event){
    event.preventDefault();
    const input = room.querySelector("input");
    socket.emit("new_message", input.value, roomName, ()=>{
        addMessage(`You ${input.value}`); //함수로 백에 넘겨주는 이유는 그냥 바로 프론트에 나타내는 것이아닌 서버에 내용을 전달하여 해당 room에 있는 전체 사람들에게 socket.to하여 전달하여 나타내기위해인듯하다.
    }); //백엔드에 new_message라는 이벤트를 발생 시키고잇음
}

 

server.js

    socket.on("new_message",(msg,room,done)=>{
        socket.to(room).emit("new_message", msg);
        done(); //백엔드에서 시켜서 프런트에 발생하는 이벤트
    })
})

 

닉네임 설정하기 

server.js

wsServer.on("connection", (socket) =>{
    socket["nickname"]="익명";
    socket.onAny((event)=>{
        console.log(`Socket event: ${event}`);
    })
    socket.on("enter_room", (roomName, done)=> {
    socket.join(roomName);
    done();
    socket.to(roomName).emit("welcome",socket.nickname);
    });
    socket.on("disconnecting",( )=>{
        socket.rooms.forEach((room)=> socket.to(room).emit("bye",socket.nickname));
    })
    socket.on("new_message",(msg,room,done)=>{
        socket.to(room).emit("new_message", `${socket.nickname} :` msg);
        done(); 
    })
    socket.on("nickname", ((nickname) => (socket["nickname"]= nickname)))
})

 

 

socket.on("enter_room", (roomName, done)=> {
    socket.join(roomName);
    done();
    socket.to(roomName).emit("welcome",socket.nickname);
    });
    socket.on("disconnecting",( )=>{
        socket.rooms.forEach((room)=> socket.to(room).emit("bye",socket.nickname));
    })

 

 

app.js

function showRoom(msg){
    room.hidden=false;
    welcome.hidden = true;
    const h3 = room.querySelector("h3");
    h3.innerText= `Room ${roomName}`;
    const msgForm = room.querySelector("#msg");
    const nameForm = room.querySelector("#name");
    msgForm.addEventListener("submit", hanldeMessageSubmit);
    nameForm.addEventListener("submit", handleNicknameSubmit);

}
function handleNicknameSubmit(event){
    event.preventDefault();
    const input = room.querySelector("#name input");
    socket.emit("nickname", input.value); //계속 메시지를 보내도 익명으로 나오고 socket.onAny에 socketevent가 안떳다.. 이유는 emit으로 이벤트를 발생시켜야하는데 socket.on을 작성했던것,,! 

    console.log(input.value);
}


socket.on("welcome",(user)=>{
    addMessage(`${user}님이 입장하였습니다.` )
});
socket.on("bye",(left)=>{
    addMessage(` ${left}님이  나갔습니다.!`)
});

 

Adapter

다른 서버들 사이에 실시간 어플리케이션을 동기화하는 것 

 

우린 데이터베이스에 아무 것도 저장하고 있지않고 서버의 메모리에서 adapter을 사용하고있음

우리가 서버를 종료하고 시작할 때 모든 방과 메시지, 소켓은 사라진다.

재시작하면 사라짐 

 

adapter은 누가 연결 되었는지,  현재 어플리케이션에 room이 얼마나 있는지 알려줌

wsServer.sockets.adapter하면 볼 수있는 것들 중

rooms : 어플리케이션에있는 모든 rooms 볼 수있다.

sids : socket ids 볼 수있음

 

모든 sockets은 private room이 있다.

그건 id가 방제목인 경우 그래서 우리가 private message를 보낼 수 있다.

 

Each Socket in Socket.IO is identified by a random, unguessable, unique identifier Socket#id. For your convenience, each socket automatically joins a room identified by its own id.

 

 

 

새로운 방이 생겼다고 모두에게 알리기wsServer.sockets.emit

socket.emit()이 아니라 

server.sockets.emit()사용 : 연결 된 모든 소켓에게 사용됨

서버에서 모든 소켓에게 보내는 것 : broadcast

wsServer.on("connection", (socket) =>{
   ("room_change", publicRooms()); //룸 체인지 emit을 방에 들어갈때나 디스커넥트 상황 외에 젤 처음 커넥트 할때도 emit할 수 있게 하면 새로 서버에 연결된 클라이언트도 실시간으로 변경된 방 내역을 볼 수 있음.
'''
}

function publicRooms() {
    const {
      sockets: {
        adapter: { sids, rooms },
      },
    } = wsServer;
    const publicRooms = [];
    rooms.forEach((_, key) => {
      if (sids.get(key) === undefined) {
        publicRooms.push(key);
      }
    });
    return publicRooms;
  }

 

    socket.on("enter_room", (roomName, done)=> {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome", socket.nickname);
        wsServer.sockets.emit("room_change", publicRooms());
    });

 

    socket.on("disconnect",()=>{
        wsServer.sockets.emit("room_change", publicRooms());
    })

 

app.js

socket.on("room_change", (rooms)=>{
    const roomList = welcome.querySelector("ul");
    roomList.innerText ="";

    rooms.forEach((room)=>{
        const li = document.createElement("li");
        li.innerText= room;
        roomList.append(li);
    })
});

 

방 안에 있는 사람들의 수 세기 

 

map의 set은 중복되지않는 값을 가진다.

우리가 가지고 있는 방들 중에 key 값을 가져와서 찾은 다음 set.size한다.

 

해당 key값을 가진 방의 사람 수 세기 함수 

function countRoom(roomName){
    return wsServer.sockets.adapter.rooms.get(roomName)?.size //Optional chaining
}

 

이것을 사람이 들어 오고 나갈 때 적용 할 것임

 

    socket.on("enter_room", (roomName, done)=> {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome", socket.nickname, countRoom(roomName));
        wsServer.sockets.emit("room_change", publicRooms());
    });

 

    socket.on("disconnecting",( )=>{
        socket.rooms.forEach((room)=> socket.to(room).emit("bye", socket.nickname, countRoom(room) - 1));
    });//나가기 전에! disconnect일 때 적용하면 room의 정보를 모름 그리고 나가기전에 count하면 나까지 포함되므로 -1

 

 

socket.on("welcome", (user,newCount)=>{
    addMessage(`${user}님이 입장하였습니다.` );
    const h3 = room.querySelector("h3");
    h3.innerText= `Room ${roomName} (${newCount})`;
});
socket.on("bye", (left, newCount)=>{
    addMessage(` ${left}님이  나갔습니다.`);
    const h3 = room.querySelector("h3");
    h3.innerText= `Room ${roomName} (${newCount})`;
});
socket.on("new_

 

 

1) socket.to(roomName).emit("enter_room", socket.nickname, countRoom(roomName));
-> 방에서 자신을 제외한 모두에게 브로드캐스팅하여 메시지를 전달
2) wsServer.to(roomName).emit("enter_room", socket.nickname, countRoom(roomName));
-> 방에서 자신을 포함한 모든 유저에게 메시지를 전달합니다.

자신이 방에 입장했을 때 유저 카운트가 몇인지 확인하고 싶다면 2번도 적절히 섞어가며 사용하면 좋을 것 같습니다

 

 

Admin UI

socket.io 백엔드를 위한 것 

소켓,룸,클라이언트 등 확인가능

https://socket.io/docs/v4/admin-ui/#current-features

 

Admin UI | Socket.IO

The Socket.IO admin UI can be used to have an overview of the state of your Socket.IO deployment.

socket.io

 

npm i @socket.io/admin-ui

 

설정/ 위에 페이지 참고

import http from "http";
import {Server} from "socket.io";
import { instrument } from "@socket.io/admin-ui";
import express from "express";


const httpServer = http.createServer(app);
const wsServer = new Server(httpServer,{
    cors: {
        origin: ["https://admin.socket.io"],
        credentials: true
    }
});
instrument(wsServer, {
    auth: false
  });
https://github.com/UHyun-K/zoomClone/commit/f4065d6a59338160686e7e620c28b239465c4654