Einzelnen Beitrag anzeigen

mytbo

Registriert seit: 8. Jan 2007
472 Beiträge
 
#1

mORMot: Einführung in Interface-based Services - Server und Client

  Alt 13. Jul 2022, 23:41
Auch der vierte Artikel der mORMot Vorstellungsreihe behandelt den Bereich, für den die Bibliothek erstellt wurde. Er setzt Grundwissen voraus, das im dritten Artikel, Einführung in methodenbasierte Services - Server und Client, erarbeitet wurde. Wer die Heranführung an das Thema noch nicht kennt, sollte es vor dem Weiterlesen nachholen. Die enthaltenen Verweise zur Dokumentation verlinken zur aktuell verfügbaren mORMot1 Hilfe. Für das Beispiel wird mORMot2 verwendet. Die Namen für Klassen und Funktionen können sich leicht unterscheiden.

Im Anhang befindet sich der Sourcecode und das ausführbare Programm für Server und Client. Disclaimer: Der Sourcecode ist weder getestet noch optimiert. Er sollte mit Delphi ab Version 10.2 funktionieren. Die Benutzung der zur Verfügung gestellten Materialien erfolgt auf eigene Gefahr. Und heute noch verschärft: Wer den Beispiel-Server trotz aller Warnungen im Internet zugänglich macht, soll zur Strafe den folgenden Satz 1000-mal schreiben: "Ich werde nie wieder ein Programm in einem Bereich einsetzen, für das es nicht vorgesehen wurde!". Der Vollzug wird von mir persönlich überwacht.

In Abwandlung der ersten drei Beispiel-Anwendungen werden dieses Mal Textdokumente verwaltet. Der Server speichert die Dokumente im benutzereigenen Verzeichnis (jeder Benutzer hat sein eigenes Verzeichnis). Mit dem Beispiel Quelltext für den Server und Client bekommt man:
  • Einen HTTP Server für HTTP/1.0/1.1 Anfragen.
  • Authentifizierung des Benutzers am Server und Anlage eines eigenen Verzeichnisses.
  • Anmeldung mit dem Client am Server und Austausch von Inhaltsliste und Textdokumenten.
  • Einen einfachen Markdown Editor mit Vorschau der HTML Ausgabe im Browser Fenster.
Grundsätzliches Vorwissen
Interface-based Services sind ein weites Gebiet in mORMot und die Art der Umsetzung ein wahres Hochamt. Die folgenden Ausführungen sind nur ein Hineinschnuppern ins jeweilige Thema und stark vereinfacht. Jedes Einzelne würde eine eigene Abhandlung rechtfertigen. Sie sind zum Verstehen der Beispiel-Anwendung wichtig. Wer mehr wissen will, sollte einen Blick in die Hilfe werfen und bei Client-Server services via interfaces beginnen.

Um zwischen Server und Client zu kommunizieren, benötigt man einen Kontrakt. Bei Interface-based Services wird dieser Kontrakt durch die Definition eines Interfaces erstellt. Und so lange die Definition des Interfaces auf dem Server und den Clients gleich ist, ist eine reibungslose Kommunikation gewährleistet. Alle Maßnahmen, die für dieses Zusammenspiel der beide Seiten (Server und Client) notwendig sind, werden von mORMot automatisch erledigt. Der Entwickler muss sich nur um die Business-Logic im jeweiligen Service Objekt kümmern. Kann oder darf man das Glauben? Dazu muss man weiter lesen...

1) Namenskonvention
Wie benenne ich meinen Service und die Objekte, die damit arbeiten? Darüber streiten sich die Geister und manche Empfehlungen auf Grundlage akademischer Betrachtungen sind konträr. Ich habe mir angewöhnt das Interface nach seiner Bestimmung (Subjekt) ohne jeden Zusatz zu benennen - darüber mag man trefflich streiten. Im Beispiel wäre das IDocument. Die Service Objekte, die diese Schnittstelle im Server und Client implementieren, werden mit dem Suffix Service versehen. Das Objekt wird demnach TDocumentService benannt.

2) Interfaces definieren
Jede Schnittstelle benötigt eine GUID um den Zugriff über RTTI zu gewährleisten. Die Verwendung von IInvokable ist eine gute Wahl. Schnittstellen dürfen vererbt werden. Interface Methoden können Funktionen oder Prozeduren sein. Die Parameter können per Value oder per Referenz übergeben werden. Die erlaubten Parameter umfassen einfache Typen wie String, Integer oder Aufzählungen, aber auch komplexe Typen wie Objecte, Records und dynamische Arrays. Eine Übersicht erlaubter Typen kann in der Hilfe nachgelesen werden.

Auf die Codierung der zwischen Server und Client übertragenen Daten hat man erst einmal keinen Einfluss. RawByteString oder komplexe Typen wie Records werden als Base64 kodiertes JSON übertragen. Vorteilhaft für die Übertragung größerer Dateien ist es, den Base64-Kodierungs-Overhead zu meiden und besser auf methodenbasierte Services zurückzugreifen. Beide Verfahren, Method- und Interface-based Services, können gemeinsam eingesetzt werden. Kleiner Einschub: Auch Interface Funktionen bieten mit dem Record TServiceCustomAnswer als Result die Möglichkeit, ein eigenes Datenformat für die Rückgabe zu verwenden.

3) Lebenszeit eines Service Objekts
Spielen wir den Ablaufvorgang vom Client zum Server bei der Ausführung eines Service Aufrufs einmal nach. Über die RestHttpClient Klasse wird das gewünschte Interface besorgt. Dieses ermöglicht den Aufruf einer seiner Methoden. mORMot schickt diese Anforderung zum Server. Im RestServer des Server ist für jede Schnittstelle ein Service Objekt registriert, das jetzt aufgerufen oder instanziiert wird. Wie es nach Abarbeitung seiner Aufgabe mit dem Service Objekt weiter geht, welche Lebenszeit es hat, wird mit einer Option vom Typ TServiceInstanceImplementation beeinflusst. Diese wird bei der Registrierung der Schnittstelle festgelegt. Eine Schnittstelle muss auf Server und Client mit den gleichen Einstellungen betrieben werden. Es gibt folgende Optionen:
  • sicSingle - Für jeden Aufruf wird eine Objektinstanz erstellt. Das ist die aufwendigste, aber auch sicherste Art einen Dienst zu betreiben. Ist der Umgang noch ungeübt, welche Option sich für den konkreten Fall eignet, behält man diese Einstellung, die auch der Standard ist, bei.
  • sicShared - Eine Objektinstanz wird für alle eingehenden Aufrufe verwendet. Die Implementierung der Funktion auf dem Server muss thread-sicher sein. Das sind z.B. alle Aufrufe über das interne ORM.
  • sicClientDriven - Eine Objektinstanz wird synchron mit der Lebensdauer der entsprechenden Schnittstelle auf der Client-Seite erstellt und auch wieder freigegeben. Einfach ausgedrückt: Wird die Schnittstelle auf dem Client abgeräumt, trifft es auch das Service Objekt auf dem Server.
  • sicPerSession, sicPerUser, sicPerGroup, sicPerThread - Wie es der Name sagt, ist die Instanz der laufenden Session, dem Benutzer, der Benutzergruppe oder dem Thread zugeordnet.
Zwischenstopp: mORMot ist so viel schneller als die Bordmittel, dass die Standard Einstellungen ausreichen und Tuning lange Zeit kein Thema sein wird. Hat man sich eingearbeitet, wird, was jetzt schwer und schwierig erscheint, einfach und leicht.

4) Ausführungsoptionen einer Schnittstelle
Bei der Registrierung einer Schnittstelle am Server können Optionen zur Ausführung TInterfaceMethodOptions spezifiziert werden. Eine Methoden wird standardmäßig in dem Thread aufgerufen, der sie empfangen hat. Das heißt, die Methoden sind re-entrant und die Ausführung muss thread-safe sein. Ausnahme: sicSingle und sicPerThread. Dieses Verhalten lässt sich über die ServiceFactoryServer Instanz, besprochen im nächsten Abschnitt, einer Schnittstelle beeinflussen. Eine Methode soll z.B. im Hauptthread ausgeführt werden: (RestServer.ServiceContainer.Info(IDocument) as TServiceFactoryServer).SetOptions(['GetAllNames'], [optExecInMainThread]); Mehr dazu in der Hilfe unter Server-side execution options (threading).

5) Sicherheit beim Zugriff auf die Schnittstelle
Für den Zugriff auf eine Schnittstelle muss eine Authentifizierung vorliegen. Außer, durch setzen der Eigenschaft TServiceFactoryServer.ByPassAuthentication wird diese Notwendigkeit deaktiviert. Jede registrierte Schnittstelle wird durch eine Instanz dieser Klasse repräsentiert. Sie ist über RestServer.ServiceContainer zugänglich. Da auch die Autorisierung der Schnittstellen-Methoden über die ServiceFactoryServer Instanz abläuft, hier schon mal ein Beispiel: (RestServer.ServiceContainer.Info(IDocument) as TServiceFactoryServer).ByPassAuthentication := True; Der Zugriff auf die Schnittstellen kann autorisiert werden. In der Voreinstellung ist er unbeschränkt. Die Sicherheitsrichtlinien lassen sich für jede Schnittstelle und jeder Methode einer Schnittstelle genau abstimmen. Dazu stehen die Funktionen Allow, AllowAll, AllowAllByID, AllowAllByName, Deny, DenyAll, DenyAllByID und DenyAllByName zur Verfügung. Diese Methoden sind als Fluent Interfaces ausgeführt. Beispiel: (RestServer.ServiceContainer.Info(IDocument) as TServiceFactoryServer).Allow(['Load', 'Save']).Deny(['GetAllNames']); Oder meiner Vorliebe folgend, die String Bezeichner durch Konstanten zu ersetzen, und Folgendes schreiben: .Allow([TDocumentService.IMN.Load, TDocumentService.IMN.Save]).Deny([TDocumentService.IMN.GetAllNames]); Die Abkürzung IMN steht für Interface-Method-Name.

Der eigene RestServer
Das Interface des Service Objekts des Servers ist sehr einfach gehalten und umfasst nur wenige Funktionen:
Delphi-Quellcode:
TDocumentService = class(TInjectableObjectRest, IDocument)
public
  function GetSessionUserDirName(out pmoDirName: TFileName): Boolean;
  //*** IDocument ***
  function Load(const pmcName: TFileName; out pmoData: RawBlob): Boolean;
  function Save(const pmcName: TFileName; const pmcData: RawBlob): Boolean;
  procedure GetAllNames(out pmoFileNames: TFileNameDynArray);
end;
Wie das Thema schon vorgibt, implementieren wir Interface-basierte Services. Das Service Objekt definiert die drei Funktionen Load, Save und GetAllNames des IDocument Interfaces. Schauen wir uns die Implementierung am Beispiel der Service Methode GetAllNames an. Die Methode liefert die Inhaltsliste des benutzereigenen Datenverzeichnisses zurück. Das Ergebnis wird als dynamisches Array of TFileName zurückgeliefert.
Delphi-Quellcode:
procedure TDocumentService.GetAllNames(out pmoFileNames: TFileNameDynArray);
var
  dirName: TFileName;
begin
  if not GetSessionUserDirName(dirName) then Exit; //=>

  pmoFileNames := FileNames(dirName, FILES_ALL, [ffoSortByName, ffoExcludesDir]);
  for var i: Integer := Low(pmoFileNames) to High(pmoFileNames) do
  begin
    if ExtractFileExt(pmoFileNames[i]) = TFileRestServer.DEFAULT_FILE_EXT then
      pmoFileNames[i] := GetFileNameWithoutExt(pmoFileNames[i]);
  end;
end;
Ganz wichtig zu beachten ist der Grundsatz: Vertraue niemals Daten, die von außen kommen! Unsere Funktion benötigt keine Eingangsparameter. Auch bei dieser Art der Service Implementierung sind alle ankommenden Daten zu überprüfen. mORMot serialisiert das TFileNameDynArray als JSON und schickt es zum Client. Dieser Vorgang läuft automatisch ab und verlangt kein Eingreifen. Es gibt keinen merklichen Unterschied in der Umsetzung der Funktion zur gewöhnlichen Handhabung.

Benutzerverwaltung für den Server
Im RestServer muss heute nur die Authentifizierung programmiert werden. Die Verwaltung wird mit den ORM Klassen TAuthGroup und TAuthUser oder deren Nachfahren aufgebaut. Unbedingt beachten muss man, dass in beiden Tabellen Vorgaben automatisch erstellt werden, wenn dieses Verhalten nicht explizit unterdrückt wird. Die angelegten Datensätze für die Gruppe sind nützlich, aber alle Benutzer sollten selbst erstellt werden.
Delphi-Quellcode:
function TFileRestServer.InitAuthForAllCustomers(const pmcFileName: TFileName): Boolean;
var
  json: RawUtf8;
  customers: TCustomerItemArray;
begin
  Result := False;
  if not FileExists(pmcFileName) then Exit; //=>

  json := AnyTextFileToRawUtf8(pmcFileName, {AssumeUtf8IfNoBom=} True);
  if IsValidJson(json)
    and (DynArrayLoadJson(customers, Pointer(json), TypeInfo(TCustomerItemArray)) <> Nil) then
  begin
    var authUser: TFileAuthUser := TFileAuthUser.Create;
    try
      for var i: Integer := 0 to High(customers) do
      begin
        if (customers[i].LoginUserName <> '')
          and (customers[i].LoginPassword <> '') then
        begin
          authUser.CustomerNum := customers[i].CustomerNum;
          ...
          authUser.GroupRights := TAuthGroup(3); // AuthGroup: User
          Server.Add(authUser, True);
        end;
      end;
    finally
      authUser.Free;
    end;

    Result := (Server.TableRowCount(TFileAuthUser) > 0);
  end;
end;
Die Benutzerdaten im Beispiel werden aus der Datei "Customer.config" im Programmverzeichnis geladen.

Der vollständige Server
Das Programm besteht aus zwei Komponenten, dem oben beschriebenen RestServer und dem Übertragungsprotokoll HTTP. Mehr Aufwand ist nicht notwendig, einen HTTP-Server an den Start zu bringen:
Delphi-Quellcode:
function TTestServerMain.RunServer(const pmcPort: RawUtf8): Boolean;
begin
  Result := False;
  if (FHttpServer = Nil)
    and FRestServer.InitAuthForAllCustomers(FCustomerConfigFile) then
  begin
    FHttpServer := TRestHttpServer.Create(pmcPort, [FRestServer], '+{DomainName}, useHttpSocket {or useHttpAsync});
    FHttpServer.AccessControlAllowOrigin := '*';
    Result := True;
  end;
end;
Für das URL-Präfix wird ein Wildcard Zeichen verwendet, "+" bindet an alle Domainnamen für den angegebenen Port.

Die Client Anwendung
Am einfachsten bauen wir uns ein Pendant zur Klasse TDocumentService für den Client. Darin wird die komplette REST Logik gekapselt. Jede Service Funktion des Servers bekommt ihr Gegenstück, mit der für den Client notwendigen Funktionalität. Zwecks der besseren Übersichtlichkeit habe ich für das Beispiel den RestServer direkt im Service Objekt des Clients erstellt:
Delphi-Quellcode:
TDocumentService = class(TObject)
strict private
  FClient: TRestHttpClient;
public
  constructor Create(const pmcServerURI: RawUtf8; const pmcServerPort: RawUtf8);
  destructor Destroy; override;
  function InitializeServices: Boolean;
  function Load(pmEditor: TMemo; const pmcDocName: String): Boolean;
  function Save(pmEditor: TMemo; const pmcDocName: String): Boolean;
  procedure GetAllNames(pmFileNames: TStrings);
  property Client: TRestHttpClient
    read FClient;
end;
Über die Funktion Resolve des RestServers erhalten wir das Interface IDocument. Um die Kommunikation mit dem Server aufzunehmen und dort die Schnittstelle mit der Instanziierung der TDocumentService Klasse zum Leben zu erwecken, ist nur ein Aufruf der Methode GetAllNames notwendig. Zu beachten ist, dass von der Schnittstelle erzeugte Objekt, z.B. in einem T*ObjArray, selbst freizugeben sind.
Delphi-Quellcode:
procedure TDocumentService.GetAllNames(pmFileNames: TStrings);
var
  service: IDocument;
  fileNames: TFileNameDynArray;
begin
  if pmFileNames = Nil then Exit; //=>
  if not FClient.Resolve(IDocument, service) then Exit; //=>

  pmFileNames.BeginUpdate;
  try
    pmFileNames.Clear;
    service.GetAllNames(fileNames);
    for var i: Integer := 0 to High(fileNames) do
      pmFileNames.Add(fileNames[i]);
  finally
    pmFileNames.EndUpdate;
  end;
end;
Ich hoffe, ich habe am Anfang nicht zu viel versprochen und das Weiterlesen hat sich gelohnt.

Zusammenfassung
Auch der heutige Artikel war vor allem durch die Notwendigkeit zum radikalen Kürzen eine besondere Herausforderung. Bei dieser Aktion besteht immer die Gefahr, dass der rote Faden zum Verständnis verloren geht. Ich hoffe, die Ausführungen blieben nachvollziehbar.

mORMot ist gut dokumentiert. Die Hilfe umfasst mehr als 2500 Seiten. Davon enthalten die ersten ca. 650 Seiten einen sehr lesenswerten allgemeinen Teil, der Rest ist API Dokumentation. mORMot muss nicht in der IDE installierten werden! Es reicht aus, die entsprechenden Bibliothekspfade einzufügen. Es stehen viele Beispiele und ein freundliches Forum zur Verfügung. Wenn mehr Interesse an mORMot besteht, kann ich auch andere Teile in ähnlicher Weise kurz vorstellen.

In der mORMot Reihe bisher veröffentlicht:
  1. ORM und DocVariant kurz vorgestellt
  2. ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
  3. Einführung in methodenbasierte Services - Server und Client
Bis bald...
Thomas
Angehängte Dateien
Dateityp: zip TestInterfaceBasedServicesSource.zip (10,8 KB, 51x aufgerufen)
Dateityp: zip TestInterfaceBasedServicesExe.zip (2,98 MB, 35x aufgerufen)
  Mit Zitat antworten Zitat