📜 ⬆️ ⬇️

Reddwarf to create a Java server using the example of the online game Stone-Scissors-Paper: Server

In the article RedDwarf - server platform for developing online games in Java, I told about the features of this platform for creating game servers. In this article I will try to show with an example how to write a server and using RedDwarf.
As an example, it was decided to write an online implementation of the game "Stone-Scissors-Paper".
In this article we will write a server and try to start it. In the next article we will write a small client for this server and check their performance.


Preparation for work


First you need to download the Reddwarf server in the sgs-server-dist-0.10.2.zip archive from here and unpack the contents into the sgs-server-dist-0.10.2 folder.

Creating a project


Create a project in your favorite development environment.
The project will be simple, so we will not use maven.
For development, the sgs-server-api-0.10.2.jar library is required from the sgs-server-dist-0.10.2 \ lib \ directory.
')
Create a folder META-INF, it should contain the manifest file MANIFEST.MF. Without it, the platform refuses to work with the project jar file. My file contains only one line:
Manifest-Version: 1.0

Also in the folder META-INF, you must create the file app.properties. This file contains server startup settings. For our project, the file contains the following properties:
#  .        com.sun.sgs.app.name=RockPaperScissors # ,   AppListener      com.sun.sgs.app.listener=hello.reddwarf.server.Server #  ,        com.sun.sgs.app.root=data 

This is the minimum required set of options. The following properties may still be useful during development:

More information about other properties can be found in the documentation .

Game architecture


The game will require the following entities.
Server is a class that stores a list of players online and handles their connection.
Player - is a player. The player has the following attributes: name (it is a login) and the number of points. May participate in the battle.
Battle is a battle. This object is waiting for answers from the players and determining the winner. Keeps references to two players.
Weapon - a simple listing of weapons: directly stone, scissors and paper.

If you depict it as a class diagram, you get this:


All game entities (except for Weapon), while the server is running, are stored in an internal database that provides transactionality, link to each other, so they must implement the java.io.Serializable and com.sun.sgs.app.ManagedObject interfaces.

Class server. Player initialization and connection

The Server class is the starting point of the server, so it must implement the com.sun.sgs.app.AppListener interface:

void initialize(Properties props) is called when the server is first started. It fills the internal database with the initial values ​​necessary for the operation. An important feature: if the server is stopped (or killed) and then restarted, this method will not be called, since The internal database is stored between server launches and allows you to continue working from the moment you stop.

ClientSessionListener loggedIn(ClientSession session) is called after successful authentication and must return an object that represents the player. In our example, this will be Player.

All players connected to the server will be stored in a special collection. In Reddwarf, for game entities there is a special collection called ScalableHashMap. The advantages of this collection are that with changes it is blocked (meaning the lock in the internal database) is not entirely, but partially. And in the Server object we will not store the collection itself, but a link to it (ManagedReference).

Moving from words to deeds, we get the following code:

 package hello.reddwarf.server; import java.io.Serializable; import com.sun.sgs.app.*; import com.sun.sgs.app.util.ScalableHashMap; import java.util.Properties; /** *  .     , *        . */ public class Server implements AppListener, Serializable, ManagedObject { public ManagedReference<ScalableHashMap<String, Player>> onlinePlayersRef; @Override public void initialize(Properties props) { //      ScalableHashMap<String, Player> onlinePlayers = new ScalableHashMap<String, Player>(); onlinePlayersRef = AppContext.getDataManager().createReference(onlinePlayers); } @Override public ClientSessionListener loggedIn(ClientSession session) { String name = session.getName(); //  .      ,    Player player = loadOrRegister(name); //   .  -  ,    //   -     player.setSession(session); //    ,    player.connected(); //     - onlinePlayersRef.get().put(player.name, player); return player; } } 


DataManager is used to work with the database, which allows you to write to the database, read from the database and create ManagedReference references. Since the database is a key-value repository, the player’s name with the prefix “player.” Is used as the key, the entire Player object is serialized to the value. Let's write the function of loading the player from the database (if the player is not found in the database, create it).

  private Player loadOrRegister(String name) { try { return (Player) AppContext.getDataManager().getBindingForUpdate("player." + name); } catch (NameNotBoundException e) { //       - //   ,       Player player = new Player(name, this); AppContext.getDataManager().setBinding("player." + name, player); return player; } } 


Player class and protocol

It was the turn to create a class Player. This class represents the player and receives notification of incoming messages from the platform. So, it's time to talk about the protocol. Reddwarf allows you to work with incoming and outgoing messages as an array of bytes, leaving the implementation of the protocol to the discretion of the game developer. For the game "Stone-scissors-paper" we will use a simple text protocol.

(server -> client) SCORE <number> - the server tells the player the number of points
(client -> server) PLAY - the player's request to start the game
(server -> client) BATLE <name> - the battle with the specified player has started
(server -> client) ERROR - the player for the battle was not found (no one on the server or all in the battle)
(client -> server) ROCK - the player says "Stone"
(client -> server) SCISSORS - the player says “Scissors”
(client -> server) PAPER - the player says "Paper"
(server -> client) DRAW - draw
(server -> client) WON - player won
(server -> client) LOST - player lost

From the protocol, one can understand the sequence of actions and capabilities of the player, so we will not dwell on this separately.

You can encode text into bytes and back with this code:
 package hello.reddwarf.server; import java.nio.ByteBuffer; public class Messages { public static ByteBuffer encodeString(String s) { return ByteBuffer.wrap(s.getBytes()); } public static String decodeString(ByteBuffer message) { byte[] bytes = new byte[message.remaining()]; message.get(bytes); return new String(bytes); } } 


Now we are going to write the player object.
The player will keep the following fields:


 package hello.reddwarf.server; import com.sun.sgs.app.*; import com.sun.sgs.app.util.ScalableHashMap; import java.io.Serializable; import java.nio.ByteBuffer; import java.util.*; public class Player implements Serializable, ManagedObject, ClientSessionListener { private final static Random random = new Random(); public final String name; private int score; //   ,        private ManagedReference<ClientSession> sessionRef; //        - private ManagedReference<Server> serverRef; //    .      -    null private ManagedReference<Battle> battleRef; public Player(String name, Server server) { this.name = name; serverRef = AppContext.getDataManager().createReference(server); score = 0; } @Override public void receivedMessage(ByteBuffer byteBuffer) { //          String message = Messages.decodeString(byteBuffer); if (message.equals("PLAY")) { play(); } else if (message.equals("ROCK")) { answer(Weapon.ROCK); } else if (message.equals("PAPER")) { answer(Weapon.PAPER); } else if (message.equals("SCISSORS")) { answer(Weapon.SCISSORS); } } @Override public void disconnected(boolean b) { serverRef.get().disconnect(this); } private void answer(Weapon weapon) { if (battleRef != null) { battleRef.getForUpdate().answer(this, weapon); } } private void play() { logger.info("Choosing enemy for "+name); //          Player target = getRandomPlayer(); if (target != null && target.battleRef == null) { Battle battle = new Battle(this, target); this.sessionRef.get().send(Messages.encodeString("BATTLE " + target.name)); target.sessionRef.get().send(Messages.encodeString("BATTLE " + this.name)); target.battleRef = AppContext.getDataManager().createReference(battle); this.battleRef = target.battleRef; battle.start(); } else { this.sessionRef.get().send(Messages.encodeString("ERROR")); } } /** *    (  ) *     ,  null * @return    null,    */ private Player getRandomPlayer() { ScalableHashMap<String,Player> onlineMap = serverRef.get().onlinePlayersRef.get(); Set<String> namesSet = new HashSet<String>(onlineMap.keySet()); namesSet.remove(name); if (namesSet.isEmpty()) { return null; } else { ArrayList<String> namesList = new ArrayList<String>(namesSet); String randomName = namesList.get(random.nextInt(namesList.size())); return onlineMap.get(randomName); } } public void connected() { //      ,     sessionRef.get().send(Messages.encodeString("SCORE " + score)); } /** *  ,      */ public void battleResult(Battle.Result result) { switch (result) { case DRAW: score+=1; sessionRef.get().send(Messages.encodeString("DRAW")); break; case WON: score+=2; sessionRef.get().send(Messages.encodeString("WON")); break; case LOST: sessionRef.get().send(Messages.encodeString("LOST")); break; } sessionRef.get().send(Messages.encodeString("SCORE " + score)); battleRef = null; } public void setSession(ClientSession session) { sessionRef = AppContext.getDataManager().createReference(session); } } 


Classes Weapon and Battle

The list of Weapon is very simple and does not require comments.
 package hello.reddwarf.server; public enum Weapon { ROCK, PAPER, SCISSORS; boolean beats(Weapon other) { return other != null && this != other && this.ordinal() == (other.ordinal() + 1) % values().length; } } 


Go to the battle.

The battle has a unique identifier, contains links to two players, the answers given by them, as well as an active flag.

As soon as the battle is created, a separate task is launched, which will end the battle in 5 seconds.
After this time, the battle is summed up. If the answer was given only by one of the players, then he is considered the winner, if both are, the winner is determined according to the usual rules of “Rock-paper-scissors”.

The task is executed using the TaskManager service, which can be obtained using AppContext.getTaskManager (). This manager allows you to run tasks performed in a separate transaction either immediately, or after a specified period of time, or periodically. As it should be expected, all tasks are also stored in the internal database, which means they will be executed after the server is restarted.

So, the class code Battle.
 package hello.reddwarf.server; import com.sun.sgs.app.AppContext; import com.sun.sgs.app.ManagedObject; import com.sun.sgs.app.ManagedReference; import com.sun.sgs.app.Task; import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; public class Battle implements ManagedObject, Serializable { //   5  private static final long BATTLE_TIME_MS = 5000; enum Result { DRAW, WON, LOST } private boolean active; private ManagedReference<Player> starterPlayerRef; private ManagedReference<Player> invitedPlayerRef; private Weapon starterWeapon = null; private Weapon invitedWeapon = null; public Battle(Player starterPlayer, Player invitedPlayer) { starterPlayerRef = AppContext.getDataManager().createReference(starterPlayer); invitedPlayerRef = AppContext.getDataManager().createReference(invitedPlayer); active = false; } /** *  . *  ,  BATTLE_TIME_MS    . */ public void start(){ active = true; AppContext.getTaskManager().scheduleTask(new BattleTimeout(this), BATTLE_TIME_MS); } /** *    . *  ,  . * @param player -  * @param weapon -   */ public void answer(Player player, Weapon weapon){ if (active) { if (player.name.equals(starterPlayerRef.get().name)) { starterWeapon = weapon; } else { invitedWeapon = weapon; } } } /** *  . *  . */ private void finish() { active = false; Player starterPlayer = starterPlayerRef.getForUpdate(); Player invitedPlayer = invitedPlayerRef.getForUpdate(); if (starterWeapon != null && starterWeapon.beats(invitedWeapon)) { starterPlayer.battleResult(Result.WON); invitedPlayer.battleResult(Result.LOST); } else if (invitedWeapon != null && invitedWeapon.beats(starterWeapon)) { invitedPlayer.battleResult(Result.WON); starterPlayer.battleResult(Result.LOST); } else { starterPlayer.battleResult(Result.DRAW); invitedPlayer.battleResult(Result.DRAW); } AppContext.getDataManager().removeObject(this); } /** * ,      . */ private static class BattleTimeout implements Serializable, Task { private ManagedReference<Battle> battleRef; public BattleTimeout(Battle battle) { battleRef = AppContext.getDataManager().createReference(battle); } @Override public void run() throws Exception { battleRef.getForUpdate().finish(); } } } 


When reading this code, the question may arise: “Why is the internal class BattleTimeout made static and keeps the reference to battle explicitly? You can also declare it non-static and access the Battle fields directly. ”
The fact is that a non-static inner class will keep the link to the parent Battle implicitly and access Battle through it. But the features of the Reddwarf platform (transactional) prohibit accessing a ManagedObject (which is Battle) from another transaction directly: in this case, an exception will be thrown, because direct reference to an object in another transaction is incorrect. The recommendation of the creators of the platform to use only static inner classes is connected with this.

Separately, I would like to mention getting a managed object by reference.
The above code for the ManagedReference uses both the get () method and the getForUpdate () method.
In principle, only get () can be used. Using getForUpdate () allows the server to know before the completion of a transaction which objects will be changed and, if there are conflicting transactions, cancel the task a little earlier. This gives some speed gain compared to using get ().

Finally our server is almost ready.
Add a bit of logging (for simplicity, use java.util.logging) and you can build the project.
As a result of the build, we need to get a jar file, say, deploy.jar.
If you don’t want to build it all by hand, you can get a ready file deploy.jar from here .
This file must be placed in sgs-server-dist-0.10.2 \ dist.
Now, being in the sgs-server-dist-0.10.2 directory, execute the following command:
 java -jar bin/sgs-boot.jar 


As a result, the following can be seen in the console:
  02, 2012 9:45:19 PM com.sun.sgs.impl.kernel.Kernel <init> INFO: The Kernel is ready, version: 0.10.2.1  02, 2012 9:45:19 PM com.sun.sgs.impl.service.data.store.DataStoreImpl <init> INFO: Creating database directory : C:\sgs-server-dist-0.10.2.1\data\dsdb  02, 2012 9:45:19 PM com.sun.sgs.impl.service.watchdog.WatchdogServerImpl registerNode INFO: node:com.sun.sgs.impl.service.watchdog.NodeImpl[1,health:GREEN,backup:(none)]@black registered  02, 2012 9:45:19 PM hello.reddwarf.server.Server initialize INFO: Starting new Rock-Paper-Scissors Server. Initialized database.  02, 2012 9:45:19 PM com.sun.sgs.impl.kernel.Kernel startApplication INFO: RockPaperScissors: application is ready 


Hooray! The server has started! Now you can do the client:
Reddwarf on the example of the online game "Stone-Scissors-Paper": Client

Links


Javadoc on server API
Community Documentation
Project Forum

Source: https://habr.com/ru/post/134812/


All Articles