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() und on_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
PY

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"""
PY

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')
PY
  • Die entfernte Dienstklasse, im Beispiel EchoService wird dann folgendermaßen registriert:
sr.root.register('examples.echo_service', 'csrpc.services.examples.EchoServer')
PY
  • Mit diesem entfernt registrierten Dienst kann nun wie oben beschrieben eine Verbindung eingegangen werden:
echo_service = registration.access_service('examples.echo_service')
PY

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')
PY
  • 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')
PY
  • 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