게임 메시지처리를위해 웹소켓을 이용할것이며,
스프링에 웹소켓을 탑재를 시도 해봅시다.
컨셉
웹소켓을 액터와 연동하여 복잡해지는 메시지 전송을 단순화할것입니다.
여기서는 웹소켓모듈을 스프링에 탑재하여, 간단한 메시지 전송을 해보는것입니다.
디펜던시
<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()); } } }
웹소켓에서 유니크한 사용자세션을 감지하고 접속과 접속끊김을 관리하는것은 제일 처음해야하는 일입니다.
게임 로직에서도 이러한 이벤트를 전달하여, 사용자의 접속 처리를 하는것은 중요한 작업입니다.
실제코드 참고:
- https://github.com/psmon/gameweb/blob/master/src/main/java/com/vgw/demo/gameweb/config/WebSocketConfig.java
- https://github.com/psmon/gameweb/blob/master/src/main/java/com/vgw/demo/gameweb/controler/ws/WebSocketEventListener.java
서버 컨트롤러 준비
@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; .... }
서버와 클라이언트가 주고받을 메시지를 정의하는것입니다.
디테일하게 여러메시지를 정의할수도 있지만, 여기서는 단일 메시지로 대부분의 게임처리를 가능하게 심플하게 설계하였습니다.
다음해야할일은, 클라이언트의 전송을 받을수 있는 서버 컨트롤러를 준비하는것입니다.
참고소스:
- https://github.com/psmon/gameweb/tree/master/src/main/java/com/vgw/demo/gameweb/message
- https://github.com/psmon/gameweb/blob/master/src/main/java/com/vgw/demo/gameweb/controler/ws/GameController.java
프론트 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 : 서버에게 전송할 메시지
참고소스: