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:
- mORMot: ORM und DocVariant kurz vorgestellt
- mORMot: ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
Bis bald...
Thomas