아직 웹소켓에 대해서 완벽하게 이해하지 못했지만 되는 한으로 정리를 해볼까 한다.
io => 서버에 접속하는 모든 유저를 위한 이벤트 처리
socket => 하나의 유저를 대상으로 한 이벤트 처리
먼저 확인해봐야 하는 것은 express.static('public')이다.
이 문법에 대해서 찾아본 결과 'public' 이라는 경로에 들어있는 파일들을 전부다 로드 할 수 있게끔 해준다고 한다.
이때 아무리 파일을 로드해준다고 하지만 URL이나 경로가 들어가있지 않은 이유는 정적 디렉터리에서 상대적인 파일을 조회하기 때문이라고 한다.
여기서 나오는 정적 디렉터리란..
미들웨어에 사용되어 직접 내부 디렉터리내의 파일들을 그대로 보여주는 것이라고 한다.
이렇게 public에 들어간 클라이언트 코드가 전부다 불러와져서 서버를 실행하였을 때 볼 수 있게 된다.
그리고 이후에 서버를 실행하면서 웹소켓 연결을 사용할 수 있도록 설정해줄 initSocket 함수를 불러와 준다.
여기서 io.attach가 무엇이냐에 대해서 검색도 한번 해봤는데,
attach 메서드는 현재 실행되는 웹 서버와 동시에 웹소켓 연결을 사용할 수 있게 해주는 것이라고 한다.
assets 폴더에는 각각 아이템과 아이템 해금, 스테이지에 대한 정보를 저장하는 json 파일들이 모여있다.
이 파일들을 불러오기 위하여 서버가 실행되었을 때 한번에 처리하는 init 폴더에 assets.js 파일을 만든다.
먼저 이 assets.js에서 해당 assets 폴더의 위치를 찾아야만 한다.
파일의 경로를 표시하여 불러올 수 도 있겠지만, path나 url 라이브러리를 import 하여 기능을 처리하고자 한다.
순서대로
1.filename -> fileURLTOPath(import.meta.url)은 현재 파일의 절대 경로를 받아온다.(RealTimePlatFormerGame\src\init\assets.js)
2.dirname -> path.dirname(filename) 은 현재 디렉터리의 위치를 가져온다.(RealTimePlatFormerGame\src\init)
이렇게 경로들을 다 받아왔다면..
3.basepath -> path.join(dirname, '../../assets'); 는 현재 디렉터리에서 assets 파일의 경로를 가져온다.
import assets from '../../assets' 이런식으로 써도 되지 않았을까 라고 생각하고 있다.
이렇게 다 불러왔다면 다음 걸로 가기전에 개념들에 대해서 알아야 할 필요가 있다.
1.path.join(경로, .. .): 여러 인자를 넣으면 하나의 경로로 합쳐준다.
2.fs.readFile(파일 경로, 옵션, 콜백함수): 비동기식으로 파일경로에 해당하는 모든 파일들을 읽고나서 콜백함수를 실행하는 것이다. 우리는 여기서 콜백함수를 에러처리용으로 사용할 것이다.
3.promise.all(): 내부에 입력되는 약속들 중에 하나라도 실행이 제대로 되지 않는다면 거부된다.
4.우리는 이런 assets파일들을 서버가 실행할 때 비동기 병렬으로 읽어와야만 한다!!
이것들을 고려하고 다음 문장들을 봐보도록 하자.
읽은 데이터 테이블을 서버 메모리에 로드하는 함수이다.
이때 파일을 비동기적으로 읽어오기 위하여 promise 함수를 사용하였고 내부에 path.join을 통하여 basepath인 assets파일의 경로에다가 filename을 붙어서 하나의 경로로 만들어 파일을 읽는 것으로 하였고 이것이 제대로 성공했을 경우엔 promise의 처리가 성공하였음을 알리는 resolve문에다가 json 형식의 data를 문자열 또는 객체로 변환하여 넘겨주게 된다. (아니라면 에러 발생하고 실패(reject)를 보내준다.)
이렇게 파일을 읽어왔다면?
이런식으로 파일들의 이름을 한번에 비동기 병렬로 다 읽어서(promise.all)
이것을 gameAssets에 저장하고 리턴해주는데 이것이 어디서 처리가 될까? 정답은 서버가 실행되는 부분이다.
여기서 아까 읽은 파일들을 가져와서 저장하는 것으로 서버가 실행이 되었을 때 같이 호출 될 수가 있게 된다!
먼저 웹소켓의 동작 과정을 이해할 필요가 있는데,
웹 소켓 통신이 가능해진 서버에 접속할 때 자동으로 'connection' 이벤트가 발생하여 서버에 전송이 된다.
이 전송된 connection 이벤트를 서버에서 처리하려고 한다면? => 서버가 열릴때 웹소켓에 달아줘야 한다.
게임을 시작하면서 실행되어야되는 핸들러 기능들이 다 여기에 들어가 있다.
이렇게 conection이 발생하였을 때 처리할 함수를 만드는데 여기서 이벤트를 처리하기 위하여 현재 접속한 user의 uuid를 v4 방식(대충 버전 4의 방식으로 uuid를 생성)으로 생성하고
addUser
해당 유저의 uuid와 소켓에서 받아온 id 값으로 유저를 추가한다.
socket.on
여기서 socket.on은 해당하는 이벤트가 들어왔을 경우에만 처리하는 문장인데 'event'나 'disconnect'인 이벤트가 클라이언트에 발생하여 서버에 넘겨졌을 때 이를 처리하기 위하여 계속 기다리게 된다.
handleDisconnect
위에서 사용했던 register.handler의 'disconnect' 이벤트에 사용되는 함수이다.
handleConnection
유저에서 연결이 들어왔을 때 해당 유저의 연결을 서버에 표시해주며 현재 유저가 얼마나 있는지 확인한 후, 해당 유저의 id로 스테이지를 생성한다.
그리고 클라이언트에게 연결되었다는 'connection'을 보낸다.
stage.handler.js
먼저 현재 유저의 아이디로 현재 스테이지를 얻어온다.
그리고 얻어온 현재 스테이지를 정렬하여 오름차순으로 정렬한 후 가장 큰 스테이지의 ID가 유저의 현재 스테이지이므로 저장한다.
여기서 조금 의문인 점은 굳이 currentStage라는 함수를 추가로 만들 필요가 있었을까?
결국은 유저는 하나의 스테이지에만 참여할 수 있고 다른 스테이지에 연속적으로 참여하지 못하기 때문에 이렇게 해도 괜찮지 않았을까?
계속 가보면,
점수 검증 로직인데 먼저 서버의 현재 시간을 가져온다.
이후에 가져온 서버의 현재 시간과 현재 스테이지의 타임 스탬프(현재 스테이지의 시작 시간)로 1초당 1씩 올라가는 점수를 세려 해당 점수가 100점 이상, 105점 이하일 경우에 다음 스테이지로 넘어가게 된다.
그리고 some함수를 이용하여(객체의 값중 하나라도 있다면 true) stage에는 stage.id와 stage.timestamp가 있는데 이중 id와 우리가 다음으로 가고자 하는 targetStage를 payload에서 받아와 이를 업데이트 해준다.
여기서 업데이트를 할 때 현재시간 역시도 같이 넘기는 이유는 이를 통하여 점수를 추가로 얻기 위함이다.
handlerMapping.js
사용할 핸들러 함수들을 매핑하여 저장한다.
handlerEvent
먼저 data로 오는 handlerId에 따라서 값의 유무를 확인하고(handlerMapping에 존재하는지) 만약 없다면 클라이언트에 값이 존재하지 않는 다는 'fail' 이벤트를 보낸다.(여기서 handler는 함수를 저장하고 있다.)
이후에 handler에 저장된 함수를 이용하여 userId와 payload를 전달하여 값을 받고 만약 결과에 broadcast가 있다면 broadcast한다.
이후에 해당 유저에게 적절하게 response를 전달한다.
game.handler.js
gamestart
게임 시작시 assets에서 stage를 가져와서 스테이지를 0으로 세팅해주는 함수이다.
gameEnd
클라이언트에서 받은 게임 종료시 타임스탬프와 총 점수를 받아와서 해당 스테이지의 지속 시간을 계산하여 총 점수를 계산한 후
점수가 100.00001 이 될 수도 있으므로 Math.abs를 통하여 절댓값을 선언한 후 점수와 스테이지간 차이로 찾은 값보다 차이가 난다면 실패 아니면 성공
model 폴더는 유저의 정보나 스테이지의 정보 등의 CRUD 작업을 처리하는 폴더이다.
유저 배열을 만들어 현재 들어온 유저들을 서버에 추가하거나 유저의 userId를 읽어서 유저를 서버에서 제외, 유저를 조회하는 module이다.
해당 스테이지의 생성, 스테이지를 얻어오기 및 해당 스테이지에 대한 정보를 {id, timestamp} 방식으로 저장한다.