Java RMI

Java RMI, Remote Method Invocation, ist Java's natives RPC.
Man kann damit Methoden auf Objekten ausführen, die in einer anderen, remote JVM ausgeführt werden.
Von der Syntax her gibt es keinen Unterschied zu ganz normalen Objekten.

RMI an sich ist eine veraltete Technologie, was man auch daran merkt, wenn man danach googelt: So beschäftigen sich viele Tutorials noch mit Stub und Skeleton, dem SecurityManager, oder aber mit einer externen RMIRegistry: Alles deprecated.
In der heutigen Zeit mit verbreiteteren, sowie sprachunabhängigen Protokollen wie JSON, hat Java RMI wohl keine Relevanz mehr, da es auch schwer hinter einer Firewall betrieben werden kann, ohne alle Ports zu öffnen. Des Weiteren geht RMI nur in eine Richtung, der Client muss aktiv den Server pollen, falls man nicht irgendwelche Hacks anwenden möchte.

Im Folgenden werde ich die Nutzung von RMI anhand einer simplen Implementation des Spiels "4-Gewinnt" als RMI-Service weiter erörtern.

Paketstruktur:

Sämtliche Logik findet auf dem Server statt, Client beschränkt sich auf das darstellen des Spielfeldes und Spielerinteraktion (Chip einwerfen).

RMI-Server:

Registry registry = LocateRegistry.createRegistry(1099); // 1099 default
registry.bind("factory", UnicastRemoteObject.exportObject(new RmiFactory(), 1100));

Es braucht nur ein einziges Objekt veröffentlicht werden, eine Factory-Factory.

RMI-Client:

Registry registry = LocateRegistry.getRegistry("localhost", 1099);
factory = (IRmiFactory) registry.lookup("factory");

Auf Clientseite kann das Objekt wie jedes andere Objekt auch verwendet werden.
Diese eine Factory dient sozusagen für den Client als Eintrittspunkt zur Nutzung der Funktionalitäten des RMI-Service.

Im Common-Bereich der Pakethierarchie werden Interfaces definiert, die von Server und Client verwendet werden.
Der Client muss nämlich die Methoden eines Remote Object kennen, nicht aber die konkrete Implementation.

Zu beachten ist hier, dass jedes Interface Remote, Serializable extended, sowie bei jeder Methode throws RemoteException deklariert wird.

Das Factory-Interface:

public interface IRmiFactory extends Remote, Serializable {
    IPlayer getPlayer(UUID id) throws RemoteException;
    IGame getGame() throws RemoteException;
}


Konkrete Implementation auf Serverseite:

public class RmiFactory implements IRmiFactory {
    private static final long serialVersionUID = -2310811000734170094L;

    @Override
    public IPlayer getPlayer(UUID id) throws RemoteException {
        return Game.getInstance().addPlayer(id);
    }

    @Override
    public IGame getGame() throws RemoteException {
        return Game.getInstance();
    }
}


Extremst wichtig ist, dass jedes Objekt, dass irgendwie zurückgegeben wird, die Klasse UnicastRemoteObject erweitert. Was bei der Factory entfällt, da es manuell mit UnicastRemoteObject.exportObject zum UnicastRemoteObject exportiert wurde, das nach dem Export nicht mehr manipuliert werden kann.
Bei der Klasse Player und Game handelt es sich wiederum um Fabriken, was der geübte Programmierer an der Benutzung der Methode getInstance erkennt.
Diese müssen, wie oben bereits beschrieben, zwingend UnicastRemoteObject extenden. Andernfalls können die krassesten Sachen passieren - z.B. ein System.out.print im auf dem Server implementierten Objekt wurde auf dem Client ausgegeben, darüber hinaus wirkten sich Methodenaufrufe auf dieses Objekt nur lokal aus.
Wird also nicht das UnicastRemoteObject extended, erhält man eine lokale Kopie, statt eines remote-Pointers auf das Objekt. RMI kann Klassen zur Laufzeit dynamisch nachladen.

Setzen eines Coins auf Clientseite:

Main.getRmiClient().getRmiFactory().getPlayer(RmiClient.ID).insertCoin(c.getX());

Es gibt wirklich keinen Unterschied zu normalen, lokalen Objekten. Autocomplete funktioniert auch wie gewöhnlich in der IDE.

Das Player-Interface:

public interface IPlayer extends Remote, Serializable {
    boolean insertCoin(int col) throws RemoteException;
    boolean isWinner() throws RemoteException;
    PlayerID getPlayerID() throws RemoteException;
}

Anzumerken habe ich hier, dass die Realisierung mit Fabriken/Singletons in der RmiFactory nicht notwendigerweise dem Client offengelegt werden muss, der Client kann z.B. die getInstance() Methode nicht auf Game ausführen, da diese nicht öffentlich im Interface deklariert wurde.

Dieses Beispielprogramm crasht, wenn sich der 3. Spieler verbindet. Würde man der Game-Klasse in der getInstance() Methode auch noch einen Parameter wie bspw. eine UUID mitgeben, so hätte man eine Spielfabrik gebaut, die mehr als zwei Spieler bedienen kann.

Möchte man nun, dass RMI keine Random-Ports, sondern eine strikt vorgebene Menge an Ports nutzt, so muss man die RMISocketFactory überschreiben:

RMI-Server:

registry = LocateRegistry.createRegistry(ConnectionConstants.PORT_RMI); // 1099 default
RMISocketFactory.setSocketFactory(RMIFixedPortSocketFactory.getInstance());
registry.bind("factory", UnicastRemoteObject.exportObject(rmiFactory, 0));


In dieser RMISocketFactory kann daraufhin der 0 Port, respektive Zufallsport überschrieben werden.

public class RMIFixedPortSocketFactory extends RMISocketFactory implements Serializable {


Am Ende hat es lokal bestens funktioniert, über Internet, mit 2 fest freigegebenen Ports (und Rest blockiert) dagegen nicht.

Zum Schluss kann ich jedem, trotz der sehr eleganten, nahtlosen Integration in die Sprache, nur von Java RMI abraten - eine veraltete, auf Java beschränkte Technologie aus den 1998er Jahren.
Google Protocol Buffers sieht da schon viel interessanter aus - unterstützt nahezu alle Sprachen, bei Bedarf natürlich auch JSON-Generation; wer schon immer die struct in Java vermisst hat, sollte sich das einmal anschauen.