게임 메시지처리를위해 웹소켓을 이용할것이며,

스프링에 웹소켓을 탑재를 시도 해봅시다.

컨셉

웹소켓을 액터와 연동하여 복잡해지는 메시지 전송을 단순화할것입니다.

여기서는 웹소켓모듈을 스프링에 탑재하여, 간단한 메시지 전송을 해보는것입니다. 


디펜던시

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>


<dependency>
   <groupId>org.webjars</groupId>
   <artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
   <groupId>org.webjars</groupId>
   <artifactId>sockjs-client</artifactId>
   <version>1.0.2</version>
</dependency>

<dependency>
   <groupId>org.webjars</groupId>
   <artifactId>stomp-websocket</artifactId>
   <version>2.3.3</version>
</dependency>

Stomp 웹소켓을 Spring에 탑재하여 사용할것입니다.

참고링크 : https://spring.io/guides/gs/messaging-stomp-websocket/

세션관리및 소켓 핸들러

public class WebSocketEventListener {
    @Autowired
    Lobby lobby;

    @Autowired
    private ActorSystem system;

    private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("Received a new web socket connection");
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = headerAccessor.getUser().getName();
    }

    @EventListener
    void handleSessionConnectedEvent(SessionConnectedEvent event) {
        // Get Accessor
        StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = sha.getUser().getName();

        // OOP VS MESSAGE

        // #OOP - It's simple to develop locally, but many things need to change in order to be extended remotely.
        lobby.addSender(sessionId,messagingTemplate);

        // #ACTOR - This can be extended to remote without implementation changes.
        ActorSelection lobby = system.actorSelection("/user/lobby");
        lobby.tell(new ConnectInfo(sessionId,messagingTemplate, ConnectInfo.Cmd.CONNECT), ActorRef.noSender());

        // Suppose you create several child actors under table.
        // You can send a message to all children with the following commands.
        // This is very useful.
        // Sameple Cmd : ActorSelection lobby = system.actorSelection("/user/table/*");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        String session = headerAccessor.getUser().getName();

        if(username != null) {
            logger.info("User Disconnected : " + username);
            GameMessage gameMessage = new GameMessage();
            gameMessage.setType(GameMessage.MessageType.LEAVE);
            gameMessage.setSender(username);
            //messagingTemplate.convertAndSend("/topic/public", gameMessage);

            // #OOP
            lobby.removeSender(session);

            // #ACTOR
            ActorSelection lobby = system.actorSelection("/user/lobby");
            lobby.tell(new ConnectInfo(session,messagingTemplate, ConnectInfo.Cmd.DISCONET), ActorRef.noSender());
        }
    }

}

웹소켓에서 유니크한 사용자세션을 감지하고 접속과 접속끊김을 관리하는것은 제일 처음해야하는 일입니다.

게임 로직에서도 이러한 이벤트를 전달하여, 사용자의 접속 처리를 하는것은 중요한 작업입니다. 


실제코드 참고:


서버 컨트롤러 준비

@Controller
@SuppressWarnings("Duplicates")
public class GameController {

    private static final Logger logger = LoggerFactory.getLogger(GameController.class);

    @MessageMapping("/game.req")
    @SendTo("/topic/public")
    public GameMessage gameReq(@Payload GameMessage gameMessage,
                               SimpMessageHeaderAccessor headerAccessor) {

        String  sessionId = headerAccessor.getUser().getName();
        logger.info("GameMsg:" + gameMessage );
        String gamePacket = gameMessage.getContent();
        String splitMessage[] = gamePacket.split("!!");

        String userName = headerAccessor.getSessionAttributes().get("username").toString();
        String userSession =  headerAccessor.getUser().getName();

        Object objTableNo = headerAccessor.getSessionAttributes().get("tableNo");
        Integer tableNo = objTableNo!=null? (Integer)objTableNo : -1;
....
}

서버와 클라이언트가 주고받을 메시지를 정의하는것입니다.

디테일하게 여러메시지를 정의할수도 있지만, 여기서는 단일 메시지로 대부분의 게임처리를 가능하게 심플하게 설계하였습니다.

다음해야할일은, 클라이언트의 전송을 받을수 있는 서버 컨트롤러를 준비하는것입니다.


참고소스:


프론트 JS코드 준비

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
        sceneControler('intro');
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new SockJS('/ws');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);

        // for broad cast
        stompClient.subscribe('/topic/public', onMessageReceived );

        // for send to some
        stompClient.subscribe('/user/topic/public', onMessageReceived );

        var username=$("#name").val();
        if(username.length<1){username="Unknown"};
        // Tell your username to the server
        stompClient.send("/app/lobby.addUser",
            {},
            JSON.stringify({sender: username, type: 'JOIN'})
        )
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function joinTable(tableNo) {
    stompClient.send("/app/game.req",
        {},
        JSON.stringify({content: 'join',num1:tableNo, type: 'GAME'})
    )
}

function seatTable() {
    stompClient.send("/app/game.req",
        {},
        JSON.stringify({content: 'seat', type: 'GAME'})
    )
}


function sendChatMsg() {
    var content = $('#gamemsg').val();
    stompClient.send("/app/hello",
        {},
        JSON.stringify({content: content, type: 'CHAT'})
    )
}

function sendGameMsg() {
    var content = $('#gamemsg').val();
    stompClient.send("/app/game.req",
        {},
        JSON.stringify({content: content, type: 'GAME'})
    )
}

function sendGameAction(action) {
    var content = $('#gamemsg').val();
    stompClient.send("/app/game.req",
        {},
        JSON.stringify({content: action.content,num1:action.num1,num2:action.num2, type: 'ACTION'})
    )
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

function onMessageReceived(payload) {
    var message = JSON.parse(payload.body);

    var messageElement = document.createElement('li');

    if(message.type == 'JOIN') {
        showGreeting('Welcome ' + message.sender)
    } else if (message.type == 'LEAVE') {
        showGreeting(message.sender + 'left!')
    } else if(message.type == 'GAME'){
        messageControler(message);
        //showGreeting(message.content);
    } else{
        showGreeting(message.content);
    }
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendGameMsg(); });


});

그리고 서버 접속이 가능한  프론트 웹소켓  .js 코드를 준비하는것입니다.

 다음 4가지 주요요소를 구현하여 실시간 메시지처리를 게임에 이용합니다.

  • connect : 컨넥션을 시도하고 성공을 감지

  • disconnect : 서버에의한 접속해지 감지
  • onMessage : 서버에의한 메시지 이벤트
  • send : 서버에게 전송할 메시지


참고소스:





  • No labels