Implemented server re-registration when a server closes / updates

- When a server is closed, it will disappear from the server list
- When a player joins a server, the number of spaces left will decrease
- Servers now disappear instead of duplicating
- Added tests for ServerDescription
- Added documentation for new classes

Tags: #story[1247]
This commit is contained in:
Michael Rausch
2017-09-01 16:05:47 +12:00
parent 0c5d661995
commit b346d5a706
10 changed files with 316 additions and 136 deletions
+8 -7
View File
@@ -7,16 +7,10 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.ServerAdvertiser;
import seng302.model.PolarTable;
import seng302.visualiser.ServerListener;
import java.io.IOException;
@@ -87,6 +81,13 @@ public class App extends Application {
// ClientPacketParser.appClose();
// ClientPacketParser.appClose();
try {
ServerAdvertiser.getInstance().unregister();
} catch (IOException e1) {
logger.warn("Could not un-register game");
}
System.exit(0);
});
+13 -21
View File
@@ -1,37 +1,22 @@
package seng302.gameServer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javafx.scene.paint.Color;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatStatus;
import seng302.gameServer.messages.CustomizeRequestType;
import seng302.gameServer.messages.MarkRoundingMessage;
import seng302.gameServer.messages.MarkType;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RoundingBoatStatus;
import seng302.gameServer.messages.YachtEventCodeMessage;
import seng302.model.GeoPoint;
import seng302.model.Limit;
import seng302.model.Player;
import seng302.model.PolarTable;
import seng302.model.ServerYacht;
import seng302.gameServer.messages.*;
import seng302.model.*;
import seng302.model.mark.CompoundMark;
import seng302.model.mark.Mark;
import seng302.model.mark.MarkOrder;
import seng302.utilities.GeoUtility;
import seng302.utilities.XMLParser;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.*;
/**
* A Static class to hold information about the current state of the game (model)
* Also contains logic for updating itself on regular time intervals on its own thread
@@ -699,4 +684,11 @@ public class GameState implements Runnable {
public static void resetCustomizationFlag() {
customizationFlag = false;
}
public static Integer getSpacesLeft(){
Integer numberOfPlayers = GameState.getPlayers().size();
Integer maxPlayers = GameState.MAX_PLAYERS;
return maxPlayers - numberOfPlayers - 1;
}
}
@@ -1,17 +1,26 @@
package seng302.gameServer;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import seng302.gameServer.messages.*;
import seng302.model.GeoPoint;
import seng302.model.Player;
import seng302.model.PolarTable;
import seng302.model.ServerYacht;
import seng302.model.mark.CompoundMark;
import seng302.model.stream.xml.parser.RegattaXMLData;
import seng302.utilities.GeoUtility;
import seng302.utilities.XMLGenerator;
import seng302.utilities.XMLParser;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import java.net.ServerSocket;
import java.time.LocalDateTime;
import java.util.*;
@@ -42,6 +51,41 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
private ArrayList<ServerToClientThread> serverToClientThreads = new ArrayList<>();
private Logger logger = LoggerFactory.getLogger(MainServerThread.class);
private void startAdvertisingServer(){
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db;
Document doc;
XMLGenerator generator = new XMLGenerator();
try {
db = dbf.newDocumentBuilder();
String regatta = generator.getRegattaAsXml();
StringReader stringReader = new StringReader(regatta);
InputSource is = new InputSource(stringReader);
doc = db.parse(is);
} catch (ParserConfigurationException | IOException | SAXException e) {
logger.warn("Couldn't load race regatta");
return;
}
RegattaXMLData regattaXMLData = XMLParser.parseRegatta(doc);
Integer spacesLeft = GameState.getSpacesLeft();
// No spaces left on server
if (spacesLeft < 1){
return;
}
// Start advertising server
try{
ServerAdvertiser.getInstance().setMapName(regattaXMLData.getCourseName()).setSpacesLeft(spacesLeft);
ServerAdvertiser.getInstance().registerGame(PORT, regattaXMLData.getRegattaName());
} catch (IOException e) {
logger.warn("Could not register server");
}
}
public MainServerThread() {
new GameState("localhost");
try {
@@ -50,13 +94,7 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
serverLog("IO error in server thread handler upon trying to make new server socket", 0);
}
// Start advertising server
try{
ServerAdvertiser.getInstance().registerGame(PORT, "PP Test Server", 10, "Random Map");
} catch (IOException e) {
logger.warn("Could not register server");
}
startAdvertisingServer();
PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv"));
GameState.addMarkPassListener(this::broadcastMessage);
@@ -189,6 +227,12 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
}
});
serverToClientThread.addDisconnectListener(this::clientDisconnected);
try {
ServerAdvertiser.getInstance().setSpacesLeft(GameState.getSpacesLeft());
} catch (IOException e) {
logger.warn("Couldn't update advertisement");
}
}
/**
@@ -215,6 +259,13 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
}
}
serverToClientThreads.remove(closedConnection);
try {
ServerAdvertiser.getInstance().setSpacesLeft(GameState.getSpacesLeft());
} catch (IOException e) {
logger.warn("Couldn't update advertisement");
}
closedConnection.terminate();
}
@@ -222,7 +273,7 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
try {
ServerAdvertiser.getInstance().unregister();
} catch (IOException e) {
logger.warn("Error unregistering server");
logger.warn("Error unregistered server");
}
initialiseBoatPositions();
@@ -6,20 +6,45 @@ import java.io.IOException;
import java.net.InetAddress;
import java.util.Hashtable;
/**
* Advertises the game server on the local network
*/
public class ServerAdvertiser {
/*
Our service name & protocol
This must be in the format _Service._Proto.Name as per http://www.ietf.org/rfc/rfc2782.txt
Where Service is unique on the network, and protocol is usually _tcp.
The pseudo-domain 'local.' must end in a full-stop. This is used to indicate that
the lookup should be performed using an IP multicast query on the local IP network.
Read this before changing any of the following values
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/NetServices/Articles/domainnames.html#//apple_ref/doc/uid/TP40002460-SW1
*/
private static String SERVICE = "_partyatsea";
private static String PROTOCOL = "_tcp";
public static String SERVICE_TYPE = SERVICE + "." + PROTOCOL + ".local.";
private static Integer PROTO_VERSION = 1;
private static ServerAdvertiser instance = null;
private static JmDNS jmdnsInstance = null;
private ServiceInfo serviceInfo; // Note: Whenever this is changed, our service will be re-registered on the network.
private Hashtable<String ,String> props;
private ServerAdvertiser() throws IOException{
jmdnsInstance = JmDNS.create(InetAddress.getLocalHost());
props = new Hashtable<>();
props.put("map", "");
props.put("spacesLeft", "0");
}
/**
* Get an instance of the ServerAdvertiser, create an instance if there isn't already one
* @return A ServerAdvertiser Instance
* @throws IOException If there was an exception creating the instance
*/
public static ServerAdvertiser getInstance() throws IOException {
if (instance == null){
instance = new ServerAdvertiser();
@@ -28,13 +53,47 @@ public class ServerAdvertiser {
return instance;
}
public void registerGame(Integer portNo, String serverName, Integer spacesLeft, String mapName) {
Hashtable<String ,String> props = new Hashtable<>();
/**
* Set the map name & broadcast an update on the network
* @param mapName The new map name
* @return The current ServerAdvertiser instance
*/
public ServerAdvertiser setMapName(String mapName){
props.replace("map", mapName);
props.put("map", mapName);
props.put("spacesLeft", spacesLeft.toString());
if (serviceInfo != null){
serviceInfo.setText(props);
}
ServiceInfo serviceInfo = ServiceInfo.create(SERVICE_TYPE, serverName, portNo, 0, 0, props);
return instance;
}
/**
* Set the spaces left on the server & broadcast an update on the network
* @param spacesLeft The number of spaces left on the server
* @return The current ServerAdvertiser instance
*/
public ServerAdvertiser setSpacesLeft(Integer spacesLeft){
props.replace("spacesLeft", spacesLeft.toString());
if (serviceInfo != null){
serviceInfo.setText(props);
}
return instance;
}
/**
* Register this service on the network
*
* Note: other parameters (map name/spaces left etc) are set after the
* service has been registered
* @param portNo The servers port number
* @param serverName The servers name
*/
public void registerGame(Integer portNo, String serverName) {
serviceInfo = ServiceInfo.create(SERVICE_TYPE, serverName, portNo, 0, 0, props);
new java.util.Timer().schedule(
new java.util.TimerTask() {
@@ -46,12 +105,14 @@ public class ServerAdvertiser {
System.out.println("Failed");
}
}
}, 0
);
}, 0);
}
/**
* Unregister the service
*/
public void unregister(){
jmdnsInstance.unregisterAllServices();
jmdnsInstance = null;
if (serviceInfo != null)
jmdnsInstance.unregisterService(serviceInfo);
}
}
@@ -3,7 +3,6 @@ package seng302.gameServer;
public class ServerDescription {
private String address;
private Integer portNum;
private String serverName;
private String mapName;
private Integer spacesLeft;
@@ -36,4 +35,39 @@ public class ServerDescription {
public Integer spacesLeft() {
return spacesLeft;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!ServerDescription.class.isAssignableFrom(obj.getClass())) {
return false;
}
final ServerDescription other = (ServerDescription) obj;
if (!this.getAddress().equals(other.getAddress()) ) {
return false;
}
if (!this.portNumber().equals(other.portNumber())){
return false;
}
if (!this.getMapName().equals(other.getMapName())){
return false;
}
if (!this.getName().equals(other.getName())){
return false;
}
return true;
}
@Override
public int hashCode() {
return this.getName().hashCode() + this.getAddress().hashCode() +
this.portNumber().hashCode() + this.getMapName().hashCode();
}
}
@@ -1,59 +1,24 @@
package seng302.gameServer;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.messages.*;
import seng302.model.Player;
import seng302.model.ServerYacht;
import seng302.model.stream.packets.PacketType;
import seng302.model.stream.packets.StreamPacket;
import seng302.model.stream.xml.generator.Race;
import seng302.utilities.XMLGenerator;
import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatLocationMessage;
import seng302.gameServer.messages.ClientType;
import seng302.gameServer.messages.CustomizeRequestType;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RegistrationResponseMessage;
import seng302.gameServer.messages.RegistrationResponseStatus;
import seng302.gameServer.messages.XMLMessage;
import seng302.gameServer.messages.XMLMessageSubType;
import seng302.gameServer.messages.YachtEventCodeMessage;
import seng302.gameServer.messages.YachtEventCodeMessage;
import seng302.model.Player;
import seng302.model.ServerYacht;
import seng302.model.stream.packets.PacketType;
import seng302.model.stream.packets.StreamPacket;
import seng302.model.stream.xml.generator.Race;
import seng302.model.stream.xml.generator.Regatta;
import seng302.utilities.XMLGenerator;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatLocationMessage;
import seng302.gameServer.messages.ClientType;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RegistrationResponseMessage;
import seng302.gameServer.messages.RegistrationResponseStatus;
import seng302.gameServer.messages.XMLMessage;
import seng302.gameServer.messages.XMLMessageSubType;
import seng302.gameServer.messages.YachtEventCodeMessage;
import seng302.model.Player;
import seng302.model.ServerYacht;
import seng302.model.stream.packets.PacketType;
import seng302.model.stream.packets.StreamPacket;
import seng302.model.stream.xml.generator.Race;
import seng302.model.stream.xml.generator.Regatta;
import seng302.utilities.XMLGenerator;
/**
* A class describing a single connection to a Client for the purposes of sending and receiving on
@@ -258,12 +223,6 @@ public class ServerToClientThread implements Runnable, Observer {
race.addBoat(yacht);
}
//@TODO calculate lat/lng values
xml.setRegatta(
new Regatta(
"Party Parrot Test Server", "Bermuda Test Course",
57.6679590, 11.8503233)
);
xml.setRace(race);
XMLMessage xmlMessage;
@@ -3,13 +3,15 @@ package seng302.utilities;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import seng302.gameServer.messages.XMLMessageSubType;
import seng302.model.stream.xml.generator.Race;
import seng302.model.stream.xml.generator.Regatta;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import seng302.model.stream.xml.generator.Race;
import seng302.model.stream.xml.generator.Regatta;
import seng302.gameServer.messages.XMLMessageSubType;
import java.util.Random;
/**
* An XML generator to generate the Race, Boat, and Regatta XML dynamically
@@ -20,9 +22,14 @@ public class XMLGenerator {
private static final String BOATS_TEMPLATE_NAME = "boats.ftlh";
private static final String RACE_TEMPLATE_NAME = "race.ftlh";
private Configuration configuration;
private Regatta regatta;
private Regatta regatta = null;
private Race race;
public static Regatta DEFAULT_REGATTA = new Regatta("Party Parrot Test Server " + new Random().nextInt(100),
"Bermuda Test Course",
57.6679590,
11.8503233);
/**
* Set up a configuration instance for Apache Freemake
*/
@@ -106,7 +113,7 @@ public class XMLGenerator {
public String getRegattaAsXml(){
String result = null;
if (regatta == null) return null;
if (regatta == null) regatta = DEFAULT_REGATTA;
try {
result = parseToXmlString(REGATTA_TEMPLATE_NAME, XMLMessageSubType.REGATTA);
@@ -8,12 +8,13 @@ import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceListener;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
/**
* Listens for servers on the local network
*/
public class ServerListener{
private static ServerListener instance;
private ServerListenerDelegate delegate;
@@ -21,35 +22,51 @@ public class ServerListener{
GameServeMonitor listener;
private class GameServeMonitor implements ServiceListener {
private List<ServerDescription> servers;
private Set<ServerDescription> servers;
GameServeMonitor(){
servers = new ArrayList<>();
servers = new HashSet<>();
}
/**
* A Service has been detected but not resolved
* @param event The ServiceEvent
*/
@Override
public void serviceAdded(ServiceEvent event) {
}
/**
* A Service has been removed / unregistered
* @param event The ServiceEvent
*/
@Override
public void serviceRemoved(ServiceEvent event) {
Integer serverId = -1;
String serverName = event.getInfo().getName();
for (int i = 0; i < servers.size(); i++){
ServerDescription server = servers.get(i);
if (server.getName().equals(event.getInfo().getName())){
serverId = i;
break;
ServerDescription toRemove = null;
for (ServerDescription server : servers){
if (server.getName().equals(serverName)){
toRemove = server;
}
}
if (serverId > 0){
servers.remove(serverId);
if (toRemove != null){
servers.remove(toRemove);
}
delegate.serverRemoved(servers);
delegate.serverRemoved(new ArrayList<ServerDescription>(servers));
// Get all other servers with the same name to respond if they are up
jmdns.requestServiceInfo(ServerAdvertiser.SERVICE_TYPE, serverName);
}
/**
* A Service has been added and resolved
* @param event The ServiceEvent
*/
@Override
public void serviceResolved(ServiceEvent event) {
String address = event.getInfo().getServer();
@@ -60,9 +77,11 @@ public class ServerListener{
Integer spacesLeft = Integer.parseInt(event.getInfo().getPropertyString("spacesLeft"));
ServerDescription serverDescription = new ServerDescription(serverName, mapName, spacesLeft, address, portNum);
servers.remove(serverDescription);
servers.add(serverDescription);
delegate.serverDetected(serverDescription, Collections.unmodifiableList(servers));
delegate.serverDetected(serverDescription, new ArrayList<ServerDescription>(servers));
}
}
@@ -80,6 +99,10 @@ public class ServerListener{
return instance;
}
/**
* Set the delegate to handle events
* @param delegate .
*/
public void setDelegate(ServerListenerDelegate delegate){
this.delegate = delegate;
}
@@ -1,14 +1,6 @@
package seng302.visualiser.controllers;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.util.*;
import com.sun.security.ntlm.Server;
import cucumber.api.java.en.But;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
@@ -17,12 +9,20 @@ import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import seng302.gameServer.GameState;
import seng302.gameServer.ServerDescription;
import seng302.visualiser.GameClient;
import seng302.visualiser.ServerListener;
import seng302.visualiser.ServerListenerDelegate;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.ResourceBundle;
/**
* A Class describing the actions of the start screen controller
* Created by wmu16 on 10/07/17.
@@ -192,6 +192,10 @@ public class StartScreenController implements Initializable, ServerListenerDeleg
return ipAddress;
}
/**
* Update the server list with new information
* @param servers The current list of servers
*/
private void refreshServerList(List<ServerDescription> servers){
this.serverList.getItems().clear();
@@ -204,20 +208,20 @@ public class StartScreenController implements Initializable, ServerListenerDeleg
public void serverRemoved(List<ServerDescription> currentServers) {
this.servers = currentServers;
refreshServerList(currentServers);
Platform.runLater(() -> refreshServerList(currentServers));
}
@Override
public void serverDetected(ServerDescription serverDescription, List<ServerDescription> servers) {
this.servers = servers;
refreshServerList(servers);
Platform.runLater(() -> refreshServerList(servers));
}
private void joinLobbyClicked() {
Integer selectedServer = serverList.getSelectionModel().getSelectedIndex();
if (this.servers == null || selectedServer >= this.servers.size()){
if (this.servers == null || selectedServer >= this.servers.size() || selectedServer < 0){
Alert alert = new Alert(Alert.AlertType.ERROR, "Invalid server selected");
alert.showAndWait();
return;