Alcatraz: Unterschied zwischen den Versionen
Reini (Diskussion | Beiträge) |
Reini (Diskussion | Beiträge) |
||
| Zeile 446: | Zeile 446: | ||
[https://lyekka.callooh.com/svn/alcatraz/ Alcatraz svn] | [https://lyekka.callooh.com/svn/alcatraz/ Alcatraz svn] | ||
[http://www.spread.org/docs/javadocs/index.html Spread Javadoc] | |||
[[Kategorie:FH]] | [[Kategorie:FH]] | ||
Version vom 17. März 2011, 11:24 Uhr
Architektur des Gesamtsystems
Technologie
Zur Kommunikation der Clients untereinander, sowie der Clients mit den Servern wird Java RMI eingesetzt. Die Kommunikation zwischen den Servern erfolgt ausschließlich via Spread, über serialisierte Objekte und Text Nachrichten in SpreadMessages.
Server Struktur
Ein Server fungiert als Koordinator, der nach dem Bully Algorithmus gewählt wird.
Registrierungs-Anfragen können an jeden der Server ergehen, allerdings müssen diese an den Koordinator weitergeleitet werden. Nur der Koordinator entscheidet ob eine Registrierung erfolgreich ist, und propagiert in diesem Fall die erfolgte Registrierung an alle Server. Dies entspricht einem Primary-Backup Protokoll.
Koordinator und Election
Variante 1: Spread
Der Koordinator ist bekannt - über die getMembers Methode des MembershipInfo Objekts kann die Liste der private Groups bezogen werden, der jeweils erste in dieser Liste ist der Koordinator.
Diese Variante wird im Alcatraz Projekt eingesetzt
Variante 2: Bully Algorithmus
Sobald ein Server über ein zu konfigurierendes Zeitinterval hinweg keine Coordinator Message erhält, muss ein Ausfall des als Koordinator fungierenden Servers angenommen werden.
In diesem Fall sendet der Server, der diesen Ausfall bemerkt, einen ElectionRequest als Multicast Message an die Multicast Gruppe. (Der Versand des Requests sollte jedoch um ein zufälliges Intervall verzögert werden, und falls in diesem Interval bereits ein ElectionRequest eines anderen Servers eingeht, storniert werden)
Jeder Server hat lokal eine Referenz auf ein Coordinator Objekt. Ist diese Referenz null wird ebenfalls ein ElectionRequest ausgelöst, falls nicht innerhalb eines bestimmten Zeitraums eines ankommt.
Solange kein gültiges Coordinator Objekt als lokale Referenz vorhanden ist, dürfen Server keine Requests von Clients annehmen. Eventuell ist für diesen Fall ein temporärer Fehler über eine spezielle Exception an den Client zu melden.
Erhält ein Server einen ElectionRequest, antwortet er mit einem ElectionResponse an die MultiCast Gruppe. Die Identität des Response Objekts wird dabei mit den Werten der ElectionRequest Identität initialisiert.
Hat ein Server ein ElectionRequest Objekt und ElectionResponse Objekte erhalten (nach konfiguriertem ElectionTimout), wird ausgewertet welches der Response Objekte den höchsten uptime Wert aufweist. Der entsprechende Absender, identifiziert durch das hostname Attribut ist der neue Koordinator. Dieser sendet ein Coordinator Objekt an die Multicast Gruppe. Erhält ein Server ein Coordinator Objekt, obwohl er bereits eine gültige Referenz auf ein Coordinator Objekt besitzt, wird das zuletzt eingegangene Objekt als neuer Koordinator gesetzt. Weisen innerhalb einer Election mehrere Server den gleichen uptime Wert auf, inkrementieren die betroffenen Server diesen Wert um einen zufälligen Wert, und senden danach erneut eine ElectionResponse Objekt.
Synchronisation nach (Re-)Join
Wenn ein Server Objekt die Multicast Gruppe joint (hier wird durch Spread automatisch eine SpreadMessage ausgelöst), sendet der Koordinator diesem ein ServerState Objekt. Diese beinhaltet die Player Objekte, und eine Liste der Namen der in der Registry registrierten Objekte.
Kommunikation der Server
RMI Registry
- Jeder Server hat eine lokale Registry.
- Jeder Server initialisiert ein RegistryServer Objekt und registriert es bei seiner lokalen Registry. In weiterer Folge wird dieses RegistryServer Objekt nur auf Anweisung des als Koordinator fungierenden Servers aktualisiert.
- Jeder Server fungiert als Registry-Proxy für die Clients (siehe RMI Proxy)
RMI Proxy
Jeder Server fungiert als RMI Proxy für einen Client. Jeder Bind-Request, der an einen der Registry-Proxies ergeht, wird an den Koordinator weitergeleitet. Dieser vergibt einen eindeutigen Objektnamen und sendet das zu registrierende Objekt gemeinsam mit dem Objektnamen an alle übrigen Server, die das Objekt unter diesem Namen in ihre lokalen RMI Registry importieren.
Die RMI-Proxy Funktionalität (bind-Methode) wird vom RegistryServer Objekt bereitgestellt.
In den meisten Fällen wird diese Proxy Funktionalität implizit vom Server aufgerufen, für den Client besteht bislang keine Notwendigkeit die bind-Methode explizit aufzurufen.
Ablauf
Ein RMI Proxy erstellt ein BindRequest Objekt, und sendet dieses via Multicast Message. Jeder Server speichert dieses Objekt vorerst lokal. Der Koordinator schreibt das zu bindende Objekt in die RMI Registry, und erstellt dann ein Bound Objekt, das als Attribut neben der Objekt-ID auch ein Attribut mit der URI (RMI URL ohne Protokoll, Hostname und Port) des gebundenen Objekts hat. Das Bound Objekt wird via Multicast Message gesendet, woraufhin die Server das entsprechenden BindRequest Objekt löschen.
Alternativ dazu kann ein Koordinator jederzeit ein Bound Objekt via Multicast Message senden. Server schreiben daraufhin das im Bound Objekt referenzierte Objekt mit dem Namen im entsprechenden Attribut fest.
Momentan kommt ausschliesslich die alternative Methode vor
Kontaktieren des Registry Servers
Clients haben eine Liste mit RMI-URLs via Konfigurations-Datei hinterlegt. Sobald ein Client gestartet wird, versucht er mit Naming.lookup() der Reihe nach über eine der URLs ein RegistryServer Object zu erhalten.
Offene Design-Fragen:
- Prüfen eleganterer Wege, eventuell via Broadcast nach Servern suchen. Oder Server machen Broadcast, und Clients lauschen auf diese Broadcasts ..
- Liste der Server-URLs am Client eventuell durch Server aktualisieren, etwa wie in diversen P2P Apps.
Spezifikation des Ablaufs zwischen Client und Registrierungsserver zum Starten des Spiels
2 Phasen: Registrierungsphase und Initialisierungs-Phase.
Voraussetzung für diese Phasen ist, dass die Registrierungsserver online sind, und das RegistryServer Object in der RMI Registry registriert haben.
Registrierungsphase
Clients versuchen, nachdem sie ein RegistryServer Object erhalten haben, sich über dieses zu registrieren.
- Der Client erstellt ein lokales Registration Objekt.
- vom Client wird die registerRequest Methode des RegistryServer Objekts aufgerufen. Diese Methode erhält als Parameter das Registration Objekt.
- dieser Request wird an den Koordinator weitergeleitet
- der Koordinator registriert das Registration Objekt in der RMI Registry und sendet Objekt und vergebenen Namen an die anderen Server.
- der Koordinator prüft den Request nach folgenden Voraussetzungen:
- ist der Name des Spielers noch frei
- ist die Maximal-Anzahl der Spieler bereits erreicht
- ist die Registrierungsphase bereits abgeschlossen
- der Koordinator ruft die reject oder accept Methode des Registry Objects auf, und propagiert diese Entscheidung an die anderen Server
Die Registrierungsphase läuft ab wenn:
- eine bestimmte Zeit vergangen ist
- die maximale Spieranzahl erreicht ist
Sobald die Registrierungsphase abgelaufen ist, beginnt die Initialisierungs-Phase. Der Beginn dieser Phase wird den Clients vom Koordinator (über dem Aufruf der Methode startInitPhase am Request Objekt) mitgeteilt, und an die anderen Server propagiert.
Initialisierungs-Phase
- Clients initialisieren ein GameClient Objekt
- Clients übergeben das GameClient Objekt an die setGameClient Methode des RegistryServer Objekts. Der Server überprüft anhand der getPlayerName Methode des GameClient Objekts ob diese Aktion überhaupt erlaubt ist.
- Clients erhalten GameClient Objekte ihrer Mitspieler über eine Methode des Registration Objekts, die vom Server aufgerufen wird sobald alle aller Mitspieler registriert sind und ihren GameClient gesetzt haben.
- Clients erhalten über die vom Server aufgerufene Methode getPlayerName des GameClient Objekts ihre Player-ID (diese wird dem Alcatraz Objekt übergeben)
- Mit dem Übertragen der GameClient Objekte endet die Aufgabe des Servers, und das Spiel beginnt
Spezifikation des Ablaufs zwischen Client und Client zum Austausch der Züge
- jeder Client erzeugt ein lokales at.falb.games.alcatraz.api.Alcatraz Object
- jeder Client erzeugt ein GameClient Objekt, das eine Referenz auf das Alcatraz Object erhält. Dieses Objekt kann als RMI fähiges Pendant zum MoveListener verstanden werden. Es implementiert eine moveDone() Methode, in der die doMove(..) Methode am Alcatraz Objekt aufgerufen wird (damit auch lokal am GUI der Zug des anderen Spielers sichtbar wird). Die moveDone() wird remote von den anderen Spielern aufgerufen.
- jeder Client erzeugt ein Objekt, das at.falb.games.alcatraz.api.MoveListener impementiert. In der Methode moveDone() erfolgt ein Aufruf der moveDone() Methoden der GameClient-Objekte der anderen Spieler.
- das GameClient Objekt wird dem RegistryServer übergeben.
- der Spieler erhält vom RegistryServer die GameClient Objekte der anderen Spieler
- der Spieler erhält vom RegistryServer die Player-ID, die die Reihenfolge der Spieler bestimmt.
- Am Alacatraz Objekt werden lokal die Methoden init() und start() aufgerufen, um das Spiel zu starten
- Während ein Client auf Züge der anderen Spieler wartet, prüft er nach einer gewissen Zeit, ob diese noch erreichbar sind. Antwortet ein Remote GameClient über einen zu definierenden Zeitraum nicht, wird auf allen Remote GameClient Objekten die abort() Methode aufgerufen, und das Spiel daraufhin abgebrochen.
Spezifikation der Mechanismen um Serverausfälle tolerieren zu können
- jeder Server hat eine lokale RMI Registry. Die RMI Registries der Server sind synchronisiert.
- das RegistryServer Objekt ist redundant. Registrierungen eines Clients werden über ein mit Spread implementiertes Primary-Backup Protokoll repliziert.
- Ein Koordinator wird mittels Election nach Bully Algorithmus gewählt sobald ein Server einen Ausfalls des Koordinators feststellt. (Dabei wird unterschieden ob die Server in der Startup-Phase sind oder der vorherige Koordinator ausgefallen ist)
- Server, die nach crash wieder verfügbar werden, müssen die lokale RMI Registry mit jener eines anderen Servers synchronisieren (via Registry.getRegistry(String host), und Auruf der list() Methode am Registry Objekt), sowie eine Kopie eines RegistryServer Objekts erhalten (durch clone()), bevor sie der Multicast-Gruppe wieder beitreten können.
Remote Interface-Definition des Registrierungsservers (inkl. typisierten Methodenparametern)
RegistryServer
- String bind(java.RMI.Remote remote)
- void registerRequest(Registration reg)
- void unregister(String id)
- void setGameClient(GameClient client)
Registration
- String getPlayerName()
- void accept()
- void deny()
- void startInitPhase()
- void startGame(GameClient[] player)
- void setPlayerID(int id)
Remote Interface-Definition des Game Clients (inkl. typisierten Methodenparametern)
GameClient:
- void moveDone(Player player, Prisoner prisoner, int rowOrCol, int row, int col) throws java.rmi.RemoteException
- boolean isAlive() throws java.rmi.RemoteException
- String getPlayerName() throws java.rmi.RemoteException
- abort() throws java.rmi.RemoteException
Code Samples
RMI Proxy
public interface BindProxyInterface extends java.rmi.Remote {
String bind(java.rmi.Remote t)
throws java.rmi.RemoteException, java.net.MalformedURLException;
}
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
public class BindProxy extends UnicastRemoteObject implements BindProxyInterface {
private int counter = 0;
private final String identifier = "rmiIdent";
private String servername;
public BindProxy(String servername) throws java.rmi.RemoteException {
super();
this.servername = servername;
}
public String bind(java.rmi.Remote t)
throws java.rmi.RemoteException, java.net.MalformedURLException {
String url = "rmi://".concat(servername).concat(":") +
BindServer.port +
"/".concat(identifier + counter++);
Naming.rebind(url, t);
return url;
}
}
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
class BindServer {
public static final int port = 1099;
public static final String bindName = "BindProxy";
public static void main(String args[]) {
String servername;
if (args==null || args.length == 0 || args[0].length()==0) {
System.out.println("Usage: BindServer serverURL");
return;
}
servername = args[0];
//startup registry:
try {
java.rmi.registry.LocateRegistry.createRegistry(port);
} catch (RemoteException e) {
e.printStackTrace();
System.exit(1);
}
try {
BindProxy b = new BindProxy(servername);
Naming.rebind("rmi://".concat(servername).concat(":") +
port +
"/".concat(bindName), b);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
import java.rmi.Naming;
import java.rmi.NotBoundException;
public class BindClient {
public static void main(String args[]) {
if (args==null || args.length == 0) {
System.out.println("Usage: BindClient serverURL");
return;
}
String serverURL = "rmi://".concat(args[0]).concat(":") +
BindServer.port +
"/".concat(BindServer.bindName);
BindProxyInterface b = null;
try {
b = (BindProxyInterface)Naming.lookup(serverURL);
} catch (Exception e) {
e.printStackTrace();
}
if (b==null) return;
String url = null;
try {
TestObject o = new TestObject(692);
url = b.bind(o);
System.out.println("proxy hat url registriert unter: " + url);
} catch (Exception e) {
e.printStackTrace();
}
if (url==null) {
System.out.println("URL is null!!");
return;
}
TestInterface o = null;
try {
o = (TestInterface)Naming.lookup(url);
} catch (Exception e) {
e.printStackTrace();
}
if (o==null) {
System.out.println("testobject is null!!");
} else {
try {
System.out.println("id ist " + o.getId());
} catch (java.rmi.RemoteException e) {
e.printStackTrace();
}
}
System.exit(0);
}
}
GameClient
Proof of Concept mit 2 Spielern, lauffähig, ohne checks ob anderer Client noch lebt:
Usage:
setenv CLASSPATH .... ; rmiregistry
java -cp alcatraz-lib.jar:. Client 0 1
java -cp alcatraz-lib.jar:. Client 1 0
import at.falb.games.alcatraz.api.Player;
import at.falb.games.alcatraz.api.Prisoner;
import at.falb.games.alcatraz.api.MoveListener;
public interface GameClientInterface extends java.rmi.Remote {
void moveDone(Player player, Prisoner prisoner, int rowOrCol, int row, int col)
throws java.rmi.RemoteException;
boolean isAlive() throws java.rmi.RemoteException;
}
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import at.falb.games.alcatraz.api.Alcatraz;
import at.falb.games.alcatraz.api.Player;
import at.falb.games.alcatraz.api.Prisoner;
public class GameClient extends UnicastRemoteObject
implements GameClientInterface {
private Alcatraz alcatraz;
public GameClient(Alcatraz a) throws java.rmi.RemoteException {
super();
alcatraz = a;
}
public void moveDone(Player player, Prisoner prisoner, int rowOrCol, int row, int col)
throws java.rmi.RemoteException {
//propagate move to gui:
alcatraz.doMove(player,prisoner,rowOrCol,row,col);
}
public boolean isAlive() throws java.rmi.RemoteException {
return true;
}
}
import java.rmi.Naming;
import at.falb.games.alcatraz.api.Alcatraz;
import at.falb.games.alcatraz.api.MoveListener;
import at.falb.games.alcatraz.api.Player;
import at.falb.games.alcatraz.api.Prisoner;
public class Client implements MoveListener {
private static Integer otherID = null;
private GameClientInterface other = null;
public static void main(String[] args) {
if (args==null || args.length < 2) {
System.err.println("Usage: Client ident ident_other");
System.exit(1);
}
int myID = new Integer(args[0]);
otherID = new Integer(args[1]);
if (!((otherID==0 && myID==1) ||
(myID==0 && otherID==1))) {
System.err.println("ident and ident_other has to be 0 1 or 1 0");
System.exit(1);
}
try {
new Client(myID);
} catch (java.rmi.RemoteException e) {
e.printStackTrace();
}
}
public Client(int ident) throws java.rmi.RemoteException {
Alcatraz a = new Alcatraz();
GameClient g = new GameClient(a);
try {
Naming.rebind("rmi://localhost:1099/GameClient" + ident, g);
} catch (java.net.MalformedURLException e) {
e.printStackTrace();
System.exit(1);
}
while (other == null) {
other = getOther();
if (other == null) {
System.err.println(".. waiting for player " + otherID);
try {
Thread.sleep(1000);
} catch (java.lang.InterruptedException e) {
e.printStackTrace();
return;
}
}
}
a.init(2, ident);
a.getPlayer(ident).setName("Player " + (ident+1));
a.getPlayer(otherID).setName("Player " + (otherID+1));
a.showWindow();
a.addMoveListener(this);
a.start();
}
public void gameWon(Player player) {
System.out.println("Game won by player " + player);
}
public void moveDone(Player player, Prisoner prisoner, int rowOrCol, int row, int col) {
System.out.println("moving " + prisoner + " to " + (rowOrCol == Alcatraz.ROW ? "row" : "col") + " " + (rowOrCol == Alcatraz.ROW ? row : col));
//propagate move from gui to other game client(s):
try {
other.moveDone(player,prisoner,rowOrCol,row,col);
} catch (java.rmi.RemoteException e) {
e.printStackTrace();
}
}
private GameClientInterface getOther() {
try {
return (GameClientInterface)Naming.lookup("rmi://localhost:1099/GameClient" + otherID);
} catch (java.rmi.NotBoundException e) {
System.err.println(e.getMessage().
concat(" - did not find other player" + otherID));
} catch (java.net.MalformedURLException e) {
e.printStackTrace();
System.exit(1);
} catch (java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(1);
}
return null;
}
}
Links
Protokoll der Präsentation vom 9.3.2011
Frage Froihofer: wodurch kommt der Name zustande, unter dem der Client in der Registry gebunden wird? (Folie 3)
- AW: kommt noch
Frage Froihofer: RMI Registry außerhalb von Server und Client gezeichnet? (Folie 4)
- AW: jeder Server schreibt lokal in sein Registry Server Object
Frage Froihofer: dh bei Ausfall des Koordinators ist der Client dafür zuständig, neu zu registrieren? Wer könnte feststellen dass er gecrashed ist?
- AW: wenn der Client direkt auf den Koordinator geht, gibt es keine andere Möglichkeit. Ist einer dazwischen, könnte der einfach an den neuen Koordinator weiterleiten
Frage Froihofer: woher wissen wir (Interfaces), welche ID wir verwenden müssen?
- Aw: die eindeutige Spieler ID wird von Registration Object zurück gegeben
Frage Froihofer: warum muss man das Registration Object in der Phase binden? Was steht in den Backup Messages?
- AW... ausreichend offenbar :D
Frage Froihofer: warum 2 Methoden accept, deny und nicht gleich über das Registration Object weitergeben? Was passiert, wenn genau da das Netzwerk weg ist?
- AW: er würde darüber nachdenken, es direkt ins Registration Object hineinzugeben, dann ist ein Request weniger.


