Client-Server RPC
Allgemeines
Information
- RPC ermöglicht den Zugriff auf entfernte Ressourcen (Prozeduren und Funktionen), üblicherweise über eine Netzwerkschnittstelle
- Dies wird in PLANTA zum gegenseitigen Zugriff auf Funktionalität und Daten zwischen PLANTA Server und PLANTA Client eingesetzt
- Die in PLANTA implementierte RPC-Lösung basiert auf der Python-RPC-Implementierung RPyC (in Version 3.3)
- Zur Nutzung in PLANTA werden die RPyC-Nachrichten über die bestehende Client-Server-Verbindung umgelenkt
- Hierzu werden die Pakete in XML-Nachrichten verpackt
Achtung
- Auf client_exec()/server_exec() basierendes Customizing muss vor Deaktivierung dieser Funktionalität umgestellt werden
- Das bislang verfügbare Standardcustomizing basiert weitgehend auf der ursprünglichen Schnittstelle
Motivation
Ursprünglicher Ansatz
- Bis zur Serverversion 39.5.15 basierte entfernter Zugriff zwischen PLANTA Server und PLANTA Client darauf, dass gegenseitig Python-Skripte über eine XML-Nachricht zugesandt werden konnten
- Der Empfänger der Nachricht führte das darin enthaltene Skript ohne weitere Betrachtung aus
- Dieses Konstrukt erlaubt die Ausführung beliebiger Anweisungen mit den Rechten des Empfängers, was beidseitig hohes Schadpotential birgt
- Ein weiterer Nachteil des Kommunikationsschemas ist die unidirektionale Kommunikation: das Skript wird abgesetzt, ohne dass auf Beendigung gewartet oder ein Rückgabewert erhalten werden kann
- Zur Wahrung der Kompatibilität mit kundenspezifischem Customizing bleibt die Funktionalität bestehen, kann (und sollte) aber deaktiviert werden, sobald bestehendes Customizing auf die RPC-Schnittstelle umgestellt ist
RPC-Ansatz mit Dienstarchitektur
- RPyC erlaubt die Nutzung von Diensten, also klar definierten Funktionalitätseinheiten, die ein Client nutzen kann
- Durch die Implementierung der Dienste ist somit der Funktionsumfang eingeschränkt
- Bei der Implementierung muss jedoch sichergestellt werden, dass dadurch keine Lücke geöffnet wird; so dürfen Parameter nicht über
eval()
ausgewertet werden, da dies wiederum die Ausführung beliebiger Anweisungen ermöglicht - RPyC basiert auf bidirektionaler Kommunikation; Hierbei kann eine Dienstfunktionalität sowohl synchron verwendet werden als auch asynchron (auch mit später abfragbarem Rückgabewert)
Benutzung
Definition von Diensten
- Ein Dienst besteht aus einer Klasse, die von
rpyc.Service
erbt - Weiterhin sollte die Klasse keinen eigenen Konstruktor besitzen
- Die Methoden
on_connect()
undon_disconnect()
erlauben die Initialisierung und Ressourcen-Freigabe bei Erstellung/Entfernung der Dienstinstanz - Freigegebene Felder und Methoden werden durch das Präfix
exposed_
gekennzeichnet - In der (empfohlenen, weil sicheren) Standardkonfiguration können nur auf diesem Wege veröffentlichte Dienstteile verwendet werden
- Das folgende Beispiel wird auch im weiteren zur Erläuterung der Verwendung der Schnittstelle genutzt:
import rpyc
import time
class EchoService(rpyc.Service):
def on_connect(self):
pass
def on_disconnect(self):
pass
def exposed_echo(self, somevalue):
return somevalue
def exposed_long_echo(self, somevalue):
time.sleep(30) # simulate a longer calculation...
return somevalue
Zugriff auf veröffentlichte Dienste
- Über die Schnittstelle ist die Registrierung und Verwendung beliebig vieler Dienste möglich
- Um den von einer Nachricht referenzierten Dienst aufzulösen, kommt ein Verzeichnis zum Einsatz, in dem alle selbst veröffentlichten Dienste und verwendete entfernte Dienste eingetragen sind
- Der Schlüssel zum Zugriff auf dieses Verzeichnis ist ein frei zu vergebender Name; zur Vermeidung von Konflikten bietet sich eine Namensraumbasierte Vergabe an (z.B. "examples.echo_service" für den Beispieldienst)
- Schnittstellenfunktionen für das Dienstverzeichnis sind im Modul
csrpc.core.registration
vorhanden:
def get_service_directory():
"""Gibt das Dienst-Verzeichnis zurück"""
def get_service(name):
"""Gibt den Dienst zurück, der unter [name] registriert ist; wirft ServiceDirectoryError, wenn der Schlüssel [name] nicht vorhanden ist"""
def register_service(name, service=None):
"""Registriert den durch die Klasse [service] definierten Dienst unter dem Namen [name]; wirft ServiceDirectoryError im Konfliktfall"""
def access_service(name):
"""Baut eine Verbindung zum entfernt unter [name] registrierten Dienst auf; entspricht Aufruf von register_service(name)"""
def deregister_service(name):
"""Entfernt Dienst/Verbindung aus dem Verzeichnis; wirft ServiceDirectoryError, wenn der Schlüssel [name] nicht vorhanden ist"""
Registrierung entfernter Dienste
- Um entfernt vorhandene Dienste verwendbar zu machen, kann ein Client- und Serverseitig verfügbarer Dienst genutzt werden
- Dieser Dienstregistrierungs-Dienst ist über den Schlüssel "service_registration" erreichbar
- Die Verbindung zu diesem Dienst muss wie bei anderen Diensten auch hergestellt werden:
from csrpc.core import registration
from csrpc.core.exceptions import ServiceDirectoryError
try:
sr = registration.access_service('service_registration')
except ServiceDirectoryError:
sr = registration.get_service('service_registration')
- Die entfernte Dienstklasse, im Beispiel EchoService wird dann folgendermaßen registriert:
sr.root.register('examples.echo_service', 'csrpc.services.examples.EchoServer')
- Mit diesem entfernt registrierten Dienst kann nun wie oben beschrieben eine Verbindung eingegangen werden:
echo_service = registration.access_service('examples.echo_service')
Verwendung der Dienstfunktionalität
- Alle veröffentlichten Methoden und Attribute des entfernten Dienstes sind im Verzeichnis
root
der lokalen Repräsentation vorhanden - Ein Methodenaufruf sieht im Beispiel dann wie folgt aus (das Präfix braucht nicht angegeben zu werden!):
result = echo_service.root.echo('hello world')
- Bei synchroner entfernter Programmausführung (wie oben) wartet der lokale Aufruf blockierend auf die Rückgabe des entfernten Aufrufes
- Bei langen Berechnungen oder bei Irrelevanz der Rückgabe können wie folgt asynchrone Aufrufe durchgeführt werden:
import rpyc
alongecho = rpyc.async(echo_service.root.long_echo)
res = alongecho('A really long calculation')
- Der Aufruf kommt augenblicklich wieder zurück; res ist ein Container, der bei Eintreffen des Rückgabewertes befüllt wird
- Mit Hilfe des Containers kann auch die Beendigung abgeprüft oder wiederum blockierend auf das Ergebnis gewartet werden