Artikel

Februar 2005 | Artikel

Nachricht an alle!

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0665)

Die Komplexität von Benachrichtigungen an mehrere Empfängerapplikationen

Text: von Ingo Rammer
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Die initiale Anforderung ist eigentlich ganz einfach: Wenn zwei Benutzer einen bestimmten Datensatz anzeigen, und ein dritter die gleichen Daten ändert, so sollen automatisch und sofort die angezeigten Informationen bei den ersten beiden Applikationen aktualisiert werden. Doch was sich so einfach anhört, kann je nach Infrastrukturanforderungen sehr komplex werden.

Die Forderung nach automatischen Benachrichtigungen, sobald ein anderer Benutzer angezeigte Daten ändert, ist aus Benutzersicht sehr verständlich. Niemand will schließlich Zeit darin investieren, einen Datensatz zu aktualisieren, nur um im Nachhinein eine Meldung zu erhalten, dass der betroffene Datensatz in der Zwischenzeit von einem anderen Benutzer ebenfalls geändert wurde und nun die aktuellen eigenen Änderungen daher verworfen wurden. Solche und ähnliche Anforderungen treten immer dann auf, wenn geänderte Informationen an eine große Zahl an Empfängern übermittelt werden müssen.

Aus technischer Sicht handelt es sich dabei um einen Problemkreis ähnlich dem des Broadcasting oder Multicasting, also der Übermittlung der gleichen Information an alle Empfänger in einer bestimmten Gruppe (beispielsweise im gleichen IP-Subnetz) oder an einen vorher definierten Empfängerkreis, der sein Interesse an einer bestimmten Informationsart bekundet hat. Interessanterweise gibt es trotz der immer wiederkehrenden Notwendigkeit der Realisierung dieser Anforderungen keine Standardtechnologie, mit deren Hilfe sich diese ein für allemal und in allen Infrastrukturumgebungen lösen lassen würden. Die gute Nachricht ist aber: Es gibt Lösungswege. Sie müssen sie nur selbst implementieren.

Synchrone oder asynchrone Übermittlung
Doch lassen Sie mich Ihre Aufmerksamkeit zuerst auf einige Herausforderungen in diesem Problemkreis lenken. Eine der ersten Fragen, die Sie sich stellen müssen ist jene, ob die Übermittlung von Informationen synchron oder asynchron erfolgen soll. Im ersten Fall würde der Server sofort nach Übermittlung des Ereignisses wissen, dass die empfangenden Clients die Information erfolgreich abgearbeitet haben. Im zweiten Fall würde die sendende Anwendung davon nicht ausgehen können, da eventuell sogar eine Message-Queueing-Infrastruktur verwendet wird, die den erfolgreichen Versand von Notifikationen auch dann erlaubt, wenn die empfangende Anwendung (und sogar die Empfängermaschine) nicht laufen oder nicht erreichbar sind. In diesem Fall würden die Informationen einfach zwischengespeichert werden.

Obwohl der erste Ansatz auf den zunächst vielleicht reizvoller erscheint - schließlich erfolgt die Zustellung anscheinend sicherer - birgt er viele Problemquellen, die die Stabilität und Skalierbarkeit Ihrer Applikationen entscheidend negativ beeinflussen können. Stellen Sie sich einfach vor, dass Ihre serverseitige Methode zur Speicherung von Kundendaten auch einige hundert Clientanwendungen benachrichtigen muss. In diesem Fall würde Ihre Serveranwendung entweder seriell oder parallel in mehreren Threads die Clients kontaktieren, die Informationen übermitteln und anschließend auf eine Bestätigung warten. Sollte nur eine einzige Clientanwendung nicht entsprechend schnell reagieren - oder zum Beispiel fehlerhafterweise eine MessageBox anzeigen - so kann Ihre Serveranwendung die ursprüngliche Anforderung zur Speicherung der Daten so lange nicht bestätigen, bis das jeweilige Problem behoben ist. Im konkreten Fall würde das bedeuten, dass so lange kein einziger Benutzer Ihrer Applikation Daten speichern kann, bis die MessageBox in der fehlerhaften Applikation bestätigt wurde. Dieses Verhalten - und eine derart große Abhängigkeit zu Clientanwendungen - ist im Normalfall nicht gewünscht, sodass synchrone Benachrichtigungsvarianten im allgemeinen ungeeignet sind. Die schließt die Verwendung von Events per .NET Remoting für Broadcast-Zwecke mit ein.
Erlaubter Informationsverlust
Auf Seite der asynchronen Protokolle gibt es verschiedene Möglichkeiten, um dieses Problem anzugehen. Die wichtigste Frage vor der Entscheidung für ein Protokoll ist die, mit welcher Sicherheit die Zustellung jedes einzelnen Ereignisses an jeden Empfänger erfolgen muss beziehungsweise soll. Ist es unter Umständen ausreichend, wenn die Benachrichtigung lediglich mit einer hohen Wahrscheinlichkeit erfolgt und Nachrichten in Einzelfällen aber verloren gehen können? Obwohl es auf den ersten Blick meist so aussieht, als ob eine Zustellungsgarantie in fast allen Anwendungsfällen notwendig ist, kann ein Verzicht darauf in vielen Fällen eine Vereinfachung der Applikationsinfrastruktur ermöglichen, da keine Notwendigkeit zur Installation von weiteren Infrastrukturkomponenten besteht.

Zusätzlich zur Frage nach der Verwendung von zuverlässigen Protokollen stellt sich bei der Zustellung von asynchronen Nachrichten immer die Frage, ob die Sender- und Empfängerapplikationen zur gleichen Zeit laufen müssen oder ob Informationen in einem Queueing-System zwischengespeichert werden sollen. Doch lassen Sie mich ein paar Beispiele bringen, um diese Fragestellungen etwas zu konkretisieren. Für die oben angeführte Anforderung zur Aktualisierung von geänderten Daten reicht zumeist die Verwendung von nicht zuverlässigen Protokollen aus. Dadurch können derartige Applikationen in allen Netzwerktypen ohne weitere Voraussetzungen installiert werden. Im Normalfall können Sie sich für diese Art der Benachrichtigung immer dann entscheiden, wenn der Verlust einer Nachricht keine weiteren Auswirkungen auf die Korrektheit des Systems hat, sondern lediglich der Benutzerfreundlichkeit dient.

Bei der Übertragung von sensiblen Daten auf der anderen Seite - beispielsweise von Änderungen an Sicherheitseinstellungen und Benutzerberechtigungen - ist meist eine zuverlässige Übertragung notwendig, da der Verlust einer einzelnen Nachricht drastische Auswirkungen auf die Systemsicherheit haben könnte. Die letzte Gruppe der zuverlässigen Nachrichten, die zwischengespeichert werden, kommt meist dann zum Tragen, wenn die asynchronen Benachrichtigungen zur Synchronisation von Informationen eingesetzt werden. So können beispielsweise Änderungen an Preislisten auf diese Art an eine Vielzahl von mobilen Geräten von Außendienstmitarbeiter übermittelt werden. Hier ist die angeführte Zwischenspeicherung notwendig, da die Zielmaschinen bzw. Zielapplikationen zum Zeitpunkt des Informationsversands nicht zwangsläufig in jenem Moment mit dem Netzwerk verbunden und daher unter Umständen nicht erreichbar sind.
UDP für nichtkritische Anwendungen
Das der IP-Familie zugehörige UDP (User Datagram Protocol) bietet eine sehr einfache Möglichkeit, Nachrichten an mehrere Empfänger zu senden. Hierzu sind keine weiteren Infrastrukturkomponenten notwendig, da dieses Protokoll von allen TCP/IP-Implementierungen direkt angeboten wird. Das Besondere an UDP ist, dass es die Übermittlung von Nachrichten an das gesamte IP-Subnetz (LAN) des Senders ermöglicht. Auf Seite des Servers reicht es daher aus, eine einzelne Nachricht zu erstellen und zu senden. Diese Nachricht kann in Folge von allen horchenden Maschinen empfangen werden. Die dafür notwendigen Klassen finden Sie im .NET Framework im Namespace System.Net und System.Net.Sockets. Der einfache Versand von Nachrichten, basierend auf beliebigen Objekten, die mit [Serializable] markiert sind, kann wie in Listing 1 gezeigt erfolgen.

Listing 1
Versand von Nachrichten an ein gesamtes IP-Subnetz
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using System.Runtime.Serialization.Formatters.Binary;
  6. public class Sender
  7. {
  8. public static void Main()
  9. {
  10. Console.Write("Enter string to broadcast: ");
  11. String str = Console.ReadLine();
  12. SendMessageToSubnet(str, 10000);
  13. Console.ReadLine();
  14. }
  15. private static void SendMessageToSubnet(object msg, int port)
  16. {
  17. MemoryStream ms = new MemoryStream();
  18. BinaryFormatter fmt = new BinaryFormatter();
  19. fmt.Serialize(ms, msg);
  20. ms.Seek(0, SeekOrigin.Begin);
  21. Socket sck = new Socket(
  22. AddressFamily.InterNetwork,
  23. SocketType.Dgram,
  24. ProtocolType.Udp);
  25. sck.Connect(new IPEndPoint( IPAddress.Broadcast,10000));
  26. sck.Send(ms.GetBuffer());
  27. sck.Close();
  28. }
  29. }

Auf Seite des Empfängers kann ähnlich verfahren werden, um die Nachrichten wieder zu deserialisieren. Sie finden ein Beispiel dazu in Listing 2.

Listing 2
Eine einfache Empfängeranwendung
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Net;
  5. using System.Runtime.Serialization.Formatters.Binary;
  6. public class Receiver
  7. {
  8. public static void Main()
  9. {
  10. int port = 10000;
  11. Socket sck = new Socket(
  12. AddressFamily.InterNetwork,
  13. SocketType.Dgram,
  14. ProtocolType.Udp);
  15. sck.Bind(new IPEndPoint( IPAddress.Any, port));
  16. BinaryFormatter fmt = new BinaryFormatter();
  17. byte[] buf = new byte[65000];
  18. while (true)
  19. {
  20. int size = sck.Receive(buf);
  21. MemoryStream ms = new MemoryStream(buf,0,size);
  22. object msg = fmt.Deserialize(ms);
  23. Console.WriteLine("Received: {0}", msg.ToString());
  24. }
  25. }
  26. }

Beachten Sie aber, dass diese Art des Nachrichtenversandes nur funktioniert, wenn der Sender und alle Empfänger im gleichen IP-Subnetz sind, die Nachrichtengröße 64 KB nicht überschritten wird und der Verlust von einzelnen Nachrichten keine kritische Auswirkung auf Ihre Applikation hat. Sollten Sie mehr als ein Subnetz überbrücken wollen, können Sie aber einfach einen Rechner im zweiten Subnetz als Zwischenstelle verwenden. Dort könnte dann ein kleines Programm laufen, das direkt an diesen Rechner gesandte Nachrichten vom ersten Server annimmt und diese in sein lokales Subnetz weitersendet.
MSMQ, wenn nichts verloren gehen darf
Wenn aber - im Gegensatz zu den UDP-basierenden Beispielen - der Verlust von Nachrichten für Ihre Applikation kritisch ist, so bietet sich die Verwendung von MSMQ (Microsoft Message Queueing) an. MSMQ ist seit dem Option Pack für Windows NT 4.0 ein kostenloser optionaler Bestandteil aller Windows-Betriebssysteme, der jedoch in den meisten Fällen auf allen Sender- und Empfänger-Computern zusätzlich installiert werden muss. Die aktuellen Versionen (2.x und 3.0) von MSMQ haben keine Notwendigkeit für SQL Server oder Active Directory, sondern lassen sich auch in Workgroup-Konfigurationen problemlos verwenden. Seit Version 3.0 bietet MSMQ die Möglichkeit an, einzelne Nachrichten an eine Vielzahl von Empfängern zu senden. Anders als bei UDP müssen sich diese Empfänger nicht im gleichen Subnetz befinden. Es muss aber stattdessen beim Versand explizit jeder Empfänger angegeben werden. Die Warteschlangen innerhalb von MSMQ können Sie sich im Prinzip wie ein E-Mail-Postfach vorstellen. Im Gegensatz zu normalen E-Mails werden die Nachrichten jedoch von einem Programm empfangen und können auch in sehr hoher Geschwindigkeit übertragen werden. Die Übertragung von mehreren Tausend Nachrichten pro Sekunde ist in MSMQ-basierenden Anwendungen keine Seltenheit.

Anders als bei Datenbanksystemen befindet sich der Nachrichtenspeicher nicht an einer Stelle zentralisiert, sondern wird von jedem Computer getrennt verwaltet. Im Normalfall lesen Applikationen daher immer die Nachrichten von ihrem lokalen Computer und senden Nachrichten an entfernte Maschinen. Wenn Sie also einen Server und 100 Clients mit ihrer Applikation bedienen möchten, so werden Sie zumindest mit 101 Warteschlagen arbeiten: eine auf dem Server, und jeweils eine auf jeder Clientmaschine. Um alle Clients zu benachrichtigen, sendet die Serveranwendung daher 100 Kopien der gleichen Nachricht an die 100 Empfängermaschinen. (Dies erledigt MSMQ aber automatisch für Sie im Hintergrund, ohne die Skalierbarkeit oder Stabilität Ihrer Applikation negativ zu beeinflussen.)

Die Anlage der Warteschlagen kann entweder bei der Programminstallation auf jedem Client, bei Programmstart (falls die Benutzerrechte dies erlauben) oder im Testfall auch manuell erfolgen. Für die automatische Erstellung können Sie die Funktion MessageQueue.Create() verwenden. Die manuelle Erzeugung von Warteschlagen erfolgt über ein MMC Snap-in, das Sie über Arbeitspatz | Eigenschaften | Verwalten starten. Dort erhalten Sie in der Kategorie Dienste und Anwendungen | Message Queueing Zugriff auf die MSMQ-Einstellungen. Bevor ich Ihnen ein Beispiel zu Versand und Empfang von Nachrichten zeige, möchte ich noch die Begriffe öffentliche Warteschlange (public queue) und private Warteschlage (private queue), die von MSMQ verwendet werden, klären. Die Unterscheidung betrifft nämlich nicht - wie manchmal fälschlicherweise angenommen - die Erreichbarkeit von Warteschlagen, sondern lediglich deren Registrierung in einem eventuell vorhandenen Active Directory. Sie können also sowohl Nachrichten an öffentliche als auch an private Warteschlagen von anderen Rechnern senden. Bei privaten Warteschlagen müssen Sie jedoch deren Namen kennen, während Sie öffentliche Warteschlagen im Active Directory suchen können.

Der Versand von Nachrichten per MSMQ gestaltet sich etwas anders als der von UDP-Paketen. Der größte Unterschied liegt darin, dass Sie bei MSMQ die Adresse jedes Empfängers explizit angeben müssen, während Sie bei UDP an ein gesamtes Subnetz senden können (zusätzlich zu der hier vorgestellten Variante bietet MSMQ noch die Möglichkeit, Nachrichten mittels IP Multicast zu verteilen. In diesem Fall würde auch nur ein einzelner Empfänger angegeben werden und die eigentliche Verteilung über die IP-Multicasting-Infrastruktur erfolgen.)
Die Adressierung erfolgt über die Angabe eines verketteten FormatNames, der die Adressen der einzelnen Queues, durch Komma getrennt, enthält. Ein Beispiel für einen derartigen Namen, der die private Queue Queue01 auf Server01 und die private Warteschlange Queue02 auf Server02 referenziert, lautet FormatName:direct=os:Server01\private$\Queue01,os:Server02\private$\Queue02.
Um beispielsweise eine Nachricht an die private Warteschlange Notifications der vier Maschinen Test01 bis Test04 zu versenden, können Sie Programmcode ähnlich dem verwenden, der in Listing 3 gezeigt wird. Sie müssen dazu Ihrem Projekt einen Verweis auf System.Messaging.DLL hinzufügen.

Listing 3
Versand einer MSMQ-Nachricht an mehrere Empfänger
  1. using System;
  2. using System.Text;
  3. using System.Collections;
  4. using System.Messaging;
  5. class Sender
  6. {
  7. static void Main(string[] args)
  8. {
  9. Console.Write("Enter string to broadcast:");
  10. String str = Console.ReadLine();
  11. ArrayList clients = new ArrayList();
  12. clients.Add("test01");
  13. clients.Add("test02");
  14. clients.Add("test03");
  15. clients.Add("test04");
  16. String formatName = BuildFormatName(clients);
  17. MessageQueue que = new MessageQueue(formatName);
  18. Message msg = new Message();
  19. msg.Formatter = new BinaryMessageFormatter();
  20. msg.Body = str;
  21. que.Send(msg);
  22. Console.ReadLine();
  23. }
  24. static string BuildFormatName(ArrayList clients)
  25. {
  26. StringBuilder bld = new StringBuilder();
  27. bld.Append("FormatName:");
  28. foreach (String cli in clients)
  29. {
  30. bld.Append("direct=os:");
  31. bld.Append(cli);
  32. bld.Append("\\private$\\NOTIFICATIONS");
  33. bld.Append(",");
  34. }
  35. bld.Remove(bld.Length-1,1);
  36. return bld.ToString();
  37. }
  38. }

Die in Listing 4 gezeigte Empfängerapplikation ist so geschrieben, dass sie per MessageQueue.Create() die jeweilige Warteschlange anlegt, sofern diese noch nicht vorhanden ist. Dieser Code ist nur zur Vollständigkeit gezeigt, da dessen Ausführung nur bei entsprechenden Benutzerberechtigungen möglich ist. Normalerweise werden Sie daher den Quellcode zur Erstellung von Warteschlangen eher in Ihr Setup-Projekt mit aufnehmen.

Listing 4
Empfangen von Nachrichten per MSMQ
  1. using System;
  2. using System.Messaging;
  3. class Receiver
  4. {
  5. static void Main(string[] args)
  6. {
  7. String queuename = @".\private$\NOTIFICATIONS";
  8. if (!MessageQueue.Exists(queuename))
  9. {
  10. MessageQueue.Create(queuename);
  11. }
  12. MessageQueue que = new MessageQueue(queuename);
  13. que.Formatter = new BinaryMessageFormatter();
  14. while (true)
  15. {
  16. using (Message msg = que.Receive())
  17. {
  18. String str = (String) msg.Body;
  19. Console.WriteLine("Received: {0}", str);
  20. }
  21. }
  22. }
  23. }
Die gezeigten Sender und Empfänger sind natürlich sehr einfach. In der Praxis können Sie beispielsweise auf Seiten des Empfängers eher BeginReceive() statt Receive() verwenden, um die Anwendung nicht zu blockieren.
Zustellungsgarantien
Wenn Sie MSMQ wie oben gezeigt verwenden, so nutzen Sie den so genannten Express-Modus beim Versand von Nachrichten. Diese Standardeinstellung führt dazu, dass Nachrichten lediglich im Speicher der einzelnen Maschinen gehalten und nicht auf einem persistenten Medium gesichert werden. Sollte eine Maschine (oder auch nur der MSMQ-Dienst) beendet und neu gestartet werden, so würden alle Nachrichten in diesem Modus, die sich momentan auf der jeweiligen Maschine befinden, verloren gehen. Dieses Verhalten ist für viele Anwendungen ausreichend, da beispielsweise die Nachrichten für die Clientapplikation nach dem Neustart nicht mehr relevant sein könnten. Wenn Nachrichten zwischengespeichert werden sollen, so müssen Sie die Eigenschaft Recoverable des Message-Objekts auf True setzen. Um eine noch höhere Nachvollziehbarkeit von Nachrichten zu erhalten, können Sie zusätzlich noch so genannte transaktionale Warteschlagen verwenden. Dies ist eine Option, die Sie beim Erstellen mittels MessageQueue.Create(), sowie beim Versand von Nachrichten mit angeben müssten. In diesem Fall könnte die Zeile zum Versand der Nachricht beispielsweise que.Send(msg, MessageQueueTransactionType.Single) sein.
Erlaubte Zeitverzögerung
Gerade diese letzte Option birgt jedoch die Gefahr in sich, dass veraltete Nachrichten länger in Queues liegen bleiben, als es applikationstechnisch Sinn macht. Wenn Ihre Applikation beispielsweise alle 24 Stunden die komplette Preisliste (und nicht nur die Differenzen zum Vortag) an eine gewisse Anzahl von Empfängern übermittelt, so ist es normalerweise nicht besonders sinnvoll, wenn diese Nachrichten länger als 24 Stunden zwischengespeichert werden. Ansonsten müsste der Außendienstmitarbeiter, der nach drei Wochen vom Urlaub zurückkommt, zuerst einmal die Preislisten der letzten Wochen durch seine Applikation abarbeiten lassen. Sie können daher - und auch um eventuell Zustellungsfehler festzustellen - die Eigenschaften TimeToReachQueue und TimeToBeReceived eines Message-Objektes setzen. Mit TimeToReachQueue geben Sie an, innerhalb welcher Zeit eine Nachricht die angegebene Queue auf dem Zielrechner erreichen muss. Die zweite Option TimeToBeReceived spezifiziert die maximale Vorhaltedauer in der eine Nachricht von der Zielapplikation gelesen wird - und damit aus der Warteschlange entfernt. Sobald eines der beiden Zeitlimits erreicht wird, wird die Zustellung der Nachricht abgebrochen und diese gegebenenfalls aus der Warteschlange entfernt. Wenn Sie bei Bedarf gleichzeitig die Optionen UseDeadLetterQueue und/oder AdministrationQueue setzten, so erhalten Sie beim Erreichen dieser Limits eine Benachrichtigung an die so genannte Dead-Letter Queue (in der die unzustellbaren Nachrichten abgelegt werden) oder an die angegebene administrative Warteschlange. Eine andere Komponente Ihrer Serverapplikation kann nun auf die fehlgeschlagenen Zustellversuche reagieren und entsprechende Aktionen setzen.
Firewalls, Proxy Server und Polling
Die beiden vorgestellten Varianten erlauben eine skalierbare Benachrichtigung von Empfängern in lokalen Netzwerken und Intranets. Bei beiden Technologien ist es notwendig, dass der Sender die jeweiligen Empfänger kontaktieren darf. Wenn aber beispielsweise Firewalls, HTTP-Proxies oder ähnliche Sicherheitsmechanismen diesen Zugriff verbieten, so müssen Sie auf andere Mechanismen zurückgreifen. Wenn Sie beispielsweise eine Applikation entwickeln möchten, die in allen Umgebungen lauffähig ist, so müssen Sie den kleinsten gemeinsamen Nenner wählen: das HTTP-Protokoll. Nur damit ist sichergestellt, dass Ihre Applikation auch dann funktioniert, wenn clientseitige Firewalls alle anderen Dienste sperren. Eine konkrete Implementierung des folgenden Konzepts würde den Rahmen dieser Kolumne sprengen (aber senden Sie mir doch einfach eine E-Mail, wenn Sie daran interessiert sind. Ich werde bei entsprechendem Interesse diesen Weg eventuell in einer Folgeausgabe weiter konkretisieren). Für eine derartige Lösung müssten Sie im Prinzip zuerst serverseitig die Ereignisse für Ihre Clients in einer Datenbank zwischenspeichern. Die Clients können dann in regelmäßigen Intervallen beispielsweise einen Web Service aufrufen, und dort anfragen, ob neue Nachrichten vorliegen. Je nach Anforderungen an Skalierbarkeit und Zustellungszeit könnten Sie hierzu eine weitere Optimierung machen: Der Server könnte in diesem Fall die eingehenden HTTP-Anfrage des Clients einfach offen und den Thread so lange pausieren lassen, ohne Daten zu senden, bis Nachrichten für den jeweiligen Client ankommen. Dies sollte eine sehr schnelle Benachrichtigung von wartenden Clients ohne den Overhead von sehr häufigen Polling-Anfragen bieten. Beachten Sie hierbei aber bitte unbedingt auch die Thread-Limits des Internet Information Server (ISS) und des ASP.NET Framework. Eventuell müssen Sie für diese eher exotische Lösung einen eigenen kleinen HTTP-Server in .NET entwickeln. Doch auch das stellt mit System.Net.Sockets kein Problem dar ...


Anzeige

Kommentare

zurück zum Seitenanfang