Made discovery more reliable & added docs/tests

- Added unit tests
- Added documentation for discovery classes
- Improved error handling

Tags: #story[1281]
This commit is contained in:
Michael Rausch
2017-09-22 00:01:13 +12:00
parent 95ad7a4840
commit 5e3ae40d03
15 changed files with 360 additions and 155 deletions
@@ -0,0 +1,117 @@
package seng302.discoveryServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RoomCodeRequest;
import seng302.gameServer.messages.ServerRegistrationMessage;
import seng302.model.stream.packets.PacketType;
import seng302.discoveryServer.util.ServerListing;
import seng302.discoveryServer.util.ServerRepoStreamParser;
import seng302.discoveryServer.util.ServerTable;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Random;
public class DiscoveryServer {
public static final String ANSI_GREEN = "\u001B[32m";
public static final String ANSI_YELLOW = "\u001B[33m";
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_RESET = "\u001B[0m";
public static String DISCOVERY_SERVER = "party.sydney.srv.michaelrausch.nz";
private ServerTable serverTable;
public static final Integer PORT_NUMBER = 9969;
private final Logger logger = LoggerFactory.getLogger(DiscoveryServer.class);
private void displayHeader(){
String selectedColor = Arrays.asList(ANSI_BLUE, ANSI_GREEN, ANSI_YELLOW).get(new Random().nextInt(2));
System.out.println(selectedColor);
System.out.println(" .ccccc. \n" +
" .cc;'coooxkl;. \n" +
" .:c:::c:,,,,,;c;;,.'. \n" +
" .clc,',:,..:xxocc;'..c; \n" +
" .c:,';:ox:..:c,,,,,,...cd, \n" +
" .c:'.,oxxxxl::l:.,loll;..;ol. \n" +
" ;Oc..:xxxxxxxxx:.,llll,....oc \n" +
" .,;,',:loxxxxxxxxx:.,llll;.,,.'ld, \n" +
" .lo;..:xxxxxxxxxxxx:.'cllc,.:l:'cO; \n" +
" .:;...'cxxxxxxxxxxxxoc;,::,..cdl;;l' \n" +
" .cl;':,'';oxxxxxxdxxxxxx:....,cooc,cO; \n" +
" .,,,::;,lxoc:,,:lxxxxxxxxxxxo:,,;lxxl;'oNc \n" +
" .cdxo;':lxxxxxxc'';cccccoxxxxxxxxxxxxo,.;lc. " + ANSI_YELLOW + "Party-Parrots-At-Sea Discovery Server v0.1 " + selectedColor +"\n" +
" .loc'.'lxxxxxxxxocc;''''';ccoxxxxxxxxx:..oc \n" +
"olc,..',:cccccccccccc:;;;;;;;;:ccccccccc,.'c, \n" +
"Ol;......................................;l' ");
System.out.println(ANSI_RESET);
}
public DiscoveryServer() throws Exception {
displayHeader();
serverTable = new ServerTable();
ServerSocket serverSocket;
try{
serverSocket = new ServerSocket(PORT_NUMBER);
}
catch(java.net.BindException e){
logger.error("FATAL - Could not bind socket, are you sure there isn't already an instance running?");
System.exit(1);
return;
}
logger.info("Started successfully - Now accepting connections");
while (true){
Socket clientSocket = serverSocket.accept();
parseRequest(clientSocket);
clientSocket.close();
}
}
private void parseRequest(Socket clientSocket) throws Exception {
ServerRepoStreamParser parser = new ServerRepoStreamParser(clientSocket.getInputStream());
if (clientSocket.isConnected() && !clientSocket.isClosed()){
PacketType parsePacketResult = parser.parse();
switch (parsePacketResult){
case SERVER_REGISTRATION:
ServerListing listing = parser.getServerListing();
if (!serverTable.getAllServers().contains(listing)){
listing.setRoomCode(serverTable.getNextRoomCode().toString());
}
serverTable.addServer(listing);
Message serverRegMessage = new RoomCodeRequest(listing.getRoomCode());
clientSocket.getOutputStream().write(serverRegMessage.getBuffer());
break;
case ROOM_CODE_REQUEST:
String desiredRoomCode = parser.getRoomCode();
ServerListing serverListing = serverTable.getServerByRoomCode(desiredRoomCode);
Message response;
if (serverListing != null){
response = new ServerRegistrationMessage(serverListing.getServerName(), serverListing.getMapName(), serverListing.getAddress(), serverListing.getPortNumber(), 0, 0, desiredRoomCode);
}
else{
response = new ServerRegistrationMessage("", "", "", 0, 0, 0, "");
}
clientSocket.getOutputStream().write(response.getBuffer());
break;
}
}
}
}
@@ -0,0 +1,121 @@
package seng302.discoveryServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RoomCodeRequest;
import seng302.gameServer.messages.ServerRegistrationMessage;
import seng302.model.stream.packets.PacketType;
import seng302.discoveryServer.util.ServerListing;
import seng302.discoveryServer.util.ServerRepoStreamParser;
import java.net.Socket;
import java.util.Timer;
import java.util.TimerTask;
public class DiscoveryServerClient {
private final Integer UPDATE_INTERVAL_MS = 5000;
private static String roomCode = null;
private Timer serverListingUpdateTimer;
private Logger logger = LoggerFactory.getLogger(DiscoveryServerClient.class);
public DiscoveryServerClient() {
}
/**
* Register the server with the discovery server
* @param serverListing The listing to register
*/
public void register(ServerListing serverListing){
if (serverListingUpdateTimer != null){
serverListingUpdateTimer.cancel();
serverListingUpdateTimer = null;
}
serverListingUpdateTimer = new Timer();
serverListingUpdateTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
sendRegistrationUpdate(serverListing);
} catch (Exception e) {
logger.debug("Could not update server listing");
}
}
}, 0, UPDATE_INTERVAL_MS);
}
/**
* Stop updating the server registration updates
*/
public void unregister(){
serverListingUpdateTimer.cancel();
}
/**
* Gets the connection information for a server given a room code
*
* @param roomCode The room code to search for
* @return The ServerListing, or null if there was an error
* @throws Exception .
*/
public ServerListing getServerForRoomCode(String roomCode) throws Exception {
Socket socket = new Socket(DiscoveryServer.DISCOVERY_SERVER, DiscoveryServer.PORT_NUMBER);
ServerRepoStreamParser parser = new ServerRepoStreamParser(socket.getInputStream());
Message request = new RoomCodeRequest(roomCode); //roomCode);
socket.getOutputStream().write(request.getBuffer());
PacketType packetType = parser.parse();
if (packetType != PacketType.SERVER_REGISTRATION){
logger.debug("Wrong packet received in response to a room code request");
return null;
}
socket.close();
return parser.getServerListing();
}
/**
* Sends a registration update to the discovery server.
*
* @param serverListing The server listing to send
* @throws Exception IF there was an error sending the update
*/
private void sendRegistrationUpdate(ServerListing serverListing) throws Exception {
Socket socket = new Socket(DiscoveryServer.DISCOVERY_SERVER, DiscoveryServer.PORT_NUMBER);
ServerRepoStreamParser parser = new ServerRepoStreamParser(socket.getInputStream());
Message req = new ServerRegistrationMessage(serverListing);
socket.getOutputStream().write(req.getBuffer());
PacketType packetType = parser.parse();
if (packetType != PacketType.ROOM_CODE_REQUEST){
socket.close();
return;
}
String roomCode = parser.getRoomCode();
if (roomCode.length() != 0){
DiscoveryServerClient.roomCode = roomCode;
}
socket.close();
}
/**
* @return The last room code received by the client
*/
public static String getRoomCode(){
return roomCode;
}
}
@@ -0,0 +1,50 @@
package seng302.discoveryServer.util;
import java.io.InputStream;
public class ReadableByteInputStream {
private InputStream is;
public ReadableByteInputStream(InputStream is){
this.is = is;
}
/**
* Get n bytes from the input stream
* @param n number of bytes
* @return the bytes read
* @throws Exception .
*/
public byte[] getBytes(int n) throws Exception {
byte[] bytes = new byte[n];
for (int i = 0; i < n; i++) {
bytes[i] = (byte) readByte();
}
return bytes;
}
/**
* Skip n bytes
* @param n number of bytes to skip
* @throws Exception
*/
public void skipBytes(long n) throws Exception {
for (int i = 0; i < n; i++) {
readByte();
}
}
/**
* Read the next byte from the stream
* @return The byte that was read
* @throws Exception .
*/
public int readByte() throws Exception {
int currentByte = is.read();
if (currentByte == -1) {
throw new Exception();
}
return currentByte;
}
}
@@ -0,0 +1,113 @@
package seng302.discoveryServer.util;
public class ServerListing {
public final static int SERVER_TTL_DEFAULT = 10;
private String serverName = "";
private String mapName = "";
private String address = "";
private int portNumber = 0;
private int capacity = 0;
private int players = 0;
private String roomCode = "";
private int ttl = SERVER_TTL_DEFAULT;
public ServerListing(String serverName, String mapName, String address, int portNumber, int capacity){
this.serverName = serverName;
this.mapName = mapName;
this.address = address;
this.portNumber = portNumber;
this.capacity = capacity;
}
public ServerListing setNumberOfPlayers(int players){
this.players = players;
return this;
}
public ServerListing setRoomCode(String roomCode){
this.roomCode = roomCode;
return this;
}
public void refreshTtl(){
ttl = SERVER_TTL_DEFAULT;
}
public void decrementTtl(){
ttl--;
}
public boolean hasTtlExpired(){
return ttl < 0;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!ServerListing.class.isAssignableFrom(obj.getClass())) {
return false;
}
final ServerListing other = (ServerListing) obj;
if (this.getPortNumber() != other.getPortNumber()){
return false;
}
if (!this.getMapName().equals(other.getMapName())){
return false;
}
if (!this.getServerName().equals(other.getServerName())){
return false;
}
if (this.getCapacity() != other.getCapacity()){
return false;
}
if (!this.getAddress().equals(other.getAddress())){
return false;
}
return true;
}
@Override
public int hashCode() {
return this.getServerName().hashCode() +
this.getAddress().hashCode() + this.getMapName().hashCode();
}
public String getRoomCode() {
return roomCode;
}
public int getPortNumber() {
return portNumber;
}
public String getMapName() {
return mapName;
}
public String getServerName() {
return serverName;
}
public int getCapacity() {
return capacity;
}
public String getAddress() {
return address;
}
public void setTtl(Integer ttl){
this.ttl = ttl;
}
}
@@ -0,0 +1,109 @@
package seng302.discoveryServer.util;
import seng302.gameServer.messages.Message;
import seng302.model.stream.packets.PacketType;
import java.io.InputStream;
import java.util.Arrays;
public class ServerRepoStreamParser {
private ReadableByteInputStream inputStream;
private String roomCode;
private String mapName;
private ServerListing serverListing;
public ServerRepoStreamParser(InputStream is){
inputStream = new ReadableByteInputStream(is);
}
public PacketType parse() throws Exception {
int sync1 = inputStream.readByte();
int sync2 = inputStream.readByte();
PacketType packetType = null;
if (sync1 == 0x47 && sync2 == 0x83) {
int type = inputStream.readByte();
inputStream.skipBytes(10);
long payloadLength = Message.bytesToLong(inputStream.getBytes(2));
byte[] payload = inputStream.getBytes((int) payloadLength);
inputStream.skipBytes(4);
packetType = PacketType.assignPacketType(type, payload);
switch (packetType) {
case ROOM_CODE_REQUEST:
roomCode = parseRoomCodeRequest(payload);
break;
case LOBBY_REQUEST:
mapName = parseLobbyRequest(payload);
case SERVER_REGISTRATION:
serverListing = parseServerRegistration(payload);
break;
}
}
return packetType;
}
private String parseLobbyRequest(byte[] payload) {
int mapNameLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 0 ,4));
return new String(Arrays.copyOfRange(payload, 4, 4+mapNameLength));
}
private String parseRoomCodeRequest(byte[] payload) {
int roomCodeLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 0 ,6));
return new String(Arrays.copyOfRange(payload, 6, 6+roomCodeLength));
}
public static ServerListing parseServerRegistration(byte[] payload) {
int nameLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 0, 6));
int mapNameLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 6, 12));
int addressLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 12, 18));
int roomCodeLength = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 18, 24));
int portNumber = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 24, 28));
int players = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 28, 32));
int capacity = (int) Message.bytesToLong(Arrays.copyOfRange(payload, 32, 36));
int currentPos = 36;
int nextPos = currentPos + nameLength;
String serverName = new String(Arrays.copyOfRange(payload, currentPos, nextPos));
currentPos = nextPos;
nextPos = currentPos + mapNameLength;
String mapName = new String(Arrays.copyOfRange(payload, currentPos, nextPos));
currentPos = nextPos;
nextPos = currentPos + addressLength;
String address = new String(Arrays.copyOfRange(payload, currentPos, nextPos));
currentPos = nextPos;
nextPos = currentPos + roomCodeLength;
String roomCode = new String(Arrays.copyOfRange(payload, currentPos, nextPos));
ServerListing serverListing = new ServerListing(serverName, mapName, address, portNumber, capacity);
serverListing.setNumberOfPlayers(players);
serverListing.setRoomCode(roomCode);
return serverListing;
}
public String getRoomCode() {
return roomCode;
}
public String getMapName() {
return mapName;
}
public ServerListing getServerListing() {
return serverListing;
}
}
@@ -0,0 +1,97 @@
package seng302.discoveryServer.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
public class ServerTable {
private List<ServerListing> servers;
private int lastRoomCode = 4020;
private Logger logger = LoggerFactory.getLogger(ServerTable.class);
public ServerTable(){
servers = new ArrayList<>();
new Timer().schedule(new TimerTask() {
@Override
public void run() {
updateServers();
}
}, 0, 1000);
}
/**
* Update the servers TTL values, and then remove expired servers
*/
private void updateServers() {
List<ServerListing> serversToRemove = new ArrayList<>();
for (ServerListing server : servers){
server.decrementTtl();
if (server.hasTtlExpired()){
logger.debug("Removed expired server - " + server.getServerName());
serversToRemove.add(server);
}
}
servers.removeAll(serversToRemove);
}
/**
* Add a server to the table
* @param server The server to add
*/
public void addServer(ServerListing server){
if (servers.contains(server)){
updateTtlForServer(server);
return;
}
logger.debug("Added new server - " + server.getServerName());
servers.add(server);
}
/**
* Update the TTL for a given server to the default TTL value
* @param server The server to update
*/
private void updateTtlForServer(ServerListing server) {
for (ServerListing serverListing : servers){
if (server.equals(serverListing)){
serverListing.refreshTtl();
}
}
}
/**
* @return All the servers in the table
*/
public List<ServerListing> getAllServers(){
return Collections.unmodifiableList(servers);
}
/**
* Get a server from the table given its room code
* @param roomCode The room code to search for
* @return The ServerListing of the found server, or null
* the server wasn't found
*/
public ServerListing getServerByRoomCode(String roomCode){
for (ServerListing serverListing : servers){
if (serverListing.getRoomCode().equals(roomCode)){
return serverListing;
}
}
return null;
}
/**
* @return The next available room code
*/
public Integer getNextRoomCode(){
lastRoomCode += 1;
return lastRoomCode;
}
}