Subsections
5. Java-Umsetzung
In diesem Kapitel möchte ich erläutern, wie ich die vorgestellte Architektur
in die Implementierung eines Frameworks umgesetzt habe. Aktuelle Dokumentation
und Sourcen sind im Internet unter [16] zu finden. Im Menü findet
sich ein Link zur Java-Dokumentation und zur HTTP-Oberfläche des MMPGP2P-Servers.
5.1 Entwicklungsumgebung
5.1.1 Programmiersprache
Ich habe mich für den Einsatz von JAVA aus mehreren Gründen entschieden.
Zum einen unterstützt JAVA eine Vielzahl von Hardware-Architekturen und Betriebssystemen.
Einmal entwickelt lässt sich der Code meist problemlos auf allen von JAVA unterstützten Systemen
einsetzen. Der Nachteil dieser Technik ist, dass ein Interpreter benötigt wird, der auf
den Zielsystemen installiert sein muß. Bei der Entwicklung eines Spieles muß dies berücksichtigt
werden.
Zum anderen unterstützt das JAVA-SDK eine große Anzahl von Techniken, die für
dieses Projekt wichtig sind. Die einfache Handhabung von Threads und Sockets
hat die Entwicklung enorm erleichtert.
JAVA wurde in der Version 1.4.2 eingesetzt.
5.1.2 Buildumgebung
Die Entwicklung des Sourcecodes und der übrigen Teile dieser Arbeit wurden unter
Debian GNU/Linux 3.1-Sarge durchgeführt. Als Buildumgebung wurde ``make''
verwendet.
5.1.3 Testumgebung
Die immer wiederkehrenden Tests habe ich auf diversen mir zur Verfügung stehenden Systemen
durchgeführt.
Als MMPGP2P-Server kam mein Rechner ``anaconda'' zum Zuge, ein
AMD Athlon(tm) XP 2200+ mit DSL-Anbindung und fester IP-Adresse 213.146.116.174.
Der Server läuft ebenfalls unter Debian GNU/Linux 3.1-Sarge mit Kernel 2.4.27.
Ich habe den MMPGP2P-Server ständig auf diesem System laufen lassen und nur während
der Weiterentwicklung der Klassen neu gestartet.
Für die Simulation von Clients habe ich zum einen meinen Rechner ``media'' eingesetzt,
eine Multimedia-Station mit Intel(R) Pentium(R) 4 CPU 2.40GHz und 100MBit-Netzwerkanbindung
an den Server. Auf diesem System lief Debian GNU/Linux 3.1-Sarge allerdings
mit Kernel 2.6.11.6. Als weitere Clients kamen diverse Testumgebungen unter
VMware Workstation 3.2.0 build-2230 zum Einsatz,
darunter Windows XP und Windows 98.
Weiteres ist im Kapitel 7 über Performanzmessungen beschrieben.
Diese Dokumentation wurde in LaTeX geschrieben und mit pdflatex kompiliert.
Die Dokumentation der Java-Klassen wurde mit javadoc erzeugt.
5.2 Konfigurationdateien
Die verschiedenen Klassen und Threads erhalten diverse Parameter zur Steuerung
ihres Verhaltens. Zur Sammlung und Übergabe dieser benötigten Parameter werden
java.util.Properties verwendet. Diese haben den Vorteil, dass man sie leicht
ausgeben oder speichern kann und dass sie ebenso einfach eingelesen werden können.
Da am MMPGP2P-System im Prinzip drei Parteien beteiligt sind, nämlich Client, Server
und RegionController, werden die Parameter entsprechend benannt. Anstatt eines Parameters
port gibt es somit rc.port und server.port. Ein Vorteil dieser Konvention
ist, dass man eine einzige Properties-Konfigurationsdatei für alle drei Teile
verwenden kann. Hier ist jedoch Vorsicht angebracht, weil unter Umständen der RegionController
des Servers mit dem RegionController eines Clients auf demselben Rechner in Konflikt
geraten kann.
Nun folgt ein Beispiel einer solchen Properties-Datei. Die hier aufgelisteten Optionen
sind nur ein Teil der verfügbaren Optionen, stellen jedoch die wichtigsten dar.
Es existieren weitere Optionen, die sich zur Aufnahme in eine Konfigurationsdatei
nicht eignen. Diese werden in Abschnitt beschrieben.
# ---- Hier beginnt der Teil für Server ---- #
# Hostname und Port des Servers konfigurieren. Diese Informationen
# stehen dann u.a. in den SessionTickets für die Clients.
server.hostname = www.mmpgp2p.org
server.port = 18987
# backlog gibt die maximale Anzahl wartender Clients an
# Siehe Java Dokumentation zur Klasse java.net.ServerSocket
server.backlog = 100
# Der nicename taucht in Protokollen oder SessionTickets auf und
# kann benutzt werden, um zusätzliche Individualität zu verleihen.
server.nicename = MMPGP2P Prototype Server
# Ping an Clients alle 60 Minuten
server.ping_interval = 60000
# Server Debugging-Ausgaben aktivieren
server.debug=1
# Die Ausgaben des Servers in folgende Datei umleiten
server.log_file = server.log
# Der Server kann regelmäßig seine Status protokollieren. Mit diesem
# Parameter gibt man die Zieldatei für die Protokolle an.
server.stats.log_file = server.stats
# append steurt, ob die Protokolle angehängt werden sollen, oder ob der
# Server die Datei überschreiben soll. Standard ist überschreiben.
server.stats.append = 0
# Intervall in ms, in dem Stats in die Datei geschrieben werden.
server.stats.inverval = 5000
# HTTP-Interface aktivieren und admin-Passwort setzen
server.http.active = 1
server.http.adminpassword = secret
# Auf der HTTP-Oberfläche wird ein RegionTree grafisch dargestellt.
Mit diesem Parameter kann man den Zoom einstellen. Wert in 1/1000..
server.http.region_tree_zoom = 20
# ---------------------------------------------------- #
# ---- Hier beginnt der Teil für RegionController ---- #
# Hostname und Port, auf dem der RegionController nach eingehenden
# Verbindungen lauscht. Für Clients-Systeme empfielt sich hier ein
# Port, der nicht durch eine Firewall geblockt wird.
rc.hostname = me.home.org
rc.port = 18900
# Der nicename taucht in Protokollen auf und kann benutzt werden, um
# dem RC zusätzliche Individualität zu verleihen.
rc.nicename = Me RC Nr. 1
# RegionController Debugging-Ausgaben aktivieren
rc.debug=1
# Die Spielewelt aus folgender Datei laden
rc.game_world_file=/tmp/initial.world
# Start-Timeout des RCs in ms
rc.start_timeout = 30000
# Logfile für RegionController (Ausgabe in Datei anstatt System.out)
rc.log_file = /tmp/rc.log
# Grenzwerte für die Benutzung der Region
rc.maxclients = 15
rc.minclients = 1
# ------------------------------------------ #
# ---- Hier beginnt der Teil für Client ---- #
# Debugging auch für den Client aktivieren
client.debug = 1
# Noch mehr Debugging. Falls hier 1 eingestellt wird, dann werden
# die Befehle in der Sende- und Empfangs- Warteschlange ausgegeben.
client.log_queue_commands = 0
# Logging aller Kommandos, die vom Client ausgeführt werden
client.debug_execute_commands = 0
# Benutzername und Passwort, das der Client beim Authentifizieren benutzt
client.username = testusr
client.password = mysecret
# Die Grösse der zufällig erzeugten TestGameWorld und
# deren tilesize konfigurieren
world.totx=100000
world.toty=100000
world.tilesize=1000
Die Beispielimplementierung (siehe 6.2) unterstützt das
Einlesen von solchen Konfigurationsdateien aus dem ``HOME''-Verzeichnis (für Unix-basierte
Systeme) oder unter ``Dokumente und Einstellungen'' (für Windows-basierte
Systeme) des aktuell angemeldeten Benutzers. Beim Starten von Client oder Server wird
dann nach einer entsprechenden Datei client.rc oder server.rc im Ordner
.mmpgp2p (Unix) oder mmpgp2p (Windows) unterhalb des Home-Verzeichnisses
gesucht und geladen. Das Home-Verzeichnis wird durch die Java-Property ``user.home''
bestimmt.
5.2.2 Weitere Parameter
Nun folgen weitere Parameter, die man nicht in eine Konfigurationsdatei
aufnehmen sollte. Sie sollten bei Bedarf durch die Methode setPropery()
eingestellt werden. Zum Beispiel der Parameter client.register in einer Konfigurationsdatei
würde dazu führen, dass bei jeder Verbindung ein neuer Account registriert wird. Dies
würde zu einem Fehler führen.
# Dieser Parameter teilt dem ClientThread mit, dass er die Kombination
# username:password beim Server registrieren soll. In diesem Fall wird ein
# REGISTER-Kommando abgesendet.
# Empfängt der Server dieses REGISTER-Kommando, dann wird seine Methode
# registerNewClient() aufgerufen.
# Der Implementierung steht es natürlich frei, die Registrierung proprietär
# und nicht über die entsprechende Methode im ServerThread zu implementieren.
client.register = 1
# Falls man einen Client ohne RC starten will, dann kann man diesen Wert
# auf 0 setzen. Clients sollten allerdings immer einen RC starten.
rc.start = 1
5.3 Wichtige Klassem
Die Implementierung verwendet einige Klassen und Datenstrukturen, die hier kurz erläutert werden sollen.
Grundsätzlich gibt die Javadoc ausreichend Informationen zur Benutzung.
5.3.1 Ruleset
Das Ruleset (Spielregeln) bietet Methoden an, durch die der Zustand der Spielewelt
verändert werden kann. Dabei bietet das Ruleset einige interne Routinen,
die vom System aufgerufen werden, und einige wenige abstrakte Methoden,
die vom Endanwender implementiert werden sollen, um seine Spielregeln
umzusetzen.
Für einen reibungslosen Betrieb des Systems sollten zunächst einige wichtige
Punkte beachtet werden.
- Niemals System.currentTimeMillis() für das Setzen von Zeitstempeln benutzen, da sich
die Uhrzeiten auf verschiedenen Clients deutlich unterscheiden können, unter Umständen im Bereich
von Minuten. Hierfür wurde die Methode getTime() (siehe Abschnitt 5.5.3.2 geschaffen,
die auf allen Systemen zu einem bestimmten Zeitpunkt diesselben Werte liefert.
- Niemals Math.random() für die Erzeugung von Zufallswerten benutzen. Alle RegionController
in einem RCPool müssen bei gleichen Eingaben auch gleiche Ergebnisse erzielen, daher
müssen auch die Zufallswerte bei allen RegionControllern gleich sein. Hierfür
existiert eine Methode random() in der Klasse Ruleset.
- Für neue Objekte niemals eine eigens gewählte ID setzen. Die Klasse GameObject
bietet einen Konstruktor, dem eine Referenz auf GameWorld übergeben wird. Dieser sorgt für das
Setzen einer systemweit eindeutige ID. Dieser Konstruktor fügt das neue Objekt noch nicht
der Welt hinzu.
- Avatare über die beiden Konstruktoren erzeugen und keinesfalls die ID modifizieren. Da ein
Spieler mehrere Avatare besitzen kann, existieren für jeden Besitzer Avatar-IDs, die nur im
Zusammenhang mit der ID des Spielers systemweit eindeutig sind (z.B. wenn Spieler 1 die Avatare
mit ID 1 und 2 besitzen, kann Spieler 2 durchaus auch Avatare mit ID 1 und 2 besitzen).
Die systemweit eindeutige ID eines Avatars wird aus der ID des Besitzers (owner)
und der ID des Avatars für diesen Benutzer berechnet.
Zur Zeit müssen drei abstrakte Methoden implementiert werden, um die Funktionalität des
Systems zu gewährleisten. Die Reihenfolge ist an dieser Stelle bewußt nach
der erwarteten Komplexität der Methoden sortiert.
Alle Methoden müssen einen Array von GameObjects zurückliefern. In diesem
Array sind alle von der Methode manipulierten Objekte enthalten. Das Ausliefern von
Updates an die Clients ist abhängig davon, welche Objekte in diesen Array zurückgeliefert
wurden.
- GameObject[] targetReached(GameWorld w, GameObject o)
Diese Methode wird von der internen Bewegungsroutine aufgerufen, sobald ein Objekt
sein Ziel rereicht hat. Die Beispielimplementierung sucht an dieser Stelle ein
neues Ziel innerhalb der Grenzen der Teilwelt.
- GameObject[] applyCommand(GameWorld world, MmpgP2PCommand command)
Sobald ein (User-) Kommando von einem Client empfangen wird, kommt diese Methode
zur Ausführung. In der Regel werden durch diese Methode Befehle wie ``Gehe-Zu'' oder
``Feuere auf'' ausgeführt.
- GameObject[] doTick(GameWorld w, GameObject o)
Diese Methode stellt die eigentliche Herausforderung an den Entwickler. In jedem
Tick wird diese Funktion mehrfach aufgerufen, für jedes Objekt einmal. Da es durchaus
geschehen kann, dass durch die Ausführung der Regeln auf ein Objekt mehrere andere
Objekte manipuliert werden, wird hier ebenfalls ein GameObject[] zurückgegeben.
5.3.3 GameWorld und GameObject
Ein GameObject ist die Basisklasse für alle Gegenstände und Objekte,
mit denen ein Spieler interagieren können soll. GameObject hat viele
Eigenschaften, von denen die meisten durch das Ruleset modifiziert
werden. Objekte werden über eine ID verwaltet, die im gesamten
System einmalig sein muß.
Die Struktur GameWorld verwalten Objekte des Typs GameObject. Sie
stellt Methoden zur Verfügung, über die Objekte innerhalb der Welt
hinzugefügt, gelöscht, abgerufen oder manipuliert werden können.
GameWorld enthält einen Zähler für die Vergabe von freien IDs. Beim
Splitting einer Teilwelt wird dabei auch das Interval freier IDs
auf die neu entstehenden Welten aufgeteilt.
Objekte sollten daher stets über die von GameWorld zur Verfügung
gestellten Operationen erzeugt werden, damit ihre ID nicht doppelt
verwendet wird.
5.3.4 GameWorldInfo und RegionInfo
Diese beiden Strukturen dienen zur Speicherung der wichtigsten Information
über die Welt oder einer Region. Es werden nur statische Informationen wie Höhe und
Breite gespeichert.
5.3.5 RCPool
Der RCPool ist eine einfache Klasse, die mehrere RegionController und die
zugehörige Region verwaltet. Es ist einfach, der Klasse weitere RegionController
hinzuzufügen oder RegionController zu entfernen
(über die Methoden addRCInfo() oder removeRCInfo).
Intern werden die Daten zu den RegionControllern als Vektor von RegionControllerInfo-Klassen
verwaltet.
Eine Methode sendToEach() erlaubt das Senden eines MmpgP2PKommandos an
jeden der beinhalteten RegionController. Da die Klasse serialisierbar ist,
kann man die Übertragung der Informationen eines RCPools einfach durch das
Übertragen der Klasse RCPool über einen ObjectOutputStream erreichen.
5.3.6 RegionTree
Diese Klasse verwaltet die Aufteilung der Welt in kleinere Regionen
anhand eines binärer Suchbaumes (siehe Abbildung 4.3 in Abschnitt 4.1.2.1).
Die Knoten innerhalb des Baumes stellen jeweils
einen Split-Vorgang dar, der entweder entlang von X- oder entlang von Y-Koordinaten
stattgefunden hat. Die beiden Kinder eines Knoten halten die linke und rechte Hälfte bzw.
die obere und untere Hälfte der geteilten Region. Einer der beiden Parameter X oder Y
ist immer -1, der andere gibt die Position des Splittings an.
Ein Blatt enthält Informationen über die Region selbst und die zugehörigen
RegionController in Form einer Referenz auf einen RCPool (siehe 5.3.5).
Nach einer Suche wird immer die Information eines Blattes zurückgegeben. Knoten
speichern keine Referenzen.
Die Darstellung als binärer Suchbaum ermöglicht das effiziente Auffinden
von Regionen zu einer (X;Y)-Koordinate. Beim Splitting einer Region wird das
zugehörige Blatt zu einem Knoten mit zwei Blättern, die nun die zwei neuen
Regionen darstellen.
Der RegionTree stellt die im Abschnitt 4.3.6 vorgestellten Operationen
über die Methoden mergeRegion() und splitNode() zur Verfügung.
Detailierte Informationen sind der Javadoc zu entnehmen.
5.4 Programmablauf und Datenfluß
Im Folgenden werden einige wichtige Programmabläufe beschrieben und
zumeist grafisch durch Sequenzdiagramme skizziert.
5.4.1 Login
Ein Client loggt sich in das System ein. Dies geschieht, sobald die von
ClientThread abgeleitete Klasse instanziert und über die start()-Methode
ausgeführt wird.
Figure 5.1:
Login des Clients
|
Das Login durchläuft einige Stationen, die hier der Reihe nach aufgelistet werden und
die in Abbildung 5.1 grafisch dargestellt sind.
- Der Client öffnet eine Verbindung zum Server
- Der Client authentifiziert sich mit Benutzername und Kennwort
- Der Server überprüft Benutzername und Kennwort
- Der Server lädt die Avatare für den Client und erzeugt ein SessionTicket
- Der Server öffnet Verbindungen zu den RCs des für den Client zuständigen RC-Pools
- Der Server lädt die zum Client gehörigen Avatare und überträgt sie an den RC-Pool
- Der Server trennt die Verbindungen zum RC-Pool und speichert, welchem RC-Pool er den Client zugewiesen hat
- Der Server antwortet dem Client mit einem SessionTicket, das u.A. die zu kontaktierenden RegionController enthält
- Der Client trennt die Verbindung zum Server
- Der Client startet seine Befehls-Warteschlangen über die initCommandQueues-Methode.
Diese Warteschlange hält Verbindung zum RC-Pool und dient als Schnittstelle
zum Übertragen und Empfangen von MmpgP2PCommands.
Der Client ist nun mit seiner Region ``verbunden'' und kann mit dem Spiel beginnen.
5.4.2 Spielstart
Nachdem der Client sich erfolgreich mit dem RC-Pool seiner Region verbunden hat, kann
er jederzeit das Spiel starten. Erst nach dem Start-Befehl erhält der Client Informationen
über Objekte und andere Spieler.
Figure 5.2:
Client gibt Befehl zum Spielstart
|
In der Grafik 5.2 sind folgende Schritte skizziert:
- Der Client erzeugt ein MmpgP2PCommand vom Typ START_GAME. Dieser wird an die RCs
übertragen.
- Der RC-Pool reagiert mit der Übertragung der Spielregeln (Ruleset)
- Der RC-Pool überträgt die Spieler-Avatare
- Der RC-Pool überträgt die für den Spieler sichtbare Welt
- Der RC-Pool bestätigt dem Client, dass er nun regulär spielen kann
Nach der erfolgreichen Übertragung der Spielregeln, Avatare und der Spielewelt beginnt das eigentliche
``spielen''. Im folgenden Abschnitt wird der Ablauf dieses schleifenartiken Zykluses dargestellt.
5.4.3 Hauptschleife: Spielzüge und Spielwelt-Updates
Kurz zusammengefasst besteht die Hauptschleife während eines Spiels aus ununterbrochenem
Senden von Zügen und Empfangen von Updates auf die Spielewelt. Updates werden regelmässig
auf die lokale Kopie der Spielewelt eines Clients angewendet und eventuell auch dann empfangen, wenn der
Client keine Züge durchführt. 5.1
Wird ein Objekt in der Welt des RegionControllers durch einen Spielzug geändert,
dann wird es im nächsten Zyklus an alle Clients übertragen, die Zugriff auf das Objekt haben
bzw. das Objekt besitzen, oder wenn sie das Objekt in Sichtweite (siehe 4.1.1.7) haben.
5.4.4 Wechseln der Region
Durch die Aufteilung der Welt in Regionen wird es zwangsweise dazu kommen,
dass Avatare die Grenzen einer Region übertreten. In diesem Fall wechseln
sie nicht nur die Region innerhalb der Welt, sondern sie wechseln auch die
RegionController, die für sie zuständig sind (waren).
- Der Avatar wird über die Grenze der Region bewegt. Dies geschieht noch
auf dem RegionController durch die Anwendung einer Bewegungsregel.
- Der RegionController bemerkt, dass der Avatar seine aktuelle Region verlassen
hat und initiiert den Regionswechsel für ihn.
- Nun baut der RegionController eine Verbindung zum Server auf (oder benutzt
eine noch offene Verbindung). Er übermittelt CHANGEREGION Kommando inklusive
Avatar an den Server.
- Nun pausiert er den Client, bis er vom Server eine Reaktion erhält.
- Der Server sucht die zuständigen RegionController für die neue Region des Avatars. Er
baut eine Verbindung zu diesen auf und übermittelt ihnen den Avatar des Spielers und
das zugehörige Session-Ticket des Clients.
- Die neuen RegionController erwarten nun die Verbindung des Clients.
- Nun übermittelt er den alten RegionContollern ein CHANGEREGION Kommando, als
Parameter werden die Session-ID des Clients und der RCPool für die neue Region übermittelt.
- Die alten RegionController weisen den Client mit einem NEWREGION Kommando an,
dass er sich mit anderen RegionControllern verbinden muß.
- Die Command-Queue wird geschlossen. Der Client schließt alle offenen Verdingungen.
- Der Client startet eine neue Befehls-Warteschlangen über die initCommandQueues()-Methode.
Die Warteschlange öffnet Verbindungen zu den neuen RegionControllern.
- Nun ruft der Client abschließend die startGame()-Methode auf und nimmt
den regulären Spielbetrieb wieder auf
- Der Client spielt weiter
5.4.5 Logout
Beim regulären Verlassen des Spiels überträgt der Client einen Befehl zum Logout
(LOGOUT-Kommando).
Figure 5.3:
Logout des Clients
|
In der Grafik 5.3 sind folgende Schritte skizziert:
- Der Client erzeugt ein MmpgP2PCommand vom Typ LOGOUT. Dieser wird an die RCs übertragen.
- Die RegionController des RC-Pools deaktivieren den Client sofort. Er erhält keine
Updates mehr und seine Spielzüge werden sofort verworfen.
- Die RCs öffnen eine Verbindung zum Server
- Die RCs übertragen die Avatare des Clients zum Server und dieser speichert sie persistent
- Die RCs teilen dem Server mit, dass sich der Client ausloggt
- Der Server deaktiviert den Client und löscht die Zuordnung zu seinem RC-Pool
- Der Server bestätigt den Vorgang
- Die RCs übertragen ein MmpgP2PCommand vom Typ CLOSE an den Client
- Die Command-Queue wird geschlossen. Der Client schließt alle offenen Verdingungen.
5.4.6 Splitting einer Region
Das Splitting einer Region findet primär auf den RegionControllern der Region
statt. Die Clients sind nur insofern beteiligt, als dass sie eventuell den RC-Pool
wechseln müssen, sobald das Splitting beendet wurde und sich ihr Standort nun in
der Region eines anderen RC-Pools befindet. Der Server ist verantwortlich für die
Zuweisung von freien RCs für den neuen Pool (hier RCPoolB).
Im Abschnitt 4.3.4 wurden diese Punkte bereits angedeutet.
Figure 5.4:
Splitting einer Region
|
In der Grafik 5.4 sind folgende Schritte skizziert:
- Der RC-Pool signalisiert dem Server, dass er seine Region splitten möchte. Gleichzeitig
wird ein Vorschlag übermittelt, wo die Grenze des Splittings verlaufen sollte.
5.2
- Der Server sucht freie RegionController (siehe 4.2.3) für den neuen RC-Pool. Die Anzahl
an RCs im Pool wird durch Parameter gesteuert.
- An jeden dieser RCs schickt er einen MmpgP2PCommand vom Typ PREPARE mit entsprechenden
Parametern, um die Übermittlung der neuen Region vorzubereiten.
- Nun übermittelt der Server den eigentlichen SPLIT Befehl an die RCs. Der neu erzeugte RC-Pool B
wird ebenfalls als Parameter übertragen, damit die RCs des alten RC-Pool A wissen, wohin sie die
neue Region übertragen müssen.
5.3
- Das Spiel wird in dieser Region für die Zeit des Splittings pausiert.
- Die RCs des ``alten'' RC-Pools A führen das Splitting der Region durch.
- RC-Pool A kontaktiert RC-Pool B und übermittelt die neue Region in Form einer GameWorld Klasse.
Die GameWorld Klasse enthält auch alle Objekte innerhalb dieses Teiles der Region.
- Nun werden noch die Avatare an RC-Pool B übertragen.
- Das Splitting ist durchgeführt und kann dem Server bestätigt werden.
- Der Server aktualisiert seinen RegionTree (siehe 4.1.2.1) mit den neuen Daten.
- Die Clients, deren Avatare sich innerhalb der neu entstandenen Region befinden, bekommen
einen MmpgP2PCommand vom Typ NEWREGION und verbinden sich mit dem neuen RC-Pool B.
- Der RC-Pool B informiert den Server darüber, dass sich ein Client nun innerhalb seiner
Region befindet (UPDATE Client). Der Server bestätigt.
- Die Clients erhalten den PLAY Befehl und können nun weiterspielen.
Es sei an dieser Stelle angemerkt, dass der Server vor dem Befehl zum Splitting noch diverse
Prüfungen auf Plausibilität durchführt oder das Splitting ablehnt, falls es unsinnig erscheint. Diese
Details würden jedoch den Rahmen sprengen.
Abbildung 5.5 soll verdeutlichen, wie der Server eine eingehende
Verbindung verarbeitet. Der Server ist in der Lage, den Typus einer Verbindung zu erkennen
und ein dementsprechendes Interface bereitzustellen.
Figure 5.5:
Starten des Servers und lauschen auf Netzwerkverbindungen
|
Wird eine neue Verbindung zum Server aufgebaut, dann nimmt der ConnectionListener
des Servers diese Verbindung an und startet einen ConnectionWorkerThread. Dieser
ist ab sofort für die weitere Verarbeitung ein und ausgehender Daten verantwortlich.
Der erste Befehl während einer neuen Verbindung ist ein String, der den Typ der
gewünschten Verbindung spezifiziert. Erkennt der Server z.B. eine HTTP-Verbindung,
dann wird ein entsprechendes Interface gestartet. Bei eingehenden Client- oder
RC-Verbindungen wird ebenfalls ein entsprechendes Interface gestartet.
Die Verbindungen bleiben so lange offen, bis ein konfigurierbarer Timeout auftritt,
oder bis das System die Verbindung manuell trennt (z.B. direkt nach der Verarbeitung
eines HTTP-Requests).
5.5.1 MmpgP2PCommand
Diese Klasse represäntiert ein Kommando und stellt Methoden zur Verarbeitung und
Übertragung bereit. Ein Kommando besteht aus einer ID und einem Array von
Java-Objekten (oder Argument null). Die Argumente können beliebig verschachtelt
werden, solange die verwendeten Objekte das Interface java.io.Serializable
implementieren.
Ein Kommando kann man folgendermaßen erzeugen:
int id = 1000000;
Object [] args = { "Parameter 1", "Parameter 2", "Parameter 3" };
MmpgP2PCommand command = new MmpgP2PCommand (id, args);
Unter den Beispielen befindet sich ein Programm namens SendCommand (siehe 6.1.2),
das dieses Prinzip verdeutlichen sollte.
Das Kommando mit der ID 0 ist ein sogenanntes No-Operation (NOP)-Kommando.
Dieses wird nicht verarbeitet und beeinflußt somit das System nicht. In diesem Fall ist
die Argument-Liste null. Ein NOP-Kommando kann durch command = new MmpgP2PCommand() erzeugt
werden und dient dazu, dem Gegenüber zu vermitteln, dass trotz Fehlen von übertragenen Kommandos
die Verbindung noch besteht.
Welche Kommandos das Spiel verwendet und interpretiert ist Sache der Implementierung. Die
vom MMPGP2P-System bereitgestellten Kommandos (ID 1024) dürfen nicht von der Implementierung
verwendet oder erzeugt werden.
Benutzerdefinierte Kommandos (ID 1024) werden durch ein Ruleset verarbeitet, das
durch den Endanwender implementiert werden muß. Bei der Definition von benutzerdefinierten
Kommandos sind keine Einschränkungen vorhanden.
Ein SessionTicket erhält der Client vom Server nach erfolgreicher Authentifizierung im Netzwerk.
Der Server kümmert sich auch um die Übermittlung des SessionTickets an die RegionController,
die für den Besitzer des SessionTickets zuständig sind.
Das SessionTicket enthält unter anderem eine Sitzungs-ID (sessionID), die im gesamten Netzwerk
eindeutig ist und den zugehörigen Client eindeutig identifiziert, und einen Sitzungsschlüssel.
Die Verwendung des Sitzungsschlüssels ist optional und kann von der Implementierung für kryptografische Funktionen
genutzt werden, um beispielsweise die Gültigkeit eines SessionTickets zu beweisen.
Das Framework benutzt diesen Sitzungsschlüssel nicht.
Weiterhin enthält ein SessionTicket eine Liste von RegionControllern in Form einer RCPool-Klasse (siehe 5.3.5).
Der Client muss die RegionController dieses RCPools kontaktieren, um am Spielgeschehen teilzunehmen.
Die Gültigkeit eines SessionTickets kann zeitlich beschränkt werden, hierfür
sind ebenfalls Methoden vorhanden.
Die abstrakte Klasse MmpgP2PServiceThread dient als Basisklasse für verschiedene
Threads. Häufig benötigte Methoden sind hier implementiert.
Die implementierenden Unterklassen rufen in ihrem eigenen Konstruktor den
Konstruktor MmpgP2PServiceThread(Properties p) auf, damit alle
benötigten Einstellungen vorhanden sind, insbesondere Daten zur Verbindung
mit dem Server des Systems.
Einige wenige Methoden müssen von einer abgeleiteten Klasse implementiert
werden. Genaue Dokumentation zu der Funktionsweise der jeweiligen Methode
befinden sich in der Javadoc.
- log()
Um das Ausgeben von Informationen oder das Logging von Fehlern zu erleichtern enthält
die Klasse abstrakte log()-Methoden. Ein MmpgP2PServiceThread muß diese Methoden
implementieren und kann damit steuern, wo Fehlermeldungen landen. Die Beispiel-Implementierung
des ClientThread (ClientImplementation) z.B. gibt die log()-Ausgaben in einem grafischen
Fenster aus.
- incomingConnection()
Falls eine Netzwerkverbindung zum MmpgP2PServiceThread aufgebaut wird, dann kommt
diese Methode zum Einsatz. In der Regel wird diese Methode von einem ConnectionListenerThread einmal
aufgerufen, sobald eine Verbindung eingeht und bevor die Verbindung von einem ConnectionWorkerThread
gehalten wird.
- incomingData()
Jeder MmpgP2PServiceThread muß eingehende Daten verarbeiten können. Dazu dient diese
abstrakte Methode, die als Argument einen ConnectionWorkerThread erhält, über den die eingehenden Daten
abgerufen werden können. Diese Methode wird in der Regel von einem ConnectionWorkerThread
aufgerufen, sobald dieser Daten auf seinem Socket empfängt.
- closedConnection()
Falls ein noch offener ConnectionWorkerThread beendet wird und seine Verbindung
schließt, dann wird diese Methode aufgerufen. Dem implementierenden MmpgP2PServiceThread
bleiben dann noch Möglichkeiten, auf die geschlossene Verbindung zu reagieren.
Die Klasse bietet einige Methoden zur einfacheren Verwendung des Systems. Einige
wichtige sollen hier genannt werden. Dokumentation zu weiteren Funktionen finden
sie in der Javadoc.
- getTime()
Diese Methode ist eine der wichtigsten Funktionen des Systems. Beim Aufbau einer
Verbindung mit dem Server wird die lokale Zeit mit der globalen Zeit des MMPG-Netzwerkes
abgeglichen und Abweichungen korrigiert. So wird sichergestellt, dass alle beteiligten
Peers beim Aufruf der Methode dieselbe Zeit zurückgeliefert bekommen (mit tolerierbaren
Abweichungen im Bereich von Millisekunden). Diese Methode löst das in Abschnitt angeschnittene Problem der zeitlichen Asynchronität der beteiligten Peers.
- isStarting() und isRunning()
Mit diesen Methoden kann man den Status des MmpgP2PServiceThread erfragen.
- stopThread()
Diese Methode sorgt dafür, dass der MmpgP2PServiceThread sachgemäß beendet wird.
- waitFor(long timeout)
Wird diese Methode aufgerufen, dann hält der aktuelle Thread so lange an, bis
entweder der Timeout (in ms) erreicht wird oder bis der MmpgP2PServiceThread seinen
Status auf running gewechselt hat. Diese Methode ist nützlich, um auf den vollständigen
Start eines benötigten Threads zu warten.
- getConnectionWorker() und getServerConnectionWorker()
Diese beiden Methoden dienen dazu, eine reguläre Verbindung zu einem anderen
MmpgP2PServiceThread aufzubauen. Erst wenn die Verbindung sicher steht, kehrt die Methode zurück.
5.5.4 ConnectionListenerThread
Dieser Thread kann von einem MmpgP2PServiceThread gestartet werden und tut
dann nichts anderes, als auf eingehende Verbindungen zu lauschen. Beim Erzeugen
eines solchen ConnectionListenerThread wird immer eine Referenz auf den erzeugenden
MmpgP2PServiceThread sowie ein Port, auf dem gelauscht werden soll, übergeben. Diese
Referenz nenne ich Parent (Vater-Prozess).
Wird eine eingehende Verbindung verarbeitet, dann wird die incomingConnection()-Methode
des erzeugenden MmpgP2PServiceThread (Parent) aufgerufen. Anschließend wird die Verbindung
durch einen neu erzeugten ConnectionWorkerThread (siehe 5.5.5) gehalten. Danach
kehrt der ConnectionListenerThread zurück zum Lauschen auf seinen Port.
Diese Thread-Klasse wird beim Server und RegionController verwendet, um viele eingehende
Verbindungen anzunehmen und offen zu halten, ohne den eigentlichen Verarbeitungsprozess
zu blockieren.
5.4
5.5.5 ConnectionWorkerThread
Ein ConnectionWorkerThread hält die Verbindung zu einem verbundenen Client offen. Sobald
er anliegende Daten erkennt, wird die incomingData()-Methode des Parents aufgerufen. Dieser
ist dann für die Verarbeitung der Daten verantwortlich.
Nach einer gewissen Zeit von Inaktivität wird die Verbindung zum Client geschlossen. Es
ist also wichtig, dass Clients regelmässig Daten liefern, um die Verbindung aufrecht zu
erhalten. Zu diesem Zweck gibt es sogenannte NOP-Kommandos (siehe 5.5.1), die keine Wirkung auf das
System oder die Spielewelt haben.
5.5.6 RegionControllerThread
Ein RegionControllerThread ist ein Prozess auf einem beliebigen beteiligten
System. Jeder Client muss mindestens einen RCThread starten und
somit einen Teil seiner Rechenleistung an das Gesamtsystem abtreten.
Da man den Clients jedoch nicht trauen kann, werden Regionen nur an Gruppen
von RCs deligiert (RC-Pools), die sich gegenseitig kontrollieren (siehe 4.3.3).
Da die Klasse RegionControllerThread unabhängig von der Klasse ClientThread implementiert wird,
ist es möglich, mehrere RegionControllerThreads auf ein und demselben System zu starten.
Der Betreiber kann Computer bereitstellt, die RegionControllerThreads (aber keine ClientThreads)
starten (vgl. Seti@Home ), und so Flachenhälsen vorzubeugen (siehe 4.2.4).
Als Voraussetzung wird angenommen, dass das komplette System zu
jeder Zeit von einem eindeutig definierten Zustand in einen weiteren
eindeutig definierten Zustand übergeht. Bei gleichen Eingaben und
gleichen Zuständen muss also jeder RegionController diesselben
Ausgaben produzieren können. Zufallswerte müssen daher ``global''
verfügbar sein. Dafür existiert eine Funktion random() im Ruleset.
Beim Initialisieren eines neuen Spieles übernimmt der initiale RegionControllerThread
des Servers die ersten Instanzen von RCThreads. Folgen dann mit der Zeit
genügend Clients mit erreichbaren RegionControllern, werden Bereiche der Welt
an diese deligiert. Die initialen Instanzen auf dem Server sind ``trusted'',
d.h. sie benötigen keine kontrollierenden Instanzen.
Zur Initialisierung bekommt der neue Regioncontroller die benötigten
Daten der Spielewelt übermittelt. Sobald der Transfer abgeschlossen ist,
müssen die Clients, welche am übertragenen Teil der Spielewelt Interesse
bekunden, den/die neunen Regioncontroller abonnieren.
Der RegionControllerThread verwaltet die Objekte, welche seinem Teil der
Spielewelt (seiner Region) zugewiesen sind. Er kann nur Objekte manipulieren,
welche sich innerhalb der Grenzen seines Teiles befinden. Die Manipulation
wird auf der Klasse GameWorld (siehe Abschnitt 5.3.3) ausgeführt.
Jeder RegionControllerThread sollte grundlegende Informationen über die
benachbarten Regionen besitzen. Da die Spielewelt nicht homogen aufgeteilt ist,
weiß man jedoch nicht im Voraus, zu welcher Region eine Kachel ausserhalb
der Region gehört.
Mit Aufruf der start()-Methode des RegionControllerThreads wird die Hauptschleife
gestartet. Diese Schleife führt so lange keinerlei Aktionen durch, bis der
RegionControllerThreads Spielregeln (in Form eines Rulesets, siehe Abschnitt 5.3.1)
und eine Spielewelt (in Form einer GameWorld-Klasse, siehe Abschnitt 5.3.3)
empfangen hat.
Hat er beides erhalten, dann führt er sogenannte Ticks aus . Ein Tick
führt periodisch Aktionen durch. Wie lange das Intervall zwischen zwei Ticks ist,
kann die Implementierung durch Setzen eines Wertes im Ruleset bestimmen. Standardmäßig
wird alle 200ms ein Tick ausgeführt. Was während eines Ticks geschieht wird ebenfalls
im Ruleset definiert, indem der Endanwender diverse abstrakte Methoden implementiert
(siehe Abschnitt 5.3.1).
Der RegionControllerThread kann durchaus seine Hauptschleife ausführen,
ohne dass ein Client verbunden wäre. Dies kann vorkommen, falls der letzte Client
die Region verlassen hat oder falls der RegionControllerThread ``initial'' ist,
das heißt er wurde vom ServerThread gestartet und wartet nun auf die ersten
Clients. Die Kommunikation mit den Clients wird im folgenden Abschnitt behandelt.
Wie bereits beschrieben verbindet sich ein Client mit seinem RegionController, sobald
er vom Server ein gültiges SessionTicket erhalten hat. Über seinen RCQueueWorkerThread
wird die Verbindung aufgebaut. Der Ablauf eines Logins wird in Abbildung 5.1
als Sequenzdiagramm dargestellt.
Sobald der erste Client verbunden ist, wird der RegionControllerThread in jedem Tick
die Änderungen an den Objekten der Welt übertragen. Er führt folgende Schritte aus:
- tickWorld() auf Ruleset ausführen, Spielewelt updaten
- Änderungen der Objekte und Avatare in Sichtweite übertragen
- Falls ein Objekt die Welt verlässt, dieses an den zuständigen RC übertragen
- Warten, bis der nächste Tick fällig ist
Falls der RegionControllerThread durch Überlastung es regelmäßig nicht schafft,
alle diese Aktionen innerhalb eines Ticks auszuführen, dann sollte er gesplittet
werden. Bei meinen durchgeführten Tests war der schwache Upstream meines DSL-Zugangs
des Öfteren dafür verantwortlich, dass ein Tick nicht in der vorgegeben Zeit
abgeschlossen wurde.
5.5.6.5 GZIP-Komprimierung
Während der Entwicklung des Frameworks wurden immer wieder Tests mit menschlichen
Spielern durchgeführt. Da während dieser Zeit meist nur eine DSL-Anbindung zum Internet
verfügbar war, wurde oft die maximale Bandbreite des Upstreams erreicht. Um kurzfristig
eine Verbesserung der Übertragungsleistung zu erreichen, wurde die Komprimierung
einiger Datenpakete mit GZIP implementiert. Der Traffic wurde dadurch auf weniger
als die Hälfte reduziert.
Bei einer Messung wurden bei vier verbundenen Clients durchschnitlich
ca. 50KB/S übertragen, nach der Einschaltung der GZIP-Komprimierung
waren es noch 24KB/S.
Ein ClientThread ist der spielerseitige Teil des Systems. Zum Teilnehmen
am System kontaktiert der Client zunächst den Server und dann die RegionController
seiner Region. Eine grafische Oberfläche enthält diese Implementierung nicht.
Da die Klasse abstrakt definiert ist, müssen nur einige wenige Methoden
implementiert werden. Bei der Entwicklung wurde wert darauf gelegt, dass
die Implementierung möglichst wenig Arbeit selbst erledigen muß. Lediglich
das Anzeigen des Zustandes und das Versenden von benutzerdefinierten Kommandos
muß eine Implementierung bewerkstelligen. Und natürlich muß die Klasse
ClientThread bzw. ihre implementierende Klasse instanziiert und gestartet
werden (siehe Abschnitt 6.6).
Die Methode login() erledigt sämtliche Kommunikation für eine
saubere Authentifizierung.
Zunächst baut der Client eine Verbindung zum Server auf. Er übermittelt
dann Benutzernamen und Kernnwort und erhält bei Erfolg vom Server ein
SessionTicket, mit dem er die Regioncontrollern
abonnieren kann. Der Server überprüft, ob der Spieler einen gültigen
Account besitzt und meldet entsprechende Objekte an die Regioncontroller.
Die Regioncontroller werden somit informiert, dass sich dieser Client
abonnieren will.
Hat der Server die Regioncontroller entsprechend informiert bzw.
hat er neue RegionController zugewiesen, dann erzeugt er das Ticket
und übermittelt es an den Client. Danach kann der Client die Regioncontrollern abonnieren.
Die Verbindung zum Server wird wieder getrennt, um Ressourcen zu sparen. Alle weitere
Kommunikation des Clients mit dem System sollte nun über die RegionController
laufen. Von diesen Erhält das System regelmäßig Updates der Spielewelt
sowie sonstige System-Kommandos. Im Abschnitt 5.5.8 werden die Details
dieser Kommunikation erläutert.
5.5.7.3 Senden von benutzerdefinierten Kommandos
Über die Methode addCommand() werden Befehle in eine Warteschlange
gegeben, die dann vom System an die RegionController übermittelt werden. Die
Implementierung sollte keine System-Kommandos senden. Für System-Funktionen
bietet ClientThread entsprechende Methoden an. Wie die Übertragung der Kommandos
genau funktioniert, wird im nachfolgenden Abschnitt 5.5.8
erläutert.
5.5.8 RCQueueWorkerThread
Der Client muß die im SessionTicket genannten RegionController abonnieren. Um
die Kommunikation mit diesen RegionControllern zu vereinfachen und zu abstrahieren,
wurde eine weitere Zwischenschicht in Form einer Klasse geschaffen: der RCQueueWorkerThread.
Der Client kommuniziert mit dem MMPGP2P-System auschließlich über Methoden,
die der RCQueueWorkerThread zur Verfügung stellt. Soll durch die Methode
ClientThread.addCommand() (siehe 5.5.7.3) ein Kommando übertragen werden,
dann wird dieses Kommando an den RCQueueWorkerThread weitergeleitet.
Er kümmert sich dann um den Versand an alle RegionController.
Der ClientThread überprüft auf der anderen Seite regelmäßig, ob der
RCQueueWorkerThread neue Befehle oder Updates empfangen hat. Die
neuen Befehle können über die Methode getNextCommand() der
Reihe nach abgefragt werden.
Dieses abstahierende Vorgehen erleichtert den Austausch der P2P-Technik,
die zum Versenden und Empfangen von Kommandos oder Updates verwendet wird.
In der aktuellen Version öffnet der RCQueueWorkerThread Netzwerkverbindungen
zu jedem RegionController und versendet Kommandos regelmässig an jeden einzelnen.
Beim Empfangen von Kommandos werden diese einfach in die Empfangswarteschlange
gelegt und können durch die Methode getNextCommand() vom ClientThread
abgerufen werden.
Der ServerThread nimmt eingehende Verbindungen entgegen und überprüft diese.
Bei gültigem Login weist er dem Client eine ID und ein SessionTicket zu.
Dieser Ablauf wurde bereits in Abbildung 5.5 skizziert.
Die Klasse ServerThread ist abstrakt. Der Entwickler muß also eine
kleine Anzahl von Funktionen implementieren, damit die Kommunikation
reibungslos verlaufen kann.
Der Haupt-Thread des Servers startet einen Hintergrundthread, der nach
neuen Verbindungen lauscht. Wird eine neue Verbindung aufgebaut, dann
akzeptiert er diese und reicht sie an den Hauptthread zurück. Danach
wartet er auf die nächste Verbindung.
5.5.9.1 Protokollierung von Server-Statistiken
Der Server kann einige Variablen in regelmäßigen Abständen protokollieren.
Dazu muß lediglich die Property server.stats.inverval auf einen Wert
größer 0 gesetzt werden. Dieser Wert gibt an, in welchen Abständen
(Wert in Millisekunden) der Server einen Protokoll-Eintrag schreibt. Für
regulären Betrieb sind 5 Sekunden ein ausreichender Wert, für intensive
Tests sollten kleiner Intervalle gewählt werden.
Mit der Property server.stats.log_file gibt man die Datei an, in welche protokolliert
wird. Setzt man den Wert server.stats.append auf 1, dann werden neue Einträge
an eine eventuell bestehende Datei angehängt.
Jede Zeile der Datei enthält eine Protokolleintrag. Eine Zeile, die mit # beginnt,
ist als Kommentarzeile markiert und sollte vor einer Auswertung entfernt werden.
Innerhalb einer Zeile sind verschiedene Werte durch Tabulatoren getrennt. Welche Werte in
welcher Spalte auftreten, gibt eine Kommentarzeile zu Beginn an.
Hier ein Beispiel:
#Server start at Sun Sep 25 19:12:10 CEST 2005 Timestamp: 1127668331027
#time regions clients freerc bytes load
52 1 0 0 0 0.10
1052 1 0 0 0 0.10
[...]
14070 1 0 0 0 0.08
15072 1 1 0 0 0.08
16073 1 2 2 0 0.08
17076 1 4 4 0 0.08
18076 1 6 5 0 0.08
19076 1 8 8 0 0.07
Die Spalten bedeuten der Reihe nach:
- Spalte enthält die vergangene Zeit seit dem Start des Servers.
- Anzahl der Regionen im RegionTree
- Verbundene Clients
- Freie RegionController
- Anzahl an übertragenen Bytes (reserviertes Feld, zur Zeit 0)
- Load des Servers. Dieser Wert wird unter Linux aus der Datei /proc/loadavg gelesen.
Das Logfile lässt sich leicht mit gängigen Unix-Tools auswerten. Gängige Tabellenkalkulationen
können die Datei als CSV-Datei einlesen, indem man das Feldtrennzeichen auf Tabulator einstellt.
5.6 HTTP-Schnittstelle
Der ServerThread bietet eine einfache HTTP-Schnittstelle (siehe Abbildung 5.6). Diese ermöglicht die
Ausführung einiger Funktionen und die Abfrage von Statusinformationen. Das Interface
ist in der Klasse ServerThreadHttpInterface umgesetzt und kann leicht erweitert
werden. Grundsätzlich sollte das HTTP-Interface problemlos mit jedem Browser funktionieren.
Getestet wurde das Interface mit Mozilla 1.8b5, FireFox 1.0.2,
Opera 7 und Internet Explorer 6.
Figure 5.6:
Die HTTP-Oberfläche des Servers
|
Erreichbar ist der Server auf dem gleichen Port, auf der auch die Clients verbinden.
Standard ist Port 18987. In der Regel läuft auf meinem System immer ein MMPGP2P-Server,
der unter [17] erreichbar ist.
Beim ersten Verbinden
wird eine Authentifizierung verlangt. Durch Benutzung einer beliebigen Kombination
aus Benutzername und Passwort (z.B: hallo:hallo) gelangt man lediglich in ein Informations-Interface.
Authentifiziert man sich mit Benutzername ``admin'' und dem zugehörigen Kennwort 5.5, dann hat man zusätzliche Möglichkeiten.
Achtung: Es sollte unbedingt ein Kennwort konfiguriert werden, bevor das HTTP-Interface aktiviert wird.
Der Server trennt eine HTTP-Verbindung nach jeder Übertragung. Dieses Vorgehen ist aus
technischen Gründen notwendig gewesen.
Auf der Seite existiert ein Eingabefeld, über das man Kommandozeilen an den Server übermitteln
kann. Einige Kommandos sind als Direkt-Link oben auf der Seite zu finden, andere muss man
eingeben mit Parameter.
- SPLITX <X>:<Y> bzw. SPLITY <X>:<Y>
Durch X:Y wird die zu splittende Region eindeutig identifiziert. Bei SPLITX wird entlang der
X-Koordinate gesplittet, bei SPLITY entsprechend an der Y-Koordinate.
- MERGE <X>:<Y>
Die Region, welche den Punkt X:Y enthält, soll gemerged werden. Der RCPool der Region wird
danach frei.
- SHOWPROPS
Die Properties des Servers ausgeben.
- RCINFO
Eine Liste der freien RegionController, eine Übersicht über den RegionTree
sowie Details zum initialen RegionController ausgeben.
- CLIENTINFO <ID>
Informationen zum Client mit der angegebenen ID ausgeben.
- RESETSYSTEMSTATS
Die SystemStats zurücksetzen, ein neues Protokoll beginnen. Zu weiteren Informatione über
die Statistikfunktionen siehe Abschnitt .
http://www.psitronic.de/ti/mmpg/mmpg-peer_to_peer/
|
|