Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Netzwerke (https://www.delphipraxis.net/14-netzwerke/)
-   -   Chat mit PM (https://www.delphipraxis.net/154972-chat-mit-pm.html)

hans ditter 3. Okt 2010 22:17

Chat mit PM
 
Ich habe nun schon einige Zeit gesucht, viel Tuts gefunden, aber keine Antwort auf meine Frage.

Ich habe mir ein Chatprogramm geschrieben, welches auch schon funktioniert. Man kann eine Nachricht schreiben und sie wird an alle gesendet.
Doch da ist meine Frage: Wie schaffe ich es, nur an einen bestimmten Clienten zu senden?

Beispiel:
Im Chat angemeldet sind A,B,C.
A möchte B eine private Nachricht schicken, sodass C nichts davon mitbekommt.

Ich hatte mir gedacht möglicherweise im Server eine Liste mit allen aktiven, angemeldeten Clients zu erstellen. Der Client schickt dann statt nur einem Text ein Record, welches den Text, der gesendet werden soll, und den User, der die Nachricht empfangen soll, enthält.
Aber ob das eine elegante Lösung ist, wage ich mal zu bezweifeln.

Falls jemand ein Tutorial oder ein bereits vorhandes Thema kennt, das auf dieses Problem eingeht, dass bitte posten! Habe persönlich aber nichts passendes gefunden.

Großer Dank, hans ditter

p.s.: und gute Nacht... ;)

himitsu 3. Okt 2010 22:23

AW: Chat mit PM
 
Wenn man erlaubt, daß jeder die IPs der anderen Chatter erfahren darf, dann
> Client fragt Server nach IP des gewünschten User, verbindet sich mit diesem Clienten und sendet direkt die gewünschte Nachricht

ansonsten, wie du es schon erkannt hast, bleibt nur der Weg über den Server
> es wird eine Nachricht an den Server geschickt, mit der Bitte diese nur an den gewünschten User zu schicken


Wie man es nun genau machen kann, das hängt von deinem Chatprogramm+Serveranwendung ab.
Aber letztendlich sollte es dir doch kein Problem darstellen, da du ja deine Anwendungen kennst, dieses entsprechend zu implementieren.



Man bedenke aber auch die beiden gegenstätzlichen Sicherheitsaspekte,
- wenn nicht jeder jeden direkt "kennt" (der Weg den über Server)
- wenn der Server nicht mitlesen kann (der direkte Weg)

Sir Rufo 3. Okt 2010 23:47

AW: Chat mit PM
 
Normale Nachricht an alle
Code:
Hallo ihr da!
Nachricht an B
Code:
@B Hallo Du da!
Alle Nachrichten werden über den Server gesendet.
Fängt die Nachricht mit @ an, dann folgt darauf der Empfänger und dann die Nachricht.
Dieses wertet der Server aus und verschickt die Nachricht dann nur an B und nicht wie sonst an alle

Welches Zeichen du dafür nimmst, ob dort auch mehrere Empfänger stehen können, etc. Legst du fest und ist dann Bestandteil von deinem Chat-Protokoll

xZise 4. Okt 2010 08:17

AW: Chat mit PM
 
Oder mach es so ähnlich wie bei IRC. Du definierst bestimmte befehle wie „Channel Nachricht“ und „Private Nachricht“ und sagst wie man die eingeben kann. Zum Beispiel so:
Code:
/message Hallo Welt
/private foo Hallo foo
Erstes sendet es an alle, zweites sendet es nur an die Person. Wenn jemand jetzt in die Chatzeile nur „Hallo Welt“ eingibt, dann fügt der Client automatisch das „/message“ an.

Das hat den Vorteil, dass man selber auch eine Nachricht verschicken kann um den anderen zu erklären wie man eine private Nachricht schickt:
Code:
// Antwort auf die Frage: „Wie kann ich ‚foo‘ eine private Nachricht schreiben?“
/message /private foo Hallo foo
Übrigens ist das normalerweise der Weg der privaten Kommunikation: Zumindest ist es bei IRC, Jabber und ICQ nicht anders. Dort werden i.d.R. alle Nachrichten an den Server geschickt. Das hat auch den Vorteil, dass man keine großen Probleme durch Router o.ä. hat.

MfG
Fabian

pustekuchen 4. Okt 2010 12:18

AW: Chat mit PM
 
Ich habe vor kurzem auch einen Chat programmiert.
Dafür hab ich ein sehr gutes/verständliches Tutorial namens "Terminatorzeichen-Protokoll-Tutorial" von Narses gelesen. Wodrin unter anderem auch die Whisper Funktion erläutert wird.
Dort geht es auch noch weiter bis zur Dateiübertragung ;)

Zacherl 4. Okt 2010 14:08

AW: Chat mit PM
 
Ich hatte es in meinem Chat immer so, dass alle Nachrichten über den Server laufen. Der Server hatte logischerweise auch eine Liste mit den Namen aller verbundener Clienten. Anhand dieser konnte bei privaten Nachrichten die IP und das entsprechende Socket herausgefunden werden, an das die Nachricht gesendet werden soll.

Beim Anlegen der Liste ist es sinnvoll einen Pointer auf das entsprechende Socket direkt nach der Authentifizierung zusätzlich im List Struct zu speichern. Das vereinfacht den Zugriff auf die einzelnen Verbindungen enorm.

hans ditter 4. Okt 2010 20:30

AW: Chat mit PM
 
Zitat:

Zitat von pustekuchen (Beitrag 1053628)
Dafür hab ich ein sehr gutes/verständliches Tutorial namens "Terminatorzeichen-Protokoll-Tutorial" von Narses gelesen.

Könntest du mal einen Link dazu geben? Ich hab auch schon ein Tut mit diesem Namen gefunden.
Allerdings war diese bei www.delphi-library.de(um genau zu sein, hier http://www.delphi-library.de/topic_T...s_54269,0.html), und da konnte ich zwar sehen, dass die Person dieses Tut woh online gestellt hat, konnte aber die PDF, die das Tutorial offensichtlich beinhaltet, nicht herunterladen. Thx!

@all: Ok, ich denke, ich werde PMs über den Server laufen lassen. Da ich aber gerade erst mit Sockets angefangen habe, kann ich mir noch nicht wirklich vorstellen, wie die Zuordnung Nachricht -> Empfänger funktionieren soll.
Ich habe im Moment eine Listbox, die alle IPs der angemeldeten Clienten in chronologischer Reihenfolge (Anmeldereinfolge) beinhaltet.
Wenn ich jetzt z.B. die Idee von Sir Rufo aufgreifen, PMs über '@192.168.2.10' anzufangen, wie muss ich das Serverprogramm dann schreiben, dass der mir aus der Liste mit den IPs die richtige Connection sucht?

Zacherl 4. Okt 2010 21:43

AW: Chat mit PM
 
Das meinte ich mit dem Eintragen des dazugehörigen Sockets in die IP Liste. Sagen wir mal ein Client verbindet sich. Dann trägst du diesen mit
Delphi-Quellcode:
ListBox1.Items.Add(IP);
in die Liste ein.

Besser wäre es später statt einer ListBox z.b. eine TList mit einem Record zu verwenden, der sowohl IP, als auch den Socket Pointer speichern kann. Als Workaround könntest du dir allerdings parallel zur ersten ListBox eine Zweite anlegen, die beim Verbinden mittels
Delphi-Quellcode:
ListBox2.Items.Add(IntToStr(Cardinal(Socket)));
das Socket speichert.

Später suchst du dann in deiner ListBox1 nach der IP und findest diese z.B. an Index 2. Jetzt benutzt du
Delphi-Quellcode:
TCustomWinSocket(StrToInt(ListBox2.Items[2])).SendText(NACHRICHT);
zum Verschicken der privaten Nachricht an den richtigen Clienten.

Die Variante mit den zwei ListBoxen ist alles andere als schön und wie gesagt eher ein Workaround. Würde dir allerdings dringend raten das ganze auf TList unzuschreiben.

hans ditter 5. Okt 2010 20:35

AW: Chat mit PM
 
@Zacherl
Ok, ich kann dir soweit folgen, dass ich vom Client ein Record mit IP und Nachricht schicken sollte. Dass diese dann mit einer TList verglichen wird.
Aber mir ist nicht klar, wie ich ein Record sende. Wie gesagt, ich bin noch ganz frisch in der Materie.

Freue mich auf weitere Antworten,
hans ditter

hans ditter 5. Okt 2010 21:03

AW: Chat mit PM
 
hm... ok, ich hab grad mal versucht mich in die Materie "Pointer" einzulesen... hab ich ehrlich gesagt auch noch nie benutzt... :oops::oops:
Könntest du, Zacherl, vlt mal einen Quelltext zu deinem Beitrag posten? Muss nichts besonderes sein, nur das ich das Prinzip mal sehen kann...
Das wär cool!

hans ditter

xZise 6. Okt 2010 07:27

AW: Chat mit PM
 
Zitat:

Zitat von hans ditter (Beitrag 1053984)
@Zacherl
Ok, ich kann dir soweit folgen, dass ich vom Client ein Record mit IP und Nachricht schicken sollte. Dass diese dann mit einer TList verglichen wird.
Aber mir ist nicht klar, wie ich ein Record sende. Wie gesagt, ich bin noch ganz frisch in der Materie.

Freue mich auf weitere Antworten,
hans ditter

Einen Rekord wirst du wohp schwer selbst versenden können. Stattdessen überlegst du dir ein Chat Protokoll.

Dabei gibt es verschiedene Verfahren. jabber beziehungsweise XMPP nutzen xml um Empfänger Text und alles andere zu definieren.

Ein Vorschlag der Recht einfach zu implementieren ist, ist das du den Empfänger direkt schreibst. Dann machst du ein Zeichen welches eindeutig zum Trennen geeignet ist. Zum Beispiel einen Umbruch. Dann schreib du den Text. Fertig. Vom Server dann kommt das so ähnlich, nur das ganz am Anfang der Absender geschrieben wird. Dadurch kannst sogenanntest spoofing verhindern. Also das sind jemand als jemand anderes ausgibt.

Aber da kannst du dir was selber überlegen.

Und zu deinen Pointe: Irgendwo musst du doch deine Sockets speichern. Und diese verknüpfst du mit einen Namen und einer IP dazu empfehle ich, wenn man noch nicht Su gut mit Pointern umgehen kann, dass du einfach nehmen Klasse erstellst und darin die IP, Namen und zugehörige Verbindung speichert. Diese speicherst du dann in einer TObjectList.

MfG
Fabian

hans ditter 6. Okt 2010 17:13

AW: Chat mit PM
 
erstmal vielen Dank für deine Antwort xZise. Ich würde aber gerne das gleich "korrekt" lösen. Wenn ich Zacherl richtig verstanden hab, dann wäre dein Vorschlag auch keine "saubere" Lösung...
Wenn man Pointer etc. aber nicht so einfach erklären kann, dann probier ich erstmal deine Variante aus.

Eine Frage hätte ich aber noch. Dieses eindeutige Zeichen wird in ein Terminator-Zeichen-Protokoll verwendet, ist das korrekt? Und wie kann ich mir so ein Protokoll vorstellen? Ist das eine Unit mit verschiedenen Befehlen, die überprüft werden können?

hans ditter

hans ditter 6. Okt 2010 18:07

AW: Chat mit PM
 
Ok, ich hab mich grad nochmal mit meinem diiiicken Delphibuch auseinandergesetzt und glaube verstanden zu haben, wie ich das mit den Pointer und der TList hinbekomme.
Aber mir ist gleich das nächste Problem unter die Finger gekommen: Wo bekomme ich den Socket Pointer her?

Hoffe auf schnelle Antwort!!
hans ditter

xZise 6. Okt 2010 18:09

AW: Chat mit PM
 
Moin,
Also meine Lösung ist nur eine einfachere als die von Zacherl. Weil du immer weißt womit du hantierst und nicht mit Pointern arbeitest.

Vielleicht habe ich das übersehen, aber womit überträgst du die Daten? Zum Beispiel wenn du IdTCP (oder IdUDP) nutzt dann hätte ich es so gespeichert:
Delphi-Quellcode:
type
  TClientConnection = class
  private
    FSocket : TIdTCP;
    FNickname : string;

    function GetIP : string; // Hier halt die Ip mithilfe des Sockets zurückgeben oder irgendwie anders :)
  public
    property Socket : TIdTCP read FSocket;
    property Nickname : string read FNickname write FNickname;
    property IP : string read GetIP;
  end;

  TClientList = class
  private
    FClients : TObjectList; // Im Konstruktor erstellen & im Destruktor freigeben.
  public
    procedure ClientConnect(Connection : TClientConnection);
    procedure ClientDisconnect(Connection : TClientConnection);
  end;
Und wie du dein Protokoll schreibst, musst du wie gesagt immer selber wissen. Meine Möglichkeit hat den Vorteil, dass du sie mit einer TStringList lesen/interpretieren kannst. So könnte zum Beispiel eine private Nachricht aussehen:
Code:
PRIVATE
hans ditter
Hallo hans ditter! Dies ist eine private Nachricht
PRIVATE als Kennzeichner, dass es eine PM ist, dann der Nickname und dann der Text. Die Nachricht bekommst du dann und musst dann nur noch folgendes machen (Pseudoquelltext):
Code:
Wenn Typ = PM dann:
  Suche in der Clientliste nach den Nickname
  Ist der Nickname vorhanden
    Schicke die PM an den Client (Empfänger)
  ansonsten
    Schicke eine Fehlermeldung an den Sender
Die könnte dann so aussehen:
Code:
PRIVATE-RECV
xZise
Hallo hans ditter! Dies ist eine private Nachricht
Und die Fehlermeldung z.B.:
Code:
PRIVATE-ERR
Benutzername "hans ditter" nicht im Netzwerk vorhanden!
Bei diesem System hat man den Nachteil, dass man viel Platz verschwendet, durch die Zeilenumbrüche und den sprechenden Befehlen (PRIVATE, PRIVATE-RECV und PRIVATE-ERR). Aber dafür kann jeder Administrator sehen, wenn was schief geht was schief geht (z.B. hat jemand PRIVAT statt PRIVATE stehen oder so ;) ).
Ein anderes Format wäre im IRC Stil:
Code:
msg <Reciever> <Content>
Hier wäre der Trenner das Leerzeichen, aber wieder sprechende Befehle, und Nicknames mit Leerzeichen sind nicht möglich.

Ansonsten kannst du dir auch XMPP anschauen. Die nutzen XML, was natürlich noch mehr Platz verschwendet. Aber dadurch ist es sehr leicht zu sehen, was verschickt wird, und das Protokoll kann einfach erweitert werden.

[redbox]Irgendwo speicherst du doch, die verschiedenen Verbindungen oder?[/redbox]

MfG
Fabian

hans ditter 6. Okt 2010 18:24

AW: Chat mit PM
 
tja, also bis jetzt hab ich es nur geschafft, an alle eine Nachricht zu schicken.
Wenn sich ein Client mit dem Server verbindet, dann schreib ich in eine ListBox die IP des Clienten und ausgehen von der IP schreibe ich den Username in eine 2. ListBox.
Aber das Socket oder so speicher ich noch nicht.
Ich hab jetzt mal ein bisschen gebastelt, mit Pointern.
Delphi-Quellcode:
type
  PClientData = ^TClientData;
  TClientData = record
    UserNick: string;
    IP: string;
    SocketPointer: pointer;
  end;

...

var
  Form4: TForm4;
  UserList: TList;
  UserData: PClientData; //Pointer auf TClientData (Record)

implementation

...

New(UserData);//neuer Pointer auf TClientData
    UserData^.IP:=Socket.RemoteAddress;
    UserData^.SocketPointer:=Cardinal(Socket);
    UserData^.UserNick:=UserNick.Items.Strings[UserNick.Items.Count];
Das ist das, was ich bisher habe. Allerdings gibt mir Delphi natürlich beim SocketPointer eine Fehlermeldung, da wäre nochmal gut zu wissen, wohin der Pointer eig zeigen soll (also, wie bringe ich da eine Pointer auf das entsprechende Socket unter?).

Zu dem Protokoll muss ich sagen, dass ich gerade total auf der Leitung stehe... :oops::pale:

[Edit]Was du damit meinst, wie ich die Daten übertrage, ist mir auch nicht ganz klar. Ich denke mal mit TCP/IP und einer ganz normalen TClientSocket bzw. TServerSocket... war es das was du meintest?[/Edit]

xZise 6. Okt 2010 18:39

AW: Chat mit PM
 
Naja irgendetwas stellt die Verbindung her. Es gibt z.B. von Indy IdTCP bzw. IdUDP, je nachdem ob du TCP oder UDP verwendest. Also die Kernkomponente wo du sagst: Schicke an die IP X.Y.Z.A den Text B.

Das was wir dir vorschlagen ist, dass du ständig einen TServerSocket oder TClientSocket hast, der einfach an die bei ihm gespeicherte IP einen bestimmten Text schickt.

Außerdem ist ein Pointer kein Cardinal. Okay beide sind 32 bit breit (noch), aber mit 64 bit muss Cardinal nicht auch auf 64 sich verbreitern. Von welchen Typ ist denn Socket (also wo du in der Zeile den auf Cardinal castest). Den gleichen Typ könntest du statt Pointer nehmen.

MfG
Fabian

hans ditter 6. Okt 2010 18:51

AW: Chat mit PM
 
Also meine zentrale Komponente ist TClientSocket / TServerSocket, die Standardkomponente von Delphi (glaub ich zumindest).

Hm, wenn ich das richtig verstanden hab mit dem Cardinal(Socket): Socket ist die Variable bei ServerSocketClientConnect(Socket: TCustomWinSocket);

Oh man, ich trau gar nicht, dass zu sagen, aber ich hab's immer noch nicht verstanden mit dem Protokoll. So wie ich mir vorstelle, läuft das so, dass User A /msg to B {content} schreibt und das an den Server schickt.
Der Server empfängt etwas, überprüft, ob am Anfang etwas steht (hier ja: /msg to B) versteht: "aha, /msg to bedeutet, dass soll eine private Nachricht sein", überprüft dann den angegebenen User, ob der angemeldet ist und schickt die Nachricht an ihn.
Ist meine Vorstellung davon überhaupt korrekt?? Ich fürchte fast nein... :(:duck:

Sir Rufo 6. Okt 2010 19:54

AW: Chat mit PM
 
Auch wenn es auf den ersten Blick komplizierter wird, solltest du die Nachrichten als JSON verschicken.

Senden vom Client geht dann z.B. so
Delphi-Quellcode:
uses
  superobject;

{...}

var
  o : ISuperObject;
begin
  o := SO; // Struktur vorbereiten
  o.S[ 'CMD' ] := 'MSG'; // Wir wollen eine Nachricht senden
  o.S[ 'MSG' ] := 'Ich grüße euch alle'; // Die Nachricht
  // Soll es nur einem Empfänger gesendet werden, dann diese Feld füllen, sonst leer lassen
  o.S[ 'TO' ] := 'Peter'; // Der Empfänger
  ClientSocket.SendText( o.AsJSON );
end;
Der Server empfängt das dann wie folgt
Delphi-Quellcode:
uses
  superobject;

{...}

procedure ClientRead( Sender : TObject; Socket : TCustomWinSocket );
var
  r : string;
  o : ISuperObject;
  idx : integer;
begin
  // Lesen vom Client
  r := Socket.ReceiveText;
  // und daraus wieder ein JSON erzeugen
  o := SO( r );
  o.S[ 'FROM' ] := ''; // Der Server könnte jetzt noch den Benutzer einfügen
  case IndexText( o.S[ 'CMD' ], [ 'MSG', 'LOGIN' ] ) of
    0 : // MSG
      begin
        o.I[ 'COUNT' ] := 0;
        // Senden
        for idx := 0 to TServerWinSocket( Sender ).ActiveConnections - 1 do
          // an alle?
          if ( ( o.[ 'TO' ] = '' ) or
            // an diesen Benutzer? 
            ( o.S[ 'TO' ] = <Deine Userabfrage> ) ) and
            // nicht an den Client der gerade sendet
            ( Socket <> TServerWinSocket( Sender ).Connections[ idx ] ) then
            begin
              TServerWinSocket( Sender ).Connections[ idx ].SendText( o.AsJSON );
              o.I[ 'COUNT' ] := o.I[ 'COUNT' ] + 1;
            end;
         // Und wieder zurück an den Client
         Socket.SendText( o.AsJSON );
      end;
    1 : // LOGIN
      begin
        // hier werden die Login-Daten überprüft
        o.B[ 'SUCCESS' ] := PruefeLogin( o.S[ 'USER' ], o.S[ 'PASS' ] );
        // Eintrag im Usermanager
        {...}
        // Rückmeldung an den Client
        Socket.SendText( o.AsJSON );
      end;
  end;
end;
btw: Um mit TServerSocket und TClientSocket auch Unicode (UTF8) zu versenden kann man einfach diese Unit mit einbinden. (D2010 tested)
Delphi-Quellcode:
unit insCustomWinSockEncoder;

interface

uses
  ScktComp;

type
  // Erlaubt dem Socket Unicode zu senden
  TCustomWinSocketEncoder = class helper for TCustomWinSocket
    function ReceiveText : string;
    function SendText( const S : string ) : integer;
  end;

implementation

uses
  SysUtils;

{ TCustomWinSocketEncoder }

function TCustomWinSocketEncoder.ReceiveText : string;
  var
    Encoding : TEncoding;
    Buffer : TBytes;
  begin
    SetLength( Buffer, ReceiveBuf( Pointer( nil )^, -1 ) );
    SetLength( Buffer, ReceiveBuf( Pointer( Buffer )^, Length( Buffer ) ) );
    Encoding := TEncoding.UTF8;
    Result := Encoding.GetString( Buffer );
    SetLength( Buffer, 0 );
  end;

function TCustomWinSocketEncoder.SendText( const S : string ) : integer;
  var
    Encoding : TEncoding;
    Buffer : TBytes;
  begin
    Encoding := TEncoding.UTF8;
    Buffer := Encoding.GetBytes( S );
    Result := SendBuf( Pointer( Buffer )^, Length( Buffer ) );
    SetLength( Buffer, 0 );
  end;

end.

xZise 6. Okt 2010 21:34

AW: Chat mit PM
 
Moin,
Zitat:

Zitat von hans ditter (Beitrag 1054156)
[...]Hm, wenn ich das richtig verstanden hab mit dem Cardinal(Socket): Socket ist die Variable bei ServerSocketClientConnect(Socket: TCustomWinSocket);[...]

Dann schreib doch statt "Pointer" einfach "TCustomWinSocket".

Zitat:

Zitat von hans ditter (Beitrag 1054156)
[...]Oh man, ich trau gar nicht, dass zu sagen, aber ich hab's immer noch nicht verstanden mit dem Protokoll. So wie ich mir vorstelle, läuft das so, dass User A /msg to B {content} schreibt und das an den Server schickt.
Der Server empfängt etwas, überprüft, ob am Anfang etwas steht (hier ja: /msg to B) versteht: "aha, /msg to bedeutet, dass soll eine private Nachricht sein", überprüft dann den angegebenen User, ob der angemeldet ist und schickt die Nachricht an ihn.
Ist meine Vorstellung davon überhaupt korrekt?? Ich fürchte fast nein... :(:duck:

Genau so! Du überlegst halt einfach, was du übertragen musst: Zum Beispiel "Schicke an alle" und "Schicke an benutzer". Das heißt du musst übertragen, welcher Typ das ist, damit der Server und die anderen Clients wissen, was für eine Nachricht war das. Dann musst du einen Empfänger definieren, wenn es eine private Nachricht ist. Und zum Schluss musst du dann noch den Inhalt dazu schreiben.

Der Server liest nun die Nachricht und entschlüsselt den Typ um heraus zu finden, was er damit machen soll und handelt dann entsprechend. Zum Beispiel wenn man eine private Nachricht verschicken will, das habe ich ja schon beschrieben.

So könntest du zum Beispiel einen Befehl definieren, um den Nickname eines Benutzers zu ändern und weitere Befehle definieren.

MfG
Fabian

hans ditter 7. Okt 2010 17:32

AW: Chat mit PM
 
Hey, dann war das ja doch gar nicht so falsch!! :D *FREU* Ich hatte das so schonmal versucht. Ich wollte /msg B Hallo B schicken. Das würde ja aber bedeuten, dass ich irgendwie erstmal das /msg rauskopieren und vergleichen und dann den User rauskopieren und vergleichen muss.
Geht das nicht einfacher?
Also vlt nach dem Motte: Wenn der Server einen String empfängt, der mit / anfängt, dass er dann weiß: "Ok, ich muss jetzt mal schauen welcher Befehlt da steht". Wenn er dann sieht, der Befehl ist msg, dass er dann automatisch nach dem User schaut. (Vielleicht könnte man das Ende des Befehls ja mit einem weiteren / markieren?)

Zu Pointer etc:
Ich hab von pointer schon auf TCustomWinSocket umgestellt. Wollte dann mit
Delphi-Quellcode:
UserData^.SocketPointer:=Socket;
dauf Socket einen Pointer speichern, wenn ich den Inhalt des Pointers dann aber ausgeben wollte, kam nichts raus...

xZise 7. Okt 2010 17:55

AW: Chat mit PM
 
Deine "Vereinfachung" ist doch keine Vereinfachung. Wenn du sagst das die Befehle kein Leerzeichen haben, dann machst du einfach:
Delphi-Quellcode:
var
  command : string;
begin
  command := Copy(input, 1, Pos(' ', input) - 1);
Dann hast du schon mal den Befehl und den Nick bekommst du ähnlich einfach:
Delphi-Quellcode:
var
  command : string;
  reciever : string;
  message : string

  delimToCommand : Integer;
  delimToReciever : Integer;
begin
  delimToCommand := Pos(' ', input); // Dort ist das erste Leerzeichen
  delimToReciever := PosEx(' ', input, delimToCommand + 1); // Da das zweite
  command := Copy(input, 1, delimToCommand - 1);
  reciever := Copy(input, delimToCommand + 1, delimToReciever - delimToCommand - 1);
  message := Copy(input, delimToReciever, Length(input) - delimToReciever);
Man könnte auch Delphi-Referenz durchsuchenExplode verwenden, aber das könnte zu übereifrig werden, wenn weitere Leerzeichen kommen.

Übrigens habe ich deshalb auch vorgeschlagen mehrere Zeilen zu nehmen. Dann hättest du einfach machen können:
Delphi-Quellcode:
var
  msg : TStrings;

  command : string;
  reciever : string;
  message : string
begin
  msg := TStringList.Create;
  try
    msg.Text := input;
    command := msg[0];
    msg.Delete(0);
    reciever := msg[0];
    msg.Delete(0);
    message := msg.Text;
  finally
    msg.Free;
  end;
Vielleicht geht das auch schöner ohne die Zeilen zu löschen.

MfG
Fabian

Sir Rufo 7. Okt 2010 17:56

AW: Chat mit PM
 
Wenn du mal erklären könntest, warum du die Sockets in einer eigenen Liste speichern möchtest.

Denn mir erschließt sich das nicht.

Der ServerSocket hat alle benutzten Sockets in einer Liste vorrätig. Da ist eine eigene Liste irgendwie doppelt gemoppelt.

Sir Rufo 7. Okt 2010 18:12

AW: Chat mit PM
 
Zitat:

Zitat von xZise (Beitrag 1054370)
Übrigens habe ich deshalb auch vorgeschlagen mehrere Zeilen zu nehmen. Dann hättest du einfach machen können:
Delphi-Quellcode:
var
  msg : TStrings;

  command : string;
  reciever : string;
  message : string
begin
  msg := TStringList.Create;
  try
    msg.Text := input;
    command := msg[0];
    msg.Delete(0);
    reciever := msg[0];
    msg.Delete(0);
    message := msg.Text;
  finally
    msg.Free;
  end;
Vielleicht geht das auch schöner ohne die Zeilen zu löschen.

MfG
Fabian

Wieso macht ihr euch das Leben immer so schwer?
Das ist z.B. die Nachricht
Code:
CMD=MSG
SEN=Walter
REC=Peter
MSG=Alles frisch im Schritt
Diese wird wie folgt erstellt:
Delphi-Quellcode:
function BuildMessage( const Sen, Rec, Msg : string ) : string;
begin
  with TStringList.Create do
    try
      Values[ 'CMD' ] := 'MSG';
      Values[ 'SEN' ] := Sen;
      Values[ 'REC' ] := Rec;
      Values[ 'MSG' ] := Msg;
      Result := Text;
    finally
      Free;
    end;
end;
Auslesen des CMDs geht so
Delphi-Quellcode:
function ReadMessageCmd( const RecvTxt : string ) : string;
begin
  with TStringList.Create do
    try
      Text := RecvTxt;
      Result := Values[ 'CMD' ];
    finally
      Free;
    end;
end;
Und auslesen der Nachricht geht dann so
Delphi-Quellcode:
procedure ReadMessage( const RecvTxt : string; var Sen, Rec, Msg : string );
begin
  with TStringList.Create do
    try
      Text := RecvTxt;
      Sen := Values[ 'SEN' ];
      Rec := Values[ 'REC' ];
      Msg := Values[ 'MSG' ];
    finally
      Free;
    end;
end;
Und nochmal bemerkt, mit JSON geht das ganze super stressfrei - aber kompliziert ist wahrscheinlich schöner :mrgreen:

hans ditter 8. Okt 2010 12:23

AW: Chat mit PM
 
Hi Sir Rufo,
ich kann ja verstehen, dass du das alles kompliziert findest... aber wie ich schonmal sagte, ich bin noch ein totaler newbie auf dem Themengebiet.
Die Liste sollte den UserNick, die IP und einen POINTER auf das entsprechende Socket enthalten, damit man das richtige Socket über den UserNick oder die IP finden kann.
Zu dem Protokoll-Ding: Ich habe sowas noch nie gemacht. Wenn ich mal Diskussionsbeiträge gelesen hab, dann hörte sich das für mich immer so an, als ob man eine Datei schreibt, die dann so allgemeine Befehle enthält, sowas wie
Code:
/msg [RecUser] [sendUser] [message]
oder so ähnlich. Ich hab inzwischen verstanden, dass das nicht so ist, sondern das man spezielle Zeichen einsetzt, um zu erkenne, wo der Befehl anfängt und wo der Text (z.B. ja mit Zeilenumbrüchen).
Zu deiner Idee mit den JSON: Ich find es super von dir, dass du dein Wissen einbringst und mir damit helfen möchtest, aber ich steige da noch nicht ganz durch und konnte bei Google auch keine Tutrials oder Anleitungen finden (vielleicht fehlte mir auch nur das richtige Stichwort).
Wenn das wirklich soviel besser ist, dann würde ich mich über eine nähere Erläuterung oder ein Tut o.ä. freuen.
Um dir meine Verständnisprobleme näher zu bringen (vielleicht sind die ja auch etwas dämlicher Natur ;) :) ):
- was machst du mit
Delphi-Quellcode:
o:=SO;//Struktur vorbereiten
?
- wenn du erst die Struktur mit "SO" vorbereitet hast, wieso verwendest du dann für die Nachricht an sich nur noch
Delphi-Quellcode:
o.S[ 'CMD' ] := 'MSG';//etc.
??
- . . .
Vielleicht könntest du das ganze nochmal näher erläutern, denn ich würd das gerne einsetzten, nur möchte ich verstehen, was ich mache und nicht nur abschreiben, was andere mir vorsetzten.

So, hoffe meine Problemlage ist jetzt ganz deutlich geworden. Würde mich freuen wenn ihr mir (vgl.bar mit einem störrischen / dämlichen Kunden...:D :P) weiter helfen würdet. Hab hier in der DP schon einiges dazugelernt.

LG, hans ditter

Sir Rufo 8. Okt 2010 14:20

AW: Chat mit PM
 
Den Link wo man JSON für Delphi bekommt habe ich hier schon gepostet (s. in den oberen Threads)
Delphi-Quellcode:
o := SO;
erzeugt mir ein leeres JSON-Object, mit dem ich ab jetzt arbeiten kann.
Jetzt möchte ich diesem JSON-Object für die Eigenschaft 'CMD' den String-Wert 'MSG' übergeben.
Das schöne bei JSON-Objekte ist, die Eigenschaft wird automatisch angelegt, wenn diese noch nicht existiert.
also
Delphi-Quellcode:
o.S[ 'CMD' ] := 'MSG';
Wenn ich einen Integer-Wert übergeben möchte schreibe ich einfach
Delphi-Quellcode:
o.I[ 'Wert' ] := 10;
Auslesen geht genauso einfach
Delphi-Quellcode:
if o.S[ 'CMD' ] = 'MSG' then
Unter dem Link findest du auch ein Forum sowie eine kurze Anleitung.

Stell dir das JSON erstmal so vor: Da kann ich sehr einfach was reinschmeissen und wieder auslesen und das gesamte bekomme ich als Text
Delphi-Quellcode:
ShowMessage( o.AsJSON );
geliefert und kann das sonstwohin schieben (z.B. als Nachricht über den Socket).

Das JSON-Objekt ist dabei nur Hilfsmittel, weil du dich um das korrekte Verpacken und Trennen der einzelnen Teile nicht selber kümmern musst. Du brauchst es nur benutzen.

hans ditter 8. Okt 2010 20:56

AW: Chat mit PM
 
Hey, super das du doch geantwortet hast... ich hatte schon befürchtet, ich hab jetzt alle abgeschreckt... :D
Das hört sich doch super an... damit sind meine größten Probleme schonmal geklärt...;) DANKE!!
Aber ich habe da noch immer ein Problem bei der Serverabfrage (wobie diese eher auf dem Verständnis des Vorgangs beruht):
Delphi-Quellcode:
o.I[ 'COUNT' ] := 0;
        // Senden
        for idx := 0 to TServerWinSocket( Sender ).ActiveConnections - 1 do
          // an alle?
          if ( ( o.[ 'TO' ] = '' ) or
            // an diesen Benutzer?
            ( o.S[ 'TO' ] = <Deine Userabfrage> ) ) and
            // nicht an den Client der gerade sendet
            ( Socket <> TServerWinSocket( Sender ).Connections[ idx ] ) then
            begin
              TServerWinSocket( Sender ).Connections[ idx ].SendText( o.AsJSON );
              o.I[ 'COUNT' ] := o.I[ 'COUNT' ] + 1;
            end;
         // Und wieder zurück an den Client
         Socket.SendText( o.AsJSON );
      end;
Was passiert in diesem Abschnitt?
1.) Ich kann dem ganzen soweit folgen, dass ein Integerwert 'Count' angelegt wird und mit null gefüllt ist. Aber wofür brauchst du diesen Integerwert??
2.) Warum fragst du ab, ob an alle oder nur an eine Person und nicht an den sendenden Clienten geschickt werden soll? Denn nach dem then schickst du die Nachricht doch sowieso wieder an alle (mit der for-Schleife).
Der Rest ist mir im Moment noch ziemlich klar... mal schauen, vlt ergeben sich da ja auch noch Probleme.

Dann hätte ich zu dem JSON-Quellcode noch eine Frage. Ich hab leider nur Turbo Delphi... :( ist das richtig, dass dies nichtmal eine Komponente ist, die man installieren kann? Sondern die man nur im uses-Bereich einfügen muss? Ich bin davon mal ausgegangen, aber in welchen Ordner muss ich dann die superobject.pas Datei speichern? Ich dachte mir unter C:/Programme/Borland/BDS/source ... aber wo dann hin? Ich hab da die Ordner dUnit, Indy10, IntraWeb, ToolsAPI und Win32 anzubieten... :pale:

Ich freue mich schon auf deine nächste Antwort,
hans ditter

p.s.: Natürlich auch an alle anderen ein großes Dank, die sich hier beteiligt haben, um mir zu helfen. Vor allem natürlich xZise.

Sir Rufo 8. Okt 2010 22:12

AW: Chat mit PM
 
Ok, das mit dem COUNT ist eigentlich nur Spielerei ... damit wollte ich dem sendenden Client zurückmelden, an wieviele Clients die Nachricht geschickt wurde. Ist also erstmal überflüssig
Delphi-Quellcode:
{...}
        // Senden

        // Wir werden jetzt alle Verbindungen prüfen, ob wir denen was senden müssen
        // dazu finden wir in TServerWinSocket( Sender ).ActiveConnections die Anzahl der aktiven Verbindungen

        for idx := 0 to TServerWinSocket( Sender ).ActiveConnections - 1 do
          begin // Das begin und end ist nicht notwendig, dient nur der Übersichtlichkeit

            // ist TO leer, dann wird wird die Nachricht an alle geschickt

            if ( ( o.[ 'TO' ] = '' ) or

              // oder steht da was drin, dann geht es nur an diesen Benutzer
              // die Userabfrage muss jetzt den Benutzer zu dem Socket aus
              // TServerWinSocket( Sender ).Connections[ idx ]
              // liefern also irgendwie sowas
              // function GetUserNameFromSocket( Socket : TCustomWinSocket ) : string

              ( o.S[ 'TO' ] = GetUserFromSocket( TServerWinSocket( Sender ).Connections[ idx ] ) ) ) and

              // aber es soll nicht an die Verbindung geschickt werden, die die Nachricht abgeschickt hat

              ( Socket <> TServerWinSocket( Sender ).Connections[ idx ] ) then
              begin

                TServerWinSocket( Sender ).Connections[ idx ].SendText( o.AsJSON );

              end;

          end; { --- hier endet die for-Schleife --- }

        // Wir schicken dem Absender die Nachricht wieder zurück, damit der weiß, dass wir alles erledigt haben

        // Damit der Client weiß, dass es sich um eine Rückantwort handelt schreiben wir noch etwas in die Nachricht

        o.S[ 'SEND' ] := 'OK';

        // und auf die Reise schicken

        Socket.SendText( o.AsJSON );
     end;
Ja, es wird einfach nur die unit superobject eingebunden. Da braucht (gottseidank) nichts installiert werden.

Wenn du diese Unit nur in deinem Projekt nutzen möchtest, dann kann st du die datei "superobject.pas" einfach in dein Projekt-Ordner kopieren.
Willst du immer wieder auf diese Unit zurückgreifen, dann sollte die Datei in einem Ordner stehen, der laut Bibliothekspfad auch eingebunden wird. Schau einfach in deiner IDE unter "Tools->Optionen->Bibliothek - Win32->Bibliothekspfad->[...]
Mit einem Klick auf den Button mit den drei Punkten (nein, der ist nicht gelb :mrgreen: ) kannst du auch weitere Pfade hinzufügen.

Bei mir sind die unter "%PUBLIC%\Dokumente\superobject" gespeichert und dann habe ich den Pfad zur Bibliothek hinzugefügt.

hans ditter 8. Okt 2010 22:54

AW: Chat mit PM
 
Ok, cool!! Ich hab das aber mit dem User herausfinden noch nicht ganz gerafft.
Mein Delphi kennt beim Server die Funktion
Delphi-Quellcode:
GetUserFromSocket
nicht. Was mache ich falsch bzw. wie komme ich dann an die Verbindung ran?
Auch finde ich im Moment "TServerWinSocket" nicht ... ich habe die ganze Weiterleitung etc. in OnClientRead (ist das korrekt?), da gibt es aber nur
Delphi-Quellcode:
...ClientRead(Sender: TObjet; Socket: T[B]Custom[/B]WinSocket);
.
Und:
Delphi-Quellcode:
 ( Socket <> TServerWinSocket( Sender ).Connections[ idx ] ) then
--> ist Socket nicht gleich TServerWinSocket??

Ich bin noch etwas auf Kriegsfuß mit der internen Verbindungsliste des TServerSocket... hast du vlt schon bemerkt...:oops::roll: Vlt kannst du mir da nochmal etwas auf die Sprünge helfen... DANKE!!

LG, hans ditter

xZise 8. Okt 2010 23:19

AW: Chat mit PM
 
Zitat:

Zitat von hans ditter (Beitrag 1054629)
Ok, cool!! Ich hab das aber mit dem User herausfinden noch nicht ganz gerafft.
Mein Delphi kennt beim Server die Funktion
Delphi-Quellcode:
GetUserFromSocket
nicht.[...]

Die Funktion musst du dir auch noch schreiben. Damit findest du den Nickname zu einer Verbindung heraus. Das heißt irgendwo musst du speichern, wie der Nickname einer bestimmten Verbindung lautet.
Am besten ginge das wohl mit einer DictionaryDictionary. Zum Beispiel einer HashlistHashlist.

MfG
Fabian

hans ditter 8. Okt 2010 23:23

AW: Chat mit PM
 
Zitat:

Zitat von xZise (Beitrag 1054632)
Die Funktion musst du dir auch noch schreiben. Damit findest du den Nickname zu einer Verbindung heraus. Das heißt irgendwo musst du speichern, wie der Nickname einer bestimmten Verbindung lautet.
Am besten ginge das wohl mit einer DictionaryDictionary. Zum Beispiel einer HashlistHashlist.

Äääääääähm. . . ???? Was?? Ist das sowas, was ich schonmal machen wollte? Mit einer Liste, die IP, UserNick und Pointer auf das Socket speichert? Oder ist das noch wieder was anderes? Wenn ja, könntest du dann mal ein Beispielcode posten?
Danke!

lg, hans ditter

xZise 8. Okt 2010 23:56

AW: Chat mit PM
 
Das ist im Grunde genommen diese Liste. Ich sagte auch "am besten ginge es ...", aber da müsste ich noch mehr erklären und man müsste sich das erst downloaden/zusammensuchen.

Ich würde dir empfehlen: Du speicherst in einer Liste zu welcher Connection, welcher Benutzername gehört. Das heißt erstmal findest du heraus, welcher Datentyp .Connections[ idx ] ist. Und dann machst du das entweder mit Records oder mit Klassen und speicherst zu jeder Verbindung, welcher Benutzername dazu gehört.

Das speicherst du in die Liste und die dir unbekannte Methode geht dann einfach die Liste durch und sucht den Benutzername.
So ungefähr könnte man das machen:
Delphi-Quellcode:
type
  PElement = ^TElement;
  TElement = record
    Nick : string;
    Connection : TConnection; // Den Datentyp von oben! Ich weiß es gerade nicht, also einfach mal gucken!
  end;

  TConnections = class(TObject)
  private
    FConnections : TList; // Im Konstruktor erstellen/Destruktor freigeben
  public
    procedure AddConnection(Nick : string; Connection : TConnection);
    procedure RemoveConnection(Nick : string);
    function GetUserFromSocket(Connection : TConnection) : string; // Das ist einfach durchgehen und testen
  end;


procedure TConnections.AddConnection(Nick : string; Connection : TConnection);
var
  e : PElement;
begin
  New(e);
  e.Nick := Nick;
  e.Connection := Connection;
  FConnections.Add(e);
end;

procedure TConnections.RemoveConnection(Nick : string);
var
  e : PElement;
begin
  // Nick suchen
  for i := 0 to FConnections.Count - 1 do
  begin
    e := PElement(FConnections[i]);
    if (e.Nick = Nick) then
    begin
      FConnections.Delete(i);
      Dispose(e);
      Exit;
    end;
  end;
end;
Alle Angaben ohne Gewähr!

MfG
Fabian

PS: Die Dictionary wäre halt schneller, aber so gehts auch erstmal.

Sir Rufo 9. Okt 2010 01:04

AW: Chat mit PM
 
Zitat:

Zitat von hans ditter (Beitrag 1054629)
Ok, cool!! Ich hab das aber mit dem User herausfinden noch nicht ganz gerafft.
Mein Delphi kennt beim Server die Funktion
Delphi-Quellcode:
GetUserFromSocket
nicht. Was mache ich falsch bzw. wie komme ich dann an die Verbindung ran?

Wie xZise schreibt, die musst du selber erstellen ... ich bin aber (nach dem was du hier geschrieben hast) davon ausgegangen, dass du schon so eine Liste führst (User,Socket). Die Funktion sucht dann aus der Liste den Socket und liefert den User.

Zitat:

Zitat von hans ditter (Beitrag 1054629)
Auch finde ich im Moment "TServerWinSocket" nicht ... ich habe die ganze Weiterleitung etc. in OnClientRead (ist das korrekt?), da gibt es aber nur
Delphi-Quellcode:
...ClientRead(Sender: TObjet; Socket: T[B]Custom[/B]WinSocket);
.
Und:
Delphi-Quellcode:
 ( Socket <> TServerWinSocket( Sender ).Connections[ idx ] ) then
--> ist Socket nicht gleich TServerWinSocket??

Du hast doch eine TServerSocket-Komponente auf deiner Form liegen. Ich weiß nicht wie die heißt ... ist aber auch egal.
Nehmen wir mal an, die heißt ServerSocket1, dann hätte ich auch schreiben können
Delphi-Quellcode:
ServerSocket1.Connections[ idx ]
aber sowas ist immer doof, denn wenn du die Komponente umbenennst, dann funktioniert dein Code nicht mehr.
In dem Parameter Sender habe ich den Verweis auf die Komponente die diese Methode aufruft, und das ist deine TServerSocket (wie immer die auch heißen mag). Durch die Vererbung (OOP) brauche ich die Sicht auf TServerWinSocket (davon ist TServerSocket abgeleitet), denn ab da kann ich auf die Connections zugreifen.
Sender ist aber erst mal vom Typ TObject (siehst du an der Definition der Methode
Delphi-Quellcode:
procedure ClientRead(Sender: TObject; Socket: TCustomWinSocket);
). Darum benötige ich/du einen TypeCast und den macht man mit
Delphi-Quellcode:
TServerWinSocket( Sender )
. Jetzt kannst du deine Komponente auch in
Delphi-Quellcode:
RudiRüsselSeineSocketKomponente
umbenennen und es (diese Methode) wird immer noch funktionieren (in D2010 kann man auch ein ü im Namen haben).

Ich hätte auch
Delphi-Quellcode:
TServerSocket( Sender )
nehmen können, es ist aber besser immer die niedrigste benötigte Ableitung im TypeCast zu verwenden. Das wirst du merken, wenn du mal etwas mehr mit OOP arbeitest ;)

Soweit zum TypeCast

Delphi-Quellcode:
( Socket <> TServerWinSocket( Sender ).Connections[ idx ] )
Hier die Bedeutung der einzelnen Elemente:

Delphi-Quellcode:
Socket
-> Verbindung zum Client, von dem aktuell die Nachricht empfangen wird
Delphi-Quellcode:
TServerWinSocket( Sender ).Connections
-> Liste mit allen Verbindungen zum Server
Delphi-Quellcode:
TServerWinSocket( Sender ).Connections[ idx ]
-> Verbindung zu einem Client

Innerhalb der for-Schleife (idx) prüfe ich, ob die Verbindung[ idx ] = der Verbindung des Clients ist von dem aktuell die Nachricht empfangen wird, weil dem will ich die Nachricht ja nicht mehr schicken.
Der bekommt im Anschluss zwar auch eine Nachricht, aber mit einer anderen Intention.

xZise 9. Okt 2010 09:33

AW: Chat mit PM
 
Moin,
könnte er sich dann nicht den Cast sparen und einfach den zweiten Parameter verwenden?

MfG
Fabian

Sir Rufo 9. Okt 2010 09:42

AW: Chat mit PM
 
Zitat:

Zitat von xZise (Beitrag 1054644)
Moin,
könnte er sich dann nicht den Cast sparen und einfach den zweiten Parameter verwenden?

MfG
Fabian

Nein (ok ich hatte mich im letzten teil vertippt, hab es aber jetzt korrigiert), denn ich brauche ja den Zugriff auf den ServerSocket um alle anderen Verbindungen (Sockets) zu erfahren

xZise 9. Okt 2010 10:45

AW: Chat mit PM
 
Achso, und der zweite Parameter gibt den "Quellsocket" an, also wo das Paket hergekommen ist? Dann hab ich nichts gesagt :stupid: Hab halt nicht so viel damit zu tun.

MfG
Fabian

hans ditter 9. Okt 2010 23:12

AW: Chat mit PM
 
Ok, das hört sich doch alles sehr gut an!! :-D Könntet ihr mir aber nochmal auf die Sprünge helfen, wie ich herausbekomme, welcher Datentyp
Delphi-Quellcode:
.Connections[idx]
ist?
Eine generelle Frag zu Pointer hab ich nochmal:
Ich weise einen Wert zu:
Delphi-Quellcode:
New(Pointer);
Pointer^.A:='A';
Ich lese den Wert eines Pointer aus:
Delphi-Quellcode:
PointerString:=Format(%p,'Pointer^.A');
Ist das richtig? Denke, da muss ich noch was dran korrigieren...

Vielen Dank für eure große Hilfe,
hans ditter

[edit]Muss ich eigentlich eine Record und eine Klasse verwenden? [/edit]

Sir Rufo 10. Okt 2010 02:20

AW: Chat mit PM
 
Frage die Online-Hile oder die Delphi-Reference

Pro Thread nur eine Frage, du kannst aber auch die SuFu hier im Forum benutzen

Allerdings, wofür einen Pointer? In der Objekt-Variable ist eh nur die Referenz auf den Speicherbereich hinterlegt - ist also quasi ein Pointer.

Und dann noch die Umwandlung in einen String ... Merk dir doch einfach die Objekt-Referenz

Du solltest unbedingt einige Grundlagen-Tutorials durcharbeiten.
Tutorials findet man über die SuFu oder google oder ...


Alle Zeitangaben in WEZ +1. Es ist jetzt 02:31 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz