#0 개요
실시간 채팅으로 이루어지는 LLM 참가 게임을 만들기 때문에 통신에 대해서 이해가 필요하다고 생각했다.
따라서 websocket과 webRTC를 사용하는 간단한 프로젝트를 구현해보면서 여러가지 조사하여 기술을 정리하고자 한다.
간단한 http 비교 개요는 다음과 같다
#1 WebSocket
프로토콜은 네트워크나 통신 환경에서 데이터를 전송·수신하기 위한 규칙과 절차를 정의한 약속을 말한다.
http와 websocket 둘 다 이러한 프로토콜의 한 종류이다.
http의 핵심은 stateless로, 벡엔드가 유저(클라이언트)를 기억하지 않는다는 것.
request를 받으면 response후에 상태를 저장하지 않아서 효율적인 통신이 가능하다. 단 실시간 통신에는 부적절.
따라서 인증 기능을 쿠키 등으로 구현하게 된다.
webSoket은 기본적으로 wss://url 과 같은 구조로 통신하도록 약속된 프로토콜이다.
양방향 통신이기 때문에 서로에게 통신을 보낼 수 있게 된다.
통신 규약이기 때문에 서버 - 클라이언트 뿐만 아니라 서버 - 서버 간에도 구현 가능한 기술.
#2 ws 라이브러리를 활용한 채팅방 실습
npm i ws
1. 프로젝트 구조가 어떻게 될까?
현재 express 서버는 http 서버이다. 즉, 서버가 http 통신 기능만 있는 상태이다.
따라서 http와 ws 둘 다 가능하도록 구조를 만들어줘야 한다.
app.listen(3000, handleListen);
기존에 작성된 다음 서버 시작 구조를, 세분화 하여 서버를 시작하도록 바꾼다.
import http from "http"
const app = express();
const server = http.createServer(app)
인자로 request listner를 받는 createServer
이제 이 server에 설정을 추가해주는 것.
ws 서버만 돌리려면 위의 http 서버를 만들어주는 과정은 필요 없다.
하지만 같이 돌리려면 아래와 같다.
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("/"));
console.log("hi")
const handleListen = () => console.log(`Listening on http://localhost:3000 and ws://localhost:3000`);
const server = http.createServer(app);
const webSocketServer = new WebSocket.Server({server});
server.listen(3000, handleListen)
2. FE BE 연동하기
먼저 프론트에서는 websoket 표준 api에 구현되어 있는 것을 어느정도 사용하게 된다.
const socket = new WebSocket(`ws://${window.location.host}`)
위 api 를 참고한다.
BE에서는 특정 이벤트를 트리거로 함수를 수행하는 과정과 유사하다. 즉 FE의 이벤트 리스너 구조와 비슷하다.
첫번째 인자로 event를 받는걸 볼 수 있다.
두번째 매개변수의 인자로 들어오는 것 중 socket은 연결된 특정 사람(통신 라인) 이라고 할 수 있다.
event 정보가 e로 들어오는 것처럼 두번째 함수에 자동으로 들어온다.
const server = http.createServer(app);
const webSocketServer = new WebSocket.Server({server});
const connectionEvet = (socket) => {
console.log(socket)
}
webSocketServer.on("connection", connectionEvet)
server.listen(3000, handleListen)
위 코드로 socket 정보를 찍어보면
위와 같이 socket 정보가 나오는 걸 알 수 있다.
이는 연결된 브라우저의 정보라고 생각하면 된다.
socket은 다양한 기능을 수행할 수 있는 메서드를 포함하는 객체이다.
두 주체의 연결을 확인하면 다음과 같다.
서버 코드:
const server = http.createServer(app);
const webSocketServer = new WebSocket.Server({server});
webSocketServer.on("connection", (socket) => {
console.log("서버가 브라우저에 연결되었습니다!")
socket.on("close", () => {console.log("클라이언트랑 연결이 끊어졌다.")})
socket.send("클라이언트에 보내는 메세지!")
})
server.listen(3000, handleListen)
클라이언트 코드:
const socket = new WebSocket(`ws://${window.location.host}`)
socket.addEventListener("open", () => {
console.log("브라우저가 서버에 연결되었습니다!")
})
socket.addEventListener("message", (message)=>{
console.log("서버로부터 받은 메세지: " + message.data)
})
socket.addEventListener("close", ()=>{
console.log("서버와 연결이 끊어졌습니다.")
})
setTimeout(() => {
socket.send("hello from the browser!");
}, 10000);
서로 다른 두 코드에서 socket은 이름만 같을 뿐, 정보를 주고 받는 두 통신 주체로 분리된 주체이다.
socket.on("message", (message)=>{})
서버는 위 코드로 브라우저가 보내는 통신을 받는다.
한마디로 기존 http는 브라우저가 데이터를 보내기만 할 수 있으니 req, 서버는 받아서 답장밖에 못하니 res였는데,
이건 양측 모두 socket.send() 와 socket.on() / socket.send()와 socket.addEventListener()를 가지고 있다.
3. chat 구현하기
1) form으로 ui 구현
2) 프론트의 JS에서 해당 html 요소와 값을 불러오기.
submit event를 받아서 데이터를 백으로 보내기.
const messageList = document.querySelector("ul");
const messageForm = document.querySelector("form");
const socket = new WebSocket(`ws://${window.location.host}`)
function handleSubmit(event) {
event.preventDefault();
const input = messageForm.querySelector("input");
socket.send(input.value);
input.value = "";
}
messageForm.addEventListener("submit", handleSubmit);
3) 하나의 클라이언트에서 받은 정보를 백엔드에서 모든 클라이언트에게 보내줘야 한다.
=> 모든 클라이언트 정보를 담은 DB가 필요하다 == 모든 Socket을 담은 DB가 필요하다.
이후 저장한 Socket에 일괄적으로 FE에서 받은 데이터를 뿌려주면 된다.
4) 다만 BE는 자신이 FE로부터 받는 데이터(message)가 어떤 종류인지 구분하는 기능이 필요하다. 단순 채팅인지, 닉네임인지 등등.
=> 따라서 json 형식으로 데이터를 보내게 된다.
{
type:"message",
contents: "this is message. not nickname"
}
JSON.stringify()를 사용한다.
json 구조를 string으로 바꿔주는 이유는, 최대한 범용성 있는 타입을 사용하여 어떤 서버이든, 환경이든 정보를 주고받을 수 있도록 만들어주기 위함이다.
#3 webSocket vs Socket io
1. Socket.io 정의
실시간, 양방향, 이벤트 기반 소켓 통신을 제공하는 프레임워크이다.
프레임워크라는 건 곧 많은 기본 골조 코드가 있고, 개발자가 그것에 맞추어서 개발해야한다는 것.
다만 여기서는 유연성이 부족하다는 뜻은 아닐 수 있는데,
그건 socket.io는 이러한 통신이 가능하게 해주는 방법 중 하나로서 webSocket을 사용하기 때문.
즉 webSocket이 안되는 경우에도 이러한 기능을 제공한다는 것이다.
(webSocket을 연결할 수 없는 경우에는 HTTP long polling 등을 사용한다.
2. Soket.io 사용하기
npm i socket.io
socket.io 로 서버 작동시 http://localhost:3000/socket.io/socket.io.js 이라는 url을 자체적으로 생성한다.
이는 긴 파일을 제공하는데,
websocket 외에도 socket.io에만 구현된 기능을 수행하기 위해 브라우저가 알아야 하는 코드들을 추가한다.
이후 클라이언트에 다음 코드를 추가한다.
script(src="/socket.io/socket.io.js")
서버와 따로 작동하도록 하고 싶으면
socket.io-client를 다운받으면 된다.
이제 서버는 아래의 코드로 기본적으로 요청을 받기 시작하고,
webSocketServer.on("connection", (socket) => {
console.log(socket);
});
클라이언트는 가져온 js 파일에서 io() function을 사용한다.
const socket = io();
3. room 구현하기
1) 방을 만드는 것 === 참여자가 0명인 방에 들어가는 것.
기존에 있는 방을 들어가는 것과 같은 로직이다.
먼저 클라이언트 코드이다.
const form = welcome.querySelector("form");
function handleRoomSubmit(event) {
event.preventDefault();
const input = form.querySelector("input");
socket.emit("enter_room", { payload: input.value }, () => {
console.log("server is done!");
});
input.value = "";
}
form.addEventListener("submit", handleRoomSubmit);
socket.emit() 의 인자로 첫번째는 사용자 지정 이벤트 명, 두번째 인자로는 쌩 object를 넣어 줄 수 있게 됐다. 또 이후 인자를 무한대로 넣어서 여러가지 값을 전달할 수 있다.
마지막 인자로(보통은 세번째 인자)는 callback 함수를 넣어줄 수도 있다.
아래는 서버 코드
wsServer.on("connection", (socket) => {
socket.on("enter_room", (msg, done) => {
console.log(msg);
setTimeout(() => {
done();
}, 10000);
});
});
server에서도 이를 그대로 받아주면 된다.
msg가 클라이언트에서 보낸 json이 되고, done이 클리이언트에서 보낸 세번째 callback함수가 된다.
물론 backend는 frontend에서 가져온 코드를 "실행" 해서는 안된다. 엄청난 보안 문제가 발생하니까.
Server에서 해당 코드를 실행하면, 이는 곧 FE로 코드가 가서 실행한다는 의미이다.
2) socket group
결국 정보를 주고받을 group을 만들어야한다.
https://socket.io/docs/v3/server-api/
Server API | Socket.IO
Server
socket.io
console.log(socket.rooms) : 현재 생성된 룸들을 볼 수 있다.
socket.join("방이름") : 해당 이름의 룸으로 들어갈 수 있다. (없으면 만드는 것)
+ 기본적으로 모든 소켓은 socket.id를 작성하면 나오는 고유 아이디 명으로 private room을 하나씩 가지고 있다.
결국 done을 통해서 채팅을 보내고 난 이후 client에 변화를 적용하게 된다.
3) 이번에는 server에서 일방적으로 통신을 시작하는 경우이다.
socket.to(roomName).emit("sendMessage");
위 코드로 server에서 이벤트를 발생시키면
4) soket.on("disconnecting") 서버단에서 클라이언트의 연결 끊어짐을 인식.
사전에 존재하는 이벤트인 "disconnecting"을 감지할 수 있음.
"disconnecting"은 끊어지기 직전이고, "disconnect"는 끊어진 이후의 이벤트이다.
+ socket은 새로고침만 해도 연결이 끊어진다.
#4 Adapter 개념
1. 개념
여러 서버를 돌리게 되면 서로 다른 서버에서 클라이언트들은 socket을 볼 수 없다. 따라서 하나 더 단계를 거쳐서 소통 및 관리할 수 있도록 해준다.
2. console.log( server.sockets.adapter )
+ server에서 확인하는 admin pannel이 있다.
'프로젝트 기록 > LLM 게임 프로젝트' 카테고리의 다른 글
개발 리뷰 -11- 게임 채팅 페이지 개발 (0) | 2025.01.23 |
---|---|
개발 리뷰 -10- 메인 페이지 개발 (0) | 2025.01.23 |
개발 리뷰 -8- FE 로그인, 로그아웃 개발 (0) | 2025.01.13 |
개발 리뷰 -4- FE 개발 환경 세팅 (0) | 2025.01.06 |
개발 리뷰 -3- 화면 설계 (figma) (0) | 2025.01.03 |