Einzelnen Beitrag anzeigen

mytbo

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

mORMot: Einführung in methodenbasierte Services - Server und Client

  Alt 8. Jul 2022, 20:10
Der dritte Artikel der mORMot Vorstellungsreihe behandelt das erste Mal den Bereich, für den die Bibliothek erstellt wurde. Arnaud Bouchez bezeichnet sein Werk als Client-Server ORM/SOA-Framework und schreibt dazu: "The Synopse mORMot framework implements a Client-Server RESTful architecture, trying to follow some MVC, N-Tier, ORM, SOA best-practice patterns". Die Umsetzung passiert auf den Grundsätzen dieser Architektur Prinzipien. Wem die Begriffe SOA und RESTful-Architektur nicht viel sagen, kann sich unter folgenden Links einlesen:Die Beispiel-Anwendung Server und Client soll möglichst viel Funktionalität mit wenigen Zeilen Quelltext präsentieren. Es geht hier um Konzepte, nicht um eine fertige Copy-Paste Lösung. 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.

Angelehnt an die ersten beiden Beispiel-Anwendungen werden auch hier wieder Bilder verwaltet. Der Server speichert die Bilder 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 Bilddokumenten.
  • Anbindung einer Fortschrittsanzeige mit Hilfe eines Mediators.
  • Beschleunigung beim Speichern und Lesen der Grafik-Formate JPEG, PNG, GIF, TIFF.
Grundsätzliches Vorwissen
Die folgenden Punkte werden nur kurz angerissen. Jeder Einzelne würde eine eigene Abhandlung rechtfertigen. Sie sind zum Verstehen der Beispiel-Anwendung wichtig.

1) REST-Server
Der RestServer erbt die REST-Funktionalität von der Klasse TRestServer* und ihrer Nachfahren. In einem Programm können beliebig viele RestServer mit jeweils eigenen Model betrieben werden. Jedoch muss jedes Model einen eindeutigen Rootnamen haben. In einer Shop-Anwendung ist es zum Beispiel sinnvoll, die Bereiche Administration und Shop mit ihren jeweiligen Funktionen auf spezialisierte RestServer zu verteilen. Wenn der RestServer "Shop" unter dem Rootnamen Shop registriert ist, wird eine Service Funktion ArticleBuy über den Root Shop/ArticleBuy aufgerufen. Eine völlständige URI könnte wie folgt aussehen:
Code:
https://www.domain.de/Shop/ArticleBuy?ArticleID=123&AccessToken=xyz
In mORMot sind zur Zeit diese Nachfahren der TRestServer Klasse vorhanden:
  • TRestServerFullMemory - Das ideale Tor zur Außenwelt. Diese leichtgewichtige Klasse implementiert eine schnelle In-Memory-Engine mit grundlegenden CRUD-Funktionen für das ORM. Sie genügt vollständig, um die Authentifizierung zu handhaben, und Dienste auf eigenständige Weise zu hosten. Funktionalität, die nicht vorhanden ist, bietet auch keine Angriffsfläche.
  • TRestServerDB - Diese Klasse ist die wichtigste ihrer Art und hostet eine SQLite-Engine als zentrale Datenbankschicht. Ihre Möglichkeiten wurden schon im ersten Artikel der Reihe aufgezeigt.
2) Verbindungsprotokoll
Der RestServer kann stand-alone, also direkt über seine Methoden oder in einem Client-Server-Modell über die Kommunikationsschicht HTTP/1.0/1.1 über TCP/IP angesprochen werden. Die Möglichkeiten hängen vom verwendeten Protokoll ab. Für eine Client-Server Anwendung findet allgemein Verwendung, spezielle Fälle wie WebSockets lassen wir mal außen vor, auf dem Server die Klasse TRestHttpServer und dem Client die Klasse TRestHttpClient* oder Nachfahren. Beim Instanziieren der Klasse TRestHttpServer stehen verschiede Server-Modi zur Auswahl:
  • useHttpApi* - Verwendet den kernel-mode http.sys Server, der seit Windows XP SP3 zur Verfügung steht.
  • useHttpAsync - Ein Socket basierter Server im event-driven Modus mit einem Thread-Pool. Daher skaliert er bei vielen Verbindungen sehr gut, insbesondere bei Keep-Alive-Verbindungen.
  • useHttpSocket - Ein Socket basierter Server mit einem Thread pro aktiver Verbindung unter Verwendung eines Thread-Pools. Dieser Typ sollte besser hinter einem Reverse-Proxy-Server im HTTP/1.0-Modus eingesetzt werden.
Für den Client stehen die Klasse TRestHttpClientSocket, TRestHttpClientWinINet und TRestHttpClientWinHttp zur Verfügung. Im Beispiel verwenden wir WinHttp um eine Fortschrittsanzeige mit Hilfe eines Mediators zu realisieren.

3) Authentifizierung
Die in mORMot vorhanden Authentifizierungsklassen sind: TRestServerAuthentication* None, Default, HttpBasic und Sspi. Als Alternative ist die Verwendung von JWT (JSON Web Tokens) möglich. Im Beispiel kommt die mORMot eigene proprietäre Lösung zur Anwendung. Sie ist in der Klasse TRestServerAuthenticationDefault implementiert. Im Beispiel-Server ist die Protokollierung aktiviert. Im Ausdruck lässt sich das Ping/Pong zwischen Client und Server zum Aufbau einer Session verfolgen. Das Verfahren ist ein guter Kompromiss zwischen Sicherheit und Geschwindigkeit.

Auch wenn die Authentifizierung aktiviert ist, können einzelne Methoden durch Aufruf der RestServer Funktion ServiceMethodByPassAuthentication('MethodName') davon ausgenommen werden. Eine der standardmäßig frei zugänglichen Funktionen ist Timestamp. Zum Testen am Beispiel-Server folgenden Aufruf im Browser eingeben:
Code:
http://localhost:8080/Files/Timestamp
Nach Ausführung wird im Browser Fenster eine mehrstellige Nummer, der Timestamp des Servers, angezeigt.

Beschleunigung beim Speichern und Lesen von Bildern
Der letzte Punkt in der Liste wurde schon in den vorherigen Artikel besprochen. Er wird nur zur Komplementierung noch einmal erwähnt. Hier der Link zu den Benchmark-Werten.

Der eigene RestServer
In mORMot unterscheidet man zwei Arten von Services:Um das Ganze nicht ausufern zu lassen, unterschlage ich weitere Möglichkeiten. Sie sollen hier keine Rolle spielen.

Das Interface des Rest-Servers ist sehr einfach gehalten und umfasst nur wenige Funktionen:
Delphi-Quellcode:
TFileRestServer = class(TRestServerFullMemory)
strict private
  FDataFolder: TFileName;
protected
  const
    DEFAULT_FILE_EXT = '._';
protected
  function GetSessionUserDirName(pmCtxt: TRestServerUriContext; out pmoDirName: TFileName): Boolean;
  property DataFolder: TFileName
    read FDataFolder;
public
  constructor Create(const pmcRootName: RawUtf8; const pmcDataFolder: TFileName); reintroduce;
  function InitAuthForAllCustomers(const pmcFileName: TFileName): Boolean;
published
  procedure LoadFile(pmCtxt: TRestServerUriContext);
  procedure SaveFile(pmCtxt: TRestServerUriContext);
  procedure GetAllFileNames(pmCtxt: TRestServerUriContext);
end;
Wie das Thema schon vorgibt, implementieren wir methodenbasierte Services. Sieht man sich die Definition an, fallen die drei Funktionen LoadFile, SaveFile und GetAllFileNames im published Abschnitt auf. Der Prototyp ist wie folgt definiert:
Delphi-Quellcode:
type
  TOnRestServerCallBack = procedure(pmCtxt: TRestServerUriContext) of Object;
Jede auf diese Weise definiert Methode im published Abschnitt wird automatisch als Service Funktion registriert. Der Name der Methode ist der Servicename. Geroutet wird immer über RootName/ServiceName. Im besprochenen Beispiel ist RootName "Files". Damit sieht die Route für die Funktion GetAllFileNames wie folgt aus:
Code:
GET Files/GetAllFileNames
Methoden lassen sich mit der RestServer Funktion ServiceMethodRegister('MethodName', MyRestServerCallBackEvent) auch direkt registrieren. Im published Abschnitt stehende Methoden dieses Typs werden automatisch registriert. Ist doch gar nicht kompliziert.

Einen Service implementieren
Schauen wir uns ein konkretes Beispiel an. Die Abfrage des Services GetAllFileNames liefert die Inhaltsliste des benutzereigenen Datenverzeichnisses zurück. Das Ergebnis wird im JSON Format zurückgeliefert.
Delphi-Quellcode:
procedure TFileRestServer.GetAllFileNames(pmCtxt: TRestServerUriContext);
var
  dirName: TFileName;
  dirFiles: TFileNameDynArray;
begin
  if (pmCtxt.Method = mGET)
    and GetSessionUserDirName(pmCtxt, dirName) then
  begin
    dirFiles := FileNames([DataFolder, dirName], FILES_ALL, [ffoSortByName, ffoExcludesDir]);
    for var i: Integer := Low(dirFiles) to High(dirFiles) do
    begin
      if ExtractFileExt(dirFiles[i]) = DEFAULT_FILE_EXT then
        dirFiles[i] := GetFileNameWithoutExt(dirFiles[i]);
    end;

    pmCtxt.Returns(DynArraySaveJson(dirFiles, TypeInfo(TFileNameDynArray)), HTTP_SUCCESS);
  end
  else
    pmCtxt.Error(StringToUtf8(SErrHttpForbidden), HTTP_FORBIDDEN);
end;
Ganz wichtig zu beachten ist der Grundsatz: Vertraue niemals Daten, die von außen kommen! Unsere Funktion benötigt keine Eingangsparameter. Die Autorisierung des Zugriffs auf die Funktion von außen wird schon über die Authentifizierung berechtigt. Die Klasse TRestServerUriContext besitzt eine Vielzahl von Funktionen, wie z.B. die komfortable Handhabung der Ein- und Ausgangswerte, oder der Session Daten. Zur Serialisierung der Daten wird die Funktion DynArraySaveJson verwendet. Mit dem mORMot Werkzeugkasten bleibt kein Wunsch offen und einfacher gehts auch nicht.

Benutzerverwaltung für den Server
Beim Instanziieren des RestServers wird die Authentifizierung aktiviert. 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. Die Benutzerdaten im Beispiel werden aus der Datei "Customer.config" im Programmverzeichnis geladen. Die gesamte Umsetzung erfolgt in wenigen Zeilen Sourcecode.

Der vollständige Server
Ein Server sollte auf Basis der Klasse TSynDaemon erstellt werden. Zwecks der besseren Übersichtlichkeit habe ich für das Beispiel eine einfachere Variante gewählt. 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 TFileRestServer für den Client. Darin wird die komplette REST Logik gekapselt. Als Vorfahre kommt die Klasse TRestHttpClientWinHttp zur Anwendung. Sie eröffnet den Zugriff auf die Klasse TWinHttpApi, die eine einfache Anbindung unserer Fortschrittsanzeige ermöglicht. Jede Service Funktion des Servers bekommt ihr Gegenstück, mit der für den Client notwendigen Funktionalität:
Delphi-Quellcode:
TFileHttpClient = class(TRestHttpClientWinHttp)
public
  constructor Create(const pmcServerURI: RawUtf8 = SERVER_URI; const pmcServerPort: RawUtf8 = SERVER_PORT); reintroduce;
  function LoadImage(pmImage: TImage; const pmcImageName: String): Boolean;
  function SaveImage(pmImage: TImage; const pmcImageName: String): Boolean;
  procedure GetAllFileNames(pmFileNames: TStrings);
end;
Über die CallBack* Funktionen werden die Service Methoden auf dem Server aufgerufen. Dabei wird das RESTful Root-Schema ModelRoot/MethodName verwendet. Benötigt ein Aufruf Parameter, wird der Query-String mit Hilfe der Funktion UrlEncode durch Übergabe von Name/Value Pairs gebildet. Im Erfolgsfall wird 200/HTTP_SUCCESS zurückgegeben. In der Unit mormot.core.os sind viele HTTP Status Code Konstanten definiert. Die konkrete Implementierung sieht wie folgt aus:
Delphi-Quellcode:
function TFileHttpClient.SaveImage(pmImage: TImage; const pmcImageName: String): Boolean;
var
  return: RawUtf8;
begin
  Result := False;
  if pmImage = Nil then Exit; //=>
  if pmcImageName = 'then Exit; //=>

  var stream: TRawByteStringStream := TRawByteStringStream.Create;
  try
    pmImage.Picture.SaveToStream(stream);
    if stream.Position > 0 then
    begin
      var methodName: RawUtf8 := TFileServiceFunction.Name.SaveFile
        + UrlEncode([TFileServiceFunction.Param.SaveFile_ImageName, StringToUtf8(pmcImageName)]);

      Result := (CallBackPut(methodName, stream.DataString, return) = HTTP_SUCCESS);
    end;
  finally
    stream.Free;
  end;
end;
Wem es noch nicht aufgefallen ist, ich vermeide String Bezeichner, wo es nur geht. Ich ziehe es vor, dafür Konstanten zu definieren. Macht am Anfang Mehrarbeit, spart sie aber schnell wieder ein.

Fortschrittsanzeige mit Mediator
Mediatoren sind eine elegante Möglichkeit Funktionalität zu kapseln und die einfache und sichere Anwendung zu erreichen. Auch wenn das folgende Beispiel unterkomplex ist, ist der Gewinn erkennbar. Hinter der Funktion könnte sich auch ein semi-modaler Dialog mit Fortschrittsanzeige verbergen.
Delphi-Quellcode:
type
  TWinHttpProgressGuiHelper = class(TCustomWinHttpProgressGuiHelper)
  private
    FProgressBar: TProgressBar;
    FProgressStepWidth: Integer;
    procedure InternalProgress(pmCurrentSize, pmContentLength: Cardinal);
  protected
    function DoUploadProgress(pmSender: TWinHttpApi; pmCurrentSize, pmContentLength: Cardinal): Boolean; override;
    procedure DoDownloadProgress(pmSender: TWinHttpApi; pmCurrentSize, pmContentLength: Cardinal); override;
  public
    constructor Create(pmProgressBar: TProgressBar; pmStepWidth: Integer = 5); reintroduce;
    procedure Prepare(pmRestClient: TRestHttpClientWinHttp; pmProgressMode: TWinHttpProgressMode); override;
    procedure Done; override;
  end;
  
function TWinHttpProgressGuiHelper.DoUploadProgress(pmSender: TWinHttpApi; pmCurrentSize, pmContentLength: Cardinal): Boolean;
begin
  Result := True;
  if pmCurrentSize = 0 then
    FProgressBar.Position := 0
  else if pmCurrentSize >= pmContentLength then
    Done
  else
    InternalProgress(pmCurrentSize, pmContentLength);
end;
Die Umsetzung der Anbindung ist ein einfacher Aufruf der Funktion Prepare:
Delphi-Quellcode:
FProgressHelper.Prepare(FRestClient, whpUpload);
try
  FRestClient.SaveImage(Image, edtImageName.Text);
finally
  FProgressHelper.Done;
end;
Um die Fortschrittsanzeige in Aktion zu sehen, sollte ein Bild von der Größe ab 10MB gewählt werden. Wer sich die Protokolldatei des Servers ansieht, stellt fest, dass die Ausführung einer Funktion nur einen Bruchteil einer Millisekunde dauert. Bei lokaler Ausführung des Servers gibt es kaum Latenz. Bei dieser Geschwindigkeit hat es jede Fortschrittsanzeige schwer. Ihr müsst der Anzeige schon eine reelle Chance geben.

Zusammenfassung
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, ich habe nicht übertrieben und 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. mORMot: ORM und DocVariant kurz vorgestellt
  2. mORMot: ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
Bis bald...
Thomas
Angehängte Dateien
Dateityp: zip TestMethodBasedServicesSource.zip (1,03 MB, 66x aufgerufen)
Dateityp: zip TestMethodBasedServicesExe.zip (3,86 MB, 43x aufgerufen)
  Mit Zitat antworten Zitat