[ 개발 ] Spring WebSocket 서버 구조

장태규·2024년 11월 19일

[ WebSocket ]

목록 보기
8/8

구조


[1] spring 서버 사용 기술


build.gradle 구성

1. Web / Websocket : 웹소켓 통신 관련 라이브러리

2. Mariadb : mariaDB 데이터 베이스 이용 관련 라이브러리

3. jpa / lombok : 쉬운 DB 이용을 위한 라이브러리


[2] spring 서버 구조


개발자 웹

MainController 클래스 :
-DB 확인 및 서버 통제를 위한 개발자 웹
-서버 상태 및 DB 확인 용도의 간단한 엔드포인트들로 구성

1. home() [ 엔드 포인트 "/" ]

@RequestMapping("/")
@ResponseBody
public String home()
{
    return "Home-Page";
}

-기본 엔드포인트와 연결
-서버 작동 확인용 기본 페이지
-서버 작동 시 "Home-Page"가 작성된 웹 페이지 출력

2. showQuiz() [ 엔드 포인트 "/showQuiz" ]

@RequestMapping("/showQuiz")
@ResponseBody
public String showQuiz(){

    String result = null;

    result = quizRepository.findAll().toString(); //Read
    result =  playerRepository.findAll().toString(); //Read

    return result.toString();
}

-DB에 존재하는 문제 리스트 출력

3. showPlayer() [ 엔드 포인트 "/showPlayer" ]

@RequestMapping("/showPlayer")
@ResponseBody
public String showPlayer(){

    String result = null;

    result =  playerRepository.findAll().toString(); //Read

    return result.toString();
} 

-DB에 존재하는 플레이어 목록 출력

4. room() [ 엔드 포인트 "/room" ]

@RequestMapping("/room")
@ResponseBody
public String room()
{

    StringBuilder result = new StringBuilder();

    result.append("[ current connected player ]")
            .append("<br>");

    for(WebSocketSession player : myWebSocketHandler.playerSessionList)
    {
        result.append(player.getId())
                .append("<br>");
    }

    result.append("[ current battle room ]")
            .append("<br>");

    for(int i = 0; i<myWebSocketHandler.PlayRoomList.size(); i++)
    {
        result.append(i)
                .append(". room : ")
                .append("player - ")
                .append(myWebSocketHandler.PlayRoomList.get(i).players.getFirst().getId())
                .append(" / ")
                .append(myWebSocketHandler.PlayRoomList.get(i).players.get(1).getId())
                .append("<br>");
    }
    return result.toString();
}

-WebSocketHandler에 접근하여 현재 접속 중인 플레이어 세션 정보 및 현재 개설된 방에 대한 정보 확인


웹소켓

WebSocketConfiguration : spring 프레임워크에서 작동하는 웹소켓 서버를 생성 및 설정
WebSocketHandler : 웹소켓 서버 생명주기에 따라 함수 호출
Room : 접속한 플레이어 세션 관리 및 퀴즈 대전 관련 메커니즘 구현


WebSocketConfiguration 클래스 구성

주요 기능 - 웹소켓 서버 생성


1. 웹소켓 핸들러를 저장할 변수 생성

private final MyWebSocketHandler myWebSocketHandler;

-생성 및 등록 후 건드릴 수 없도록 final 변수로 객체 변수 작성

2. 웹소켓 핸들러를 생성하여 할당

public WebSocketConfig(MyWebSocketHandler myWebSocketHandler) 
{
    this.myWebSocketHandler = myWebSocketHandler;
}

-생성자에서 웹소켓 핸들러 객체 변수에 할당

3. 생성한 웹소켓 핸들러 등록

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
{
    registry.addHandler(myWebSocketHandler, "/chat")
            .setAllowedOrigins("*");
}

-서버의 "/chat" 엔드포인트로 웹소켓 핸들러를 등록
-CORS 허용 설정, 모든 접속을 허용 (모든 프로토콜/호스트/포트 허용)


WebSocketHandler 클래스 구성

주요 기능 - 웹소켓 라이프 사이클 함수 관리


0. 주요 멤버 변수

// 매칭
List<WebSocketSession> playerSessionList = new ArrayList<>();
List<Room> EmptyRoomList = new ArrayList<>(), PlayRoomList = new ArrayList<>();

-playerSessionList : 현재 서버에 접속한 플레이어 세션 목록
-EmptyRoomList : 플레이어 한 명만 접속한, 대기중인 방 목록
-PlayRoomList : 두 명의 플레이어가 참여한 게임이 진행중인 방 목록록


1. afterConnectionEstablished(WebSocketSession session) : 플레이어가 웹소켓 서버에 접속하면 호출, 방 생성 및 매칭 기능

playerSessionList.add(session);

-접속한 플레이어 목록에 추가

if(EmptyRoomList.isEmpty()) EmptyRoomList.add(new Room());

-대기중인 방이 없다면, 대기방 하나 추가

EmptyRoomList.getFirst().players.add(session);

-대기중인 방의 가장 첫 번째 방에 플레이어 추가

if(EmptyRoomList.getFirst().Check())
{
    // 예시로 QuizRepository를 통해 count 개의 Quiz 데이터를 가져온다고 가정
    List<Quiz> quizList = quizRepository.findRandomQuizzes(5); // 예시 메서드

    EmptyRoomList.getFirst().Set(quizList);
    PlayRoomList.add(EmptyRoomList.getFirst());
    EmptyRoomList.removeFirst();
}

-Check() : 해당 방의 인원 수가 2명인지 확인
-DB에서 quiz 데이터 5개를 가져와서 Room 객체의 Set() 함수를 통해 해당 방의 퀴즈 세팅
-해당 방을 게임 진행중인 방으로 추가 및 대기방 목록에서 제거


2. afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) : 플레이어의 접속이 끊겼을 때 호출, 플레이어 접속 종료 및 방 삭제 기능

playerSessionList.remove(session);

var playerRoom = findPlayerRoom(session);
if(playerRoom != null)
{
    for(WebSocketSession player : playerRoom.players) player.close();

    PlayRoomList.remove(playerRoom);
}
else
{
    for (Room room : EmptyRoomList)
    {
        if (room.players.contains(session)) playerRoom = room;
    }

    EmptyRoomList.remove(playerRoom);
}

-접속 중인 플레이어 목록에서 삭제
-플레이 중인 방에 접속중인 경우, 상대방 세션 접속을 끊고 진행중인 방 삭제
-플레이 중인 방에 없는 경우 접속 중인 대기방 삭제
( remove는 별도 null 예외처리 메커니즘 포함 )


2. handleTextMessage(WebSocketSession session, TextMessage message) : 플레이어가 메시지를 보내면 호출, 플레이어가 보낸 신호 판별 및 실행 함수로의 전달 기능

String msg = message.getPayload();

-getPayload를 통해 받은 TextMessage 형태의 데이터를 String 형태로 변환

var playerRoom = findPlayerRoom(session);
if(playerRoom == null)
{
 	// 매칭되지 않은 플레이어
}
else 
{
	// 매칭된 플레이어
}

-findPlayerRoom() : PlayRoomList에서 메세지를 보낸 플레이어의 방이 있는지 찾는 함수, 없을 경우 null을 반환하며 아직 매칭되지 않은 플레이어로 취급한다. 있는 경우 매칭된 플레이어로 취급한다.

// 매칭되지 않은 플레이어의 경우
{
  // json 형태로 매핑
  Map<String, Object> responseData = Map.of(
          "type", "sign",
          "contents", "fail"
  );
  jsonResponse = objectMapper.writeValueAsString(responseData);

  // 클라이언트로 응답
  session.sendMessage(new TextMessage(jsonResponse));
}

-메시지를 보낸 플레이어에게 실패했다는 내용의 신호를 json 형태로 전달

// 매칭된 경우
{
	// json 형식 읽기
    var data = objectMapper.readValue(msg, Map.class);
    
    // 신호인 경우
    if(Objects.equals(data.get("type").toString(), "sign"))
    {
        System.out.println("in sign");
        // 내용에 따라 액션
        actSign(session, data.get("contents").toString());
    }
}

-신호를 보낸 경우, 신호에 따라 적절한 대응을 하는 actSign() 실행


3. actSign(WebSocketSession playerSession, String contents) : 받은 신호에 따라 적절한 대응 함수 실행

// 플레이어 방 찾기
var room = findPlayerRoom(playerSession);

System.out.println("in act");
switch (contents)
{
    case "correct" : room.AnswerCorrect(playerSession); break; // 정답
    case "ready" : room.AnswerReady(playerSession); break; // 준비
    case "wrong" : room.AnswerWrong(playerSession); break; // 오답
    case "late" : room.AnswerLate(playerSession); break; // 늦음
}

-플레이어가 접속 중인 Room 객체을 찾아, 해당 Room의 신호별 함수 실행


Room 클래스 구성

주요 기능 - 접속한 플레이어 세션 관리 및 퀴즈 대전 관련 메커니즘 구현


0. 주요 멤버 변수

List<WebSocketSession> players = new ArrayList<>();

int quizCount = 0;
boolean[] answers = new boolean[]{false, false};

-players : 현재 방에 접속 중인 플레이어 세션
-quizCount : 현재 진행한 문제의 개수
-answers : 두 플레이어의 정답 입력 여부


1. AnswerReady(WebSocketSession playerSession) : 준비 신호

// 응답 여부 기록
int index = players.indexOf(playerSession);
answers[index] = true;

// 둘 다 결과 입력했는지 확인
CheckForNextQuestion();

-신호를 보낸 플레이어 입력 여부 변경
-두 플레이어 모두 결과를 보내주었는 지 확인하는 CheckForNextQuestion() 함수 실행


2. AnswerCorrect(WebSocketSession playerSession) : 정답 신호

// 응답 여부 기록
int index = players.indexOf(playerSession);
answers[index] = true;

// 데이터 보냄
for(int i = 0; i<players.size(); i++)
{
    if(i == index)
    {
        answers[i] = true;
        SendSign(players.get(i), "correct");// 플레이어에게 정답 신호 보내기
    }
    else if(!answers[i])
    {
        answers[i] = true;
        SendSign(players.get(i), "late");// 반대 플레이어에게 늦음 신호 보내기
    }
}

// 둘 다 결과 입력했는지 확인
CheckForNextQuestion();

-신호를 보낸 플레이어 입력 여부 변경
-정답을 입력한 플레이어에게는 정답 신호를 보내고, 그 상대편 플레이어에게는 늦음 신호를 전송
-두 플레이어 모두 결과를 보내주었는 지 확인하는 CheckForNextQuestion() 함수 실행


3. AnswerWrong(WebSocketSession playerSession) : 오답 신호

// 응답 여부 기록
int index = players.indexOf(playerSession);
answers[index] = true;

// 데이터 보냄
SendSign(playerSession, "wrong");

// 둘 다 결과 입력했는지 확인
CheckForNextQuestion();

-신호를 보낸 플레이어 입력 여부 변경
-오답을 입력한 플레이어에게 오답 신호를 전송
-두 플레이어 모두 결과를 보내주었는 지 확인하는 CheckForNextQuestion() 함수 실행


4. AnswerLate(WebSocketSession playerSession) : 늦음 신호

// 응답 여부 기록
int index = players.indexOf(playerSession);
answers[index] = true;

// 데이터 보냄
SendSign(playerSession, "late");

// 둘 다 결과 입력했는지 확인
CheckForNextQuestion();

-신호를 보낸 플레이어 입력 여부 변경
-늦은 플레이어에게 늦음 신호를 전송
-두 플레이어 모두 결과를 보내주었는 지 확인하는 CheckForNextQuestion() 함수 실행


5. CheckForNextQuestion() : 두 플레이어 모두 결과를 보내주었는 지 확인

if(answers[0] && answers[1])
{
    answers[0] = false;
    answers[1] = false;

    quizCount += 1;
    // 3초 후에 다음 질문 호출
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.schedule(() ->
    {
        try
        {
            callNextQuestion();
        } catch (IOException e)
        {
            System.err.println("Error during delay: " + e.getMessage());
        }
    }, 3, TimeUnit.SECONDS);
}

-두 플레이어가 모두 결과 신호를 보내준 경우 실행
-결과 입력 여부 초기화
-진행한 퀴즈 개수 1 증가
-3초 후 플레이어에게 다음 문제 신호를 보냄


6. CallNextQuestion() : 플레이어에게 다음 문제 신호를 전달

for(WebSocketSession playerSession : players)
{
    var contents = quizCount < 6 ? "next" : "finish";

    // 클라이언트로 응답
    playerSession.sendMessage(new TextMessage(ToJson("sign", contents)));
}

-진행한 문제가 5개가 되지 않은 경우 "next" 신호를 보냄
-모든 문제를 진행한 경우 "finish" 신호를 보냄

profile
무럭무럭 자라나는 중

0개의 댓글