[ZoomCloneCode] Websocket 채팅구현

junghan·2022년 12월 12일
0

zoom클론코딩

목록 보기
2/4
post-thumbnail

websocket이란?

웹소켓은 하나의 TCP접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜입니다. RFC 6455 명세서로 표준화된 프로토콜이며, 웹소켓(WebSocket)은 HTTP와 마찬가지로 OSI모델의 제 7계층에 위치해 있으며, 제 4계층의 TCP에 의존합니다.

웹소켓은 HTTP 포트 80과 443 위에 동작하도록 설계되었으며 HTTP 프록시 및 중간 층을 지원하도록 설계되었으므로 HTTP 프로토콜과 호환이 됩니다. 다만 호환을 하기 위해서는 HTTP 업그레이드 헤더와 함께 웹소켓 핸드쉐이크를 사용하여 HTTP프로토콜에서 웹소켓 프로토콜로 변경해야합니다.

웹소켓 프로토콜은 HTTP 폴링과 같은 반이중방식에 비해 더 낮은 부하를 사용하여 웹 브라우저(또는 다른 클라이언트 애플리케이션)과 웹 서버 간의 통신을 가능케 하며, 서버와의 실시간 데이터 전송을 용이하도록 합니다. 이는 먼저 클라이언트에 의해 요청을 받는 방식이 아닌, 서버가 내용을 클라이언트에 보내는 표준화된 방식을 제공함으로써, 또 연결이 유지된 상태에서 '패킷'형태인 메시지들을 오갈 수 있게 허용함으로써 가능하게 할 수 있습니다. 이러한 방식으로 클라이언트와 서버 간에 양방향 통신을 수행합니다.

이런 특징 때문에 웹소켓은 실시간 채팅이나 온라인 게임, 주식 트레이딩 시스템같이 끊김없이 데이터 교환이 지속적으로 이뤄져야 하는 서비스에 주로 사용됩니다.


websocket동작방식

백엔드 서버에서 WS 패키지를 설치하고 프론트 엔드 서버에서 Websocket API를 가져와서 세팅한 뒤, 실행을 시키면 위와 같은 핸드쉐이크 방식을 통해 연결이 이뤄집니다.

아래는 최초 연결을 수행할 시, 클라이언트에서서버로 보내는 HTTP Upgrade 요청

GET ws://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Sec-WebSocket-Key: ekqDd2U76RqH042ZiJiPmg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

백엔드 서버에서 보낸 upgrage 응답 (101)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: LvI2a1HuNYx/pKnHxse/3MV3+OE=

핸드 셰크이가 성공하면, 어느 한쪽에서 Disconnect요청을 날리기 전 까지 지속적으로 양방향 통신을 하게 됩니다.


백엔드 서버 구축

클라이언트와의 연결

  1. node.js의 웹소켓 라이브러리 WS 설치
    패키지: https://www.npmjs.com/package/ws

WS는 node.js에서 websocket을 조금 더 편하게 구축할 수 있도록 도와주는 패키지입니다.

	$ npm i ws 
  1. 웹소켓서버 생성
const wss = new WebSocket.Server({ server }); 

백엔드 websocket 연결코드

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

// app.listen(3000, handleListen);
//express환경에서 http와 ws를 둘다 돌리는 작업
//필수사항은 아님 여기서는 동일한 포트에서 두가지 작업을 처리하기 위해 설정
const server = http.createServer(app); //ws를 쓰기위해 express로부터 http서버를 생성
const wss = new WebSocket.Server({ server }); //브라우저와 서버의 연결

//const wss = new WebSocketServer({ port: 8080 }); 포트번호를 다르게 쓰려면 이렇게 사용

server.listen(3000, handleListen);

클라이언트와 소통

  1. 클라이언트와 연결이 되었는지 확인
wss.on("connection", (socket) => {});

클라이언트로 부터 메시지 수신하는 함수

  • socket.on("", () => {});
    아래의 이벤트들을 감지하여 콜백을 통해 처리해줄 수 있습니다.

  • socket.send("");
    파라미터로 보내기 원하는 인자를 보낼 수 있습니다. send() 메서드는 텍스트나 blob같은 이진데이터들만 보내기 때문에 일반적으로 string 형태로 데이터를 전송합니다.

클라이언트와 소통하는 백엔드 서버 코드

wss.on("connection", (socket) => {
  console.log("Connected to Browser V");
  socket.on("close", () => console.log("Disconnected from browser X")); //브라우저와의 연결이 끊어지는 이벤트체크
  socket.on("message", (message) => console.log(message.toString("utf8"))); //브라우저로부터 받는 메시지가 blob형태 이므로 string으로 변환
  socket.send("hello!"); //소켓 고유의 send메시지로 브라우저에게 메시지 송신
});

프론트 엔드 서버 구축

백엔드와 연결

  1. Websocket API 설정
    WebSocket API는 사용자의 브라우저와 서버 간에 양방향 대화형 통신 세션을 열 수 있게 해주는 고급 기술입니다. 이 API를 사용하면 회신을 위해 서버를 폴링하지 않고도 서버에 메시지를 보내고 이벤트 기반 응답을 받을 수 있습니다.
WebSocket(`ws://${window.location.host}`);
  1. Websocket 연결 확인
    기본적으로 이벤트를 감지하는 방식으로 작동합니다.
socket.addEventListener("open", () => {
  console.log("Connected to Server V");
}); //server와 연결을 시작

백엔드와 연결되는 프론트 엔드 코드

const socket = new WebSocket(`ws://${window.location.host}`); //브라우저에서 지원하는 웹소켓
//localhost:3000 => window.location.host 모바일을 지원하기 위해 변경
//연결할 서버 소켓

socket.addEventListener("open", () => {
  console.log("Connected to Server V");
}); //server와 연결을 시작

백엔드와 소통

백엔드 코드와 비슷하게 addEventListener()함수를 통해 이벤트를 감지하여 백엔드에서 전송되는 데이터를 수신하고 .send()함수를 통해 데이터를 전송합니다.

프론트 엔드와 연결되는 백엔드 코드

socket.addEventListener("message", (message) => {
  console.log("Just got this: ", message, "from the server");
});

socket.addEventListener("close", () => {
  console.log("Disconnected to Server V");
}); //server와 연결을 시작

setTimeout(() => {
  socket.send("hello from the browser");
}, 10000);

실시간 채팅 구현

메시지 입력을 위한 html수정

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 
            form#nick
                input(type="text", placeholder="choose a nickname", required)
                button Save
            ul
            form#message
                input(type="text", placeholder="write a msg", required)
                button Send

        script(src="/public/js/app.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");

// app.listen(3000, handleListen);
//express환경에서 http와 ws를 둘다 돌리는 작업
//필수사항은 아님 여기서는 동일한 포트에서 두가지 작업을 처리하기 위해 설정
const server = http.createServer(app); //ws를 쓰기위해 express로부터 http서버를 생성
const wss = new WebSocket.Server({ server }); //브라우저와 서버의 연결
//브라우저 상에서 websocket의 open이벤트로 감지가능

//socket은 누군가와 연결될지 판단하는 연락망
//wss의 connection 이벤트 감지

const sockets = []; //서버와 연결된 브라우저 소켓들

wss.on("connection", (socket) => {
  sockets.push(socket);
  socket["nickname"] = "Anon"; //닉네임 초기화
  console.log("Connected to Browser V");
  socket.on("close", () => console.log("Disconnected from browser X")); //브라우저와의 연결이 끊어지는 이벤트체크
  socket.on("message", (msg) => {
    const message = JSON.parse(msg); //string을 다시 객체화

    switch (message.type) {
      case "new_message": //타입에 따라 메시지처리
        sockets.forEach((aSocket) =>
          aSocket.send(`${socket.nickname}: ${message.payload}`)
        );
        break;
      case "nickname": //타입에 따라 닉네임처리
        socket["nickname"] = message.payload; //소켓 객체에 닉네임을 추가
        break;
    }
  });
});
//브라우저와 연결된 소켓

server.listen(3000, handleListen);

프론트엔드

app.js파일

const messageList = document.querySelector("ul");
const nickForm = document.querySelector("#nick");
const messageForm = document.querySelector("#message"); //html의 기능을 재정의

const socket = new WebSocket(`ws\://${window.location.host}`); //브라우저에서 지원하는 웹소켓
//localhost:3000 => window.location.host 모바일을 지원하기 위해 변경
//연결할 서버 소켓

//소켓으로 보낸 메시지의 성격을 구분하기 위해 이벤트명을 작성
function makeMessage(type, payload) {
  const msg = { type, payload };
  return JSON.stringify(msg);
  // websocket API가 브라우저에서 제공하는 것이기 때문에, 환경을 유연하게 둬야함
  //응답을 받는 서버가 통일되지 않은 상태 즉, js서버가 아닐 수 있기에, string으로 보내야 처리가 가능
}

socket.addEventListener("open", () => {
  console.log("Connected to Server V");
}); //server와 연결을 시작

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

socket.addEventListener("close", () => {
  console.log("Disconnected to Server V");
}); //server와 연결을 시작

function handleSubmit(event) {
  event.preventDefault(); //기본동작을 막아줌 행동 재정의
  const input = messageForm.querySelector("input");
  socket.send(makeMessage("new_message", input.value)); //입력한 값을 서버로 전송
  input.value = "";
}

function handleNickSubmit(event) {
  event.preventDefault();
  const input = nickForm.querySelector("input");
  socket.send(makeMessage("nickname", input.value));
  input.value = "";
}

messageForm.addEventListener("submit", handleSubmit); //버튼 초기화
nickForm.addEventListener("submit", handleNickSubmit);

WS는 제한된 이벤트만을 감지하기 때문에 직접 객체를 만들어 이벤트를 관리해야하기에 다양한 이벤트처리하는게 까다롭고, 자기 자신을 제외한 이벤트 전송을 해야할 때, 직접 일일이 구분하여 데이터를 전송해야는 불편함이 있습니다.

그리하여 이를 쉽게 처리해주게 위한 방법으로는 Socket.io 프레임워크가 있습니다. 해당 프레임워크를 사용하면 간편한 이벤트 관리 뿐만아니라 채팅방을 위한 room 생성 및 브로드캐스팅까지 지원하기 때문에 간편하게 이벤트를 처리할 수 있습니다.

ref:
https://nomadcoders.co/noom
https://hudi.blog/websocket-with-nodejs/
https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API
https://blog.naver.com/PostView.naver?blogId=marketopsry&logNo=222009736698&from=search&redirect=Log&widgetTypeCall=true&directAccess=false

profile
42seoul, blockchain, web 3.0

0개의 댓글