
¿Qué es un WebSocket?
Un WebSocket es una aplicación asíncrona, de mensajería bidireccional a través de una única conexión TCP permitiendo una comunicación en ambas direcciones simultáneamente. Este protocolo aprovecha una actualización de la cabecera HTTP, con los WebSockets de HTML5 podemos crear aplicaciones en tiempo real con una arquitectura muy sencilla.
Algunos de los posibles usos de WebSocket son:
- Aplicaciones de chat
- Juegos
- El comercio de acciones o de las aplicaciones financieras
- Edición de documentos de colaboración
- Aplicaciones de redes sociales
Desarrollo de un Juego de BlackJack usando Java WebSockets y AngularJS
Ahora vamos a poner a prueba los Sockets desarrollando una juego de cartas muy conocido usando la tecnología Java JSR 356, AngularJS y Maven.
Lo primero es crear una clase que represente una carta:
1 2 3 4 5 6 7 8 9 |
public class Card { private String rank; private String suit; private int value; private int sort; ... } |
Los tipos de cartas y los valores los voy a colocar en un Enum:
1 2 3 |
public enum Suits { SPADES, DIAMONDS, CLUBS, HEARTS } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public enum FaceCards { TWO("2", 2), THREE("3", 3), FOUR("4", 4), FIVE("5", 5), SIX("6", 6), SEVEN("7", 7), EIGHT("8", 8), NINE("9", 9), TEN("10", 10), J("J", 10), Q("Q", 10), K("K", 10), A("A", 11); private final String rank; private final int value; private FaceCards(String rank, int value) { this.rank = rank; this.value = value; } public String getRank() { return rank; } public int getValue() { return value; } } |
Ahora creamos una la clase que contenedora de cartas o un Maso este representa las 52 cartas del Juego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class Deck { private final List<Card> cards; public Deck() { // Crear paquete de 52 cartas this.cards = new ArrayList<>(); for (Suits p : Suits.values()) { //System.out.printf("Suits %s\n", p); int i = 0; for (FaceCards f : FaceCards.values()) { Card card = new Card(); card.setSuit(p.toString()); card.setRank(f.getRank()); card.setSort(i); card.setValue(f.getValue()); //System.out.printf("Face %s - %s\n", f.getRank(), f.getValue()); i++; cards.add(card); } } // Barajear las cartas Random random = new Random(); Collections.shuffle(cards, random); } public List<Card> getCards() { return cards; } public Card dealCard() { return cards.remove(0); } } |
En el constructor se inicia el maso con 52 cartas y se barajan usando colecciones de Java.
Al jugador lo vamos a representar con la clase Player:
1 2 3 4 |
public class Player { private String playerName; private double balance; |
Una mano o una partida la vamos a representar con la Clase Hand
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class Hand { private final List<Card> cards; private int score; public Hand() { this.cards = new ArrayList<>(); } public List<Card> getCards() { return cards; } public void addCard(Card card) { this.cards.add(card); if ("A".equalsIgnoreCase(card.getRank())) { if (score <= 10) { score += 11; } else { score += 1; } } else { // No es "A" tomar el valor de la carta score += card.getValue(); } // Recalcular para cambiar el valor del "A" if(score > 21) { score = 0; for (Card cardTmp : cards) { if ("A".equalsIgnoreCase(cardTmp.getRank())) { score += 1; } else { // No es "A" tomar el valor de la carta score += cardTmp.getValue(); } } } } public int getScore() { return score; } public boolean isBlackjack() { return score == 21; } public boolean isGameOver() { return score > 21; } } |
En esta clase tenemos los siguientes métodos:
- addCard, con este método el jugador recibe una carta.
- isBlackjack, este método verificar si el jugador tiene 21 puntos
- isGameOver, este método valida que aun no se ha pasado de los 21 puntos
Ahora vamos a desarrollar una clase con la lógica del Juego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
public class Blackjack21 implements Serializable { private final Logger LOG = LoggerFactory.getLogger(Blackjack21.class); private String gameStatus; private Deck deck; private Hand playerHand; private Hand dealerHand; private final Player player; private double bet; public Blackjack21(Player player, double bet) throws Exception { this.bet = bet; this.player = player; if (bet > player.getBalance()) { throw new Exception("El monto apostado " + bet + ", es superior a su saldo " + player.getBalance()); } // Comprometer el saldo del cliente player.decreaseBalance(bet); // Crear una nueva mano deck = new Deck(); // Jugador playerHand = new Hand(); // Crupier dealerHand = new Hand(); // Repartir primeras 4 cartas playerHand.addCard(deck.dealCard()); dealerHand.addCard(deck.dealCard()); playerHand.addCard(deck.dealCard()); dealerHand.addCard(deck.dealCard()); gameStatus = "OPEN"; // Validar si el juego no ha terminado en la primera mano if (dealerHand.isBlackjack() && playerHand.isBlackjack()) { LOG.info("Hay un empate entre el crupier y el jugador se retorna su apuesta inicial"); player.increaseBalance(bet); gameStatus = "TIE"; } if (playerHand.isBlackjack()) { LOG.info("El jugador tiene Blackjack! gana 3 a 2"); player.increaseBalance(bet * 1.5); gameStatus = "BLACKJACK"; } //Validar si el Crupier tiene Blackjack if (dealerHand.isBlackjack()) { LOG.info("El Crupier tiene Blackjack! el jugador pierde"); gameStatus = "LOSE"; } } public void hitDealer() { if(dealerHand.getScore() <=21 && dealerHand.getScore() >= playerHand.getScore()) { // El crupier gana LOG.info("El crupier gana! Crupier: " +dealerHand.getScore() + " Player: "+playerHand.getScore()); gameStatus = "LOSE"; } else { Card card = deck.dealCard(); LOG.info("La carta del crupier es: " + card); dealerHand.addCard(card); if(dealerHand.isGameOver()) { // El jugador gana LOG.info("El jugador gana!"); gameStatus = "WIN"; player.increaseBalance(this.bet * 2); } if(dealerHand.isBlackjack()) { // El crupier gana LOG.info("El crupier gana! Crupier: " +dealerHand.getScore() + " Player: "+playerHand.getScore()); gameStatus = "LOSE"; } } } public void hit() { Card card = deck.dealCard(); LOG.info("La carta del jugador es: " + card); playerHand.addCard(card); if (playerHand.isGameOver()) { gameStatus = "LOSE"; } if (playerHand.isBlackjack()) { gameStatus = "WIN"; player.increaseBalance(this.bet * 2); } } public void stand() { if ("OPEN".equalsIgnoreCase(gameStatus)) { gameStatus = "STAND"; } } public void split() { // FIXME Tengo que implemetar el SPLIT } public void doubles(double bet) throws Exception { if (bet > player.getBalance()) { throw new Exception("No tiene saldo para doblar la apuesta " + bet + " < " + player.getBalance()); } player.decreaseBalance(bet); this.bet += bet; Card card = deck.dealCard(); playerHand.addCard(card); if (playerHand.isGameOver()) { gameStatus = "LOSE"; } if (playerHand.isBlackjack()) { gameStatus = "WIN"; player.increaseBalance(this.bet * 2); } } public String getGameStatus() { return gameStatus; } public void setGameStatus(String gameStatus) { this.gameStatus = gameStatus; } public Hand getPlayerHand() { return playerHand; } public Hand getDealerHand() { if("OPEN".equalsIgnoreCase(gameStatus)) { Hand hideHand = new Hand(); hideHand.addCard(dealerHand.getCards().get(0)); return hideHand; } else { return dealerHand; } } public double getBet() { return bet; } public void setBet(double bet) { this.bet = bet; } } |
Hasta este punto solo hemos realizado la lógica del juego, ahora vamos a comenzar a implementar el socket.
La clase SocketMessage representa el formato del mensaje con el cual vamos ha realizar la comunicación cliente – servidor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
public class SocketMessage { private final Logger LOG = LoggerFactory.getLogger(SocketMessage.class); private String listener; private String type; private String message; public String getListener() { return listener; } public void setListener(String listener) { this.listener = listener; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getType() { return type; } public void setType(String type) { this.type = type; } public static class MessageEncoder implements Encoder.Text<SocketMessage> { @Override public void init(EndpointConfig config) { } @Override public String encode(SocketMessage message) throws EncodeException { return Json.createObjectBuilder() .add("listener", message.getListener()) .add("type", message.getType()) .add("message", message.getMessage()).build().toString(); } @Override public void destroy() { } } public static class MessageDecoder implements Decoder.Text<SocketMessage> { private JsonReaderFactory factory = Json.createReaderFactory(Collections.<String, Object>emptyMap()); @Override public void init(EndpointConfig config) { } @Override public SocketMessage decode(String str) throws DecodeException { SocketMessage message = new SocketMessage(); JsonReader reader = factory.createReader(new StringReader(str)); JsonObject json = reader.readObject(); message.setListener(json.getString("listener")); message.setMessage(json.getString("message")); message.setType(json.getString("type")); return message; } @Override public boolean willDecode(String str) { return true; } @Override public void destroy() { } } } |
La clase WebSocketsDispatcher será la encarga de despachar los mensajes a todos los clientes conectados al socket.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.util.Map; import javax.websocket.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class WebSocketsDispatcher { private final Logger LOG = LoggerFactory.getLogger(WebSocketsDispatcher.class); public void dispatch(String listener, SocketMessage message) { try { if (listener == null || "ALL".equalsIgnoreCase(listener)) { for (Map.Entry<String, Session> entry : ServerEndpoint.getListenerMap().entrySet()) { Session session = entry.getValue(); session.getBasicRemote().sendObject(message); LOG.info("To: " + message.getListener() + ";; Who: " + message.getMessage() + " "); } } else { Session session = ServerEndpoint.getListenerMap().get(message.getListener()); session.getBasicRemote().sendObject(message); LOG.info("To: " + message.getListener() + ";; Who: " + message.getMessage() + " "); } } catch (Exception e) { LOG.error("Error dispatch {}", e); e.printStackTrace(); } } } |
La clase ServerEndpoint crea un punto de acceso usando la especificación de Java JSR 356
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
@javax.websocket.server.ServerEndpoint(value = "/server", encoders = {SocketMessage.MessageEncoder.class}, decoders = {SocketMessage.MessageDecoder.class}) public class ServerEndpoint { private final Logger LOG = LoggerFactory.getLogger(ServerEndpoint.class); private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>()); private static ConcurrentHashMap<String, Session> sessionsMap = new ConcurrentHashMap<String, Session>(); private static WebSocketsDispatcher webSocketsDispatcher = new WebSocketsDispatcher(); @OnOpen public void onOpen(Session session) { sessions.add(session); } @OnClose public void onClose(Session session) { sessions.remove(session); } @OnMessage public void onMessage(SocketMessage message, Session client) throws IOException, EncodeException { LOG.info("websockets onMessage {} / {} ", message.getListener(), message.getMessage()); if ("Handshake".equalsIgnoreCase(message.getType())) { Player player = new Player(message.getListener(), 1000); client.getUserProperties().put("player", player); sessionsMap.put(message.getListener(), client); } else if ("Bye".equalsIgnoreCase(message.getType())) { // Terminar sesion del usuario try { client.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game finished")); } catch (IOException e) { throw new RuntimeException(e); } sessionsMap.remove(message.getListener()); } else { //String oldCmd = (String) client.getUserProperties().get("cmd"); play(message, client); } for (Session session : sessions) { session.getBasicRemote().sendObject(message); } } private void play(SocketMessage message, Session session) { try { session.getUserProperties().put("cmd", message.getType()); Player player = (Player) session.getUserProperties().get("player"); Blackjack21 blackjack21; // Si es dibujar if ("DRAW".equalsIgnoreCase(message.getType())) { double bet = Double.parseDouble(message.getMessage()); blackjack21 = new Blackjack21(player, bet); session.getUserProperties().put("blackjack21", blackjack21); } else { blackjack21 = (Blackjack21) session.getUserProperties().get("blackjack21"); } if ("OPEN".equalsIgnoreCase(blackjack21.getGameStatus())) { if ("HIT".equalsIgnoreCase(message.getType())) { blackjack21.hit(); } else if ("STAND".equalsIgnoreCase(message.getType())) { blackjack21.stand(); } else if ("SPLIT".equalsIgnoreCase(message.getType())) { blackjack21.split(); } else if ("DOUBLE".equalsIgnoreCase(message.getType())) { double bet = Double.parseDouble(message.getMessage()); blackjack21.doubles(bet); } ObjectMapper mapper = new ObjectMapper(); HashMap<String, Object> map = new HashMap<>(); map.put("blackjack21", blackjack21); WebSocketsDispatcher wss = ServerEndpoint.getWebSocketsDispatcher(); String json = mapper.writeValueAsString(map); SocketMessage responseMessage = new SocketMessage(); responseMessage.setListener(message.getListener()); responseMessage.setMessage(json); responseMessage.setType("CURRENT_GAME"); wss.dispatch(message.getListener(), responseMessage); } if ("STAND".equalsIgnoreCase(blackjack21.getGameStatus())) { // Le toca jugar al crupier while ("STAND".equalsIgnoreCase(blackjack21.getGameStatus())) { blackjack21.hitDealer(); ObjectMapper mapper = new ObjectMapper(); HashMap<String, Object> map = new HashMap<>(); map.put("blackjack21", blackjack21); WebSocketsDispatcher wss = ServerEndpoint.getWebSocketsDispatcher(); String json = mapper.writeValueAsString(map); SocketMessage responseMessage = new SocketMessage(); responseMessage.setListener(message.getListener()); responseMessage.setMessage(json); responseMessage.setType("CURRENT_GAME"); wss.dispatch(message.getListener(), responseMessage); } } } catch (Exception e) { e.printStackTrace(); LOG.error("Play ERROR {}", e); } } public static WebSocketsDispatcher getWebSocketsDispatcher() { return webSocketsDispatcher; } protected static ConcurrentHashMap<String, Session> getListenerMap() { return sessionsMap; } } |
Ya tenemos listo el socket del lado del servidor, ahora vamos a desarrollar el cliente web usando AngularJS.
El archivo services.js inicia el servicio del lado del cliente usando AngularJS y WebSockets de JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
angular.module('starter') .factory('WSS', function ($rootScope, API) { var websocket; var open = 0; return { isOpen: function () { return open; }, subscribe: function () { websocket = new WebSocket(API.WSS_SOCKET); websocket.onopen = function (event) { console.log("ws opened"); open = 1; var message = { "listener": "listener:" + MASTER_ID, "message": "subscribe", "type": "Handshake" }; websocket.send(JSON.stringify(message)); }; websocket.onmessage = function (event) { var json = JSON.parse(event.data); if (json.type === "CURRENT_GAME") { $rootScope.$broadcast('rootScope:broadcast', ['blackjack21', JSON.parse(event.data)]); } }; websocket.onclose = function (event) { console.log(event.data); }; websocket.onerror = function (event) { console.log(event.data); }; return websocket; }, closeSocket: function () { if (websocket !== undefined) { var message = { "listener": "listener:" + MASTER_ID, "message": "unsubscribe", "type": "Bye" }; websocket.send(JSON.stringify(message)); } open = 0; }, sendMessage: function (msg, type) { if (websocket !== undefined) { var message = { "listener": "listener:" + MASTER_ID, "message": msg+"", "type": type }; websocket.send(JSON.stringify(message)); } } }; }); |
Podemos ver como se implementa el metodo sendMessage que enviara los mensajes al socket y la implantación de los métodos escuchadores de WebSockets, donde podemos resaltar el método onmessage que traduce los mensajes enviados desde el servidor y son enviados a toda la aplicación usando $rootScope.$broadcast de AngularJS.
El archivo constants.js creamos la variable WSS_SOCKET con el URI del Socket
1 2 3 4 5 |
angular.module('starter') .constant("API", { "WSS_SOCKET": "ws://"+ (document.location.hostname === "" ? "localhost" : document.location.hostname)+ ":" + (document.location.port === "" ? "8080" : document.location.port) + "/blackjack21/server", } ); |
El fichero mainController.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
angular.module('starter') .controller('MainCtrl', ['$rootScope', '$scope', '$state', '$location', '$http', 'API', 'WSS', '$cookies', function ($rootScope, $scope, $state, $location, $http, API, WSS, $cookies) { $scope.bet = '1'; $scope.blackjack21 = {}; $rootScope.$on('rootScope:broadcast', function (event, data) { //console.log(data); $scope.blackjack21 = {}; if (data !== undefined && data.length > 0) { if ('blackjack21' === data[0]) { $scope.blackjack21 = (JSON.parse(data[1].message)).blackjack21; $scope.$apply(); } } }); if (WSS.isOpen() === 0) { $scope.wss = WSS.subscribe(); } $scope.commad = function (type) { WSS.sendMessage($scope.bet, type); //console.log(type); }; }]); |
Vamos a crear una interfaz muy simple en HTML,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<input type="text" class="form-control text-right" ng-model="bet" required placeholder="0.00"> <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('DOUBLE')">DOUBLE</button> <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('SPLIT')">SPLIT</button> <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('HIT')">HIT</button> <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('STAND')">STAND</button> <button type="button" class="btn btn-default" ng-click="commad('DRAW')">DRAW</button> <h2>{{blackjack21.gameStatus}}</h2> <p><i>{{blackjack21.playerHand.score}}</i> </p> <div class="cardstack" style="font-size: 18px;"> <div ng-repeat="cards in blackjack21.playerHand.cards" class="card rank{{cards.rank}}{{cards.suit}}">{{cards.rank}} </div> <p></p> </div> <p><i>{{blackjack21.dealerHand.score}}</i> </p> <div class="cardstack" style="font-size: 18px;"> <div ng-repeat="cards in blackjack21.dealerHand.cards" class="card rank{{cards.rank}}{{cards.suit}}">{{cards.rank}} </div> <p></p> </div> |
Como vemos ya tenemos una aplicación en tiempo real usando Java WebSocket y AngularJS
Si lo quieres ver en funcionamiento:
https://wss.araguaneybits.com:8443/blackjack21
Para descargar el código completo:
https://github.com/jestevez/blackjack21
Ya tenemos un juego de Jackblack un poco Básico nos falta implementar la función SPLIT, aplicar saldos y puntos entre otras cosas. Si quieres colaborar o mejorar el código eres bienvenido.