C#: Eine Message Queuing-Anwendung

Veröffentlicht: 06. Jan 2001 | Aktualisiert: 09. Nov 2004
Von Carl Nolan

In diesem Artikel wird eine Windows-Dienstlösung beschrieben, die für die Verarbeitung mehrerer Nachrichtenwarteschlangen entwickelt wurde. Der Schwerpunkt liegt dabei auf der Anwendung von .NET Framework und C#.

Auf dieser Seite

Einführung Einführung
Die .NET Framework-Anwendung Die .NET Framework-Anwendung
Struktur der Anwendung Struktur der Anwendung
Dienstklassen Dienstklassen
Instrumentierung Instrumentierung
Installation Installation
Schlussfolgerung Schlussfolgerung
Referenzmaterial Referenzmaterial

Einführung

Microsoft führte vor kurzer Zeit eine neue Plattform für die Erstellung integrierter Anwendungen ein – Microsoft .NET Framework. Mit .NET Framework können Entwickler schnell Webdienste in jeder Programmiersprache erstellen und weitergeben. Diese sprachneutrale Struktur wird durch die Microsoft Intermediate Language (MSIL) und Just-In-Time-Compiler ermöglicht (JIT).

Zusammen mit .NET Framework wurde eine neue Programmiersprache, C# (sprich "C sharp") bereitgestellt. C# ist eine einfache, moderne, objektorientierte und typsichere Programmiersprache. Durch die Nutzung von .NET Framework und C# (zusätzlich zu Microsoft® Visual Basic® und Managed C++) können hochfunktionale Microsoft Windows®- und Webanwendungen und -Dienste geschrieben werden. In diesem Artikel wird eine solche Lösung vorgestellt, wobei sich die Anwendung auf .NET Framework und C# konzentriert anstatt auf die Programmiersprache. Eine Einführung in die C#-Sprache finden Sie unter C# Introduction and Overview (in Englisch).

Im letzten Artikel Message Queuing: A Scalable, Highly Available Load-Balancing Solution (in Englisch) wurde eine Lösung für immer verfügbares Message Queuing (MSMQ) mit skalierbarer Lösungsarchitektur für Lastenausgleich vorgestellt. Diese Lösung beinhaltete die Entwicklung eines Windows-Dienstes, der als intelligenter Nachrichtenrouter fungiert. Bislang gehörte eine solche Lösung in den Aufgabenbereich des Microsoft Visual C++®-Programmierers. Seit der Einführung von .NET Framework ist dies nicht mehr so, wie die folgende Lösung zeigt.

Die .NET Framework-Anwendung

Die hier beschriebene Lösung ist ein Windows-Dienst für die Verarbeitung mehrerer Nachrichtenwarteschlangen. Dabei wird jede Warteschlange durch mehrere Threads verarbeitet, die Nachrichten empfangen und verarbeiten. Zum Leiten von Nachrichten aus einer Liste mit Zielwarteschlangen per Round-Robin-Verfahren oder mit Hilfe eines anwendungsspezifischen Wertes (die AppSpecific-Eigenschaft der Nachricht) und zum Aufrufen einer Komponentenmethode mit den Eigenschaften der Nachricht sind Beispielprozesse verfügbar. Beim Abrufen einer Komponente muss diese eine gegebene Schnittstelle mit dem Namen "IWebMessage" implementieren. Für die Fehlerbehandlung sendet die Anwendung Nachrichten, die nicht in einer Fehlerwarteschlange verarbeitet werden können.

Die Strukturierung der Messaginganwendung ähnelt der der vorherigen Active Template Library-Anwendung (ATL). Der größte Unterschied besteht darin, dass für die Verwaltung des Dienstes und die Verwendung der .NET Framework-Komponenten Code gekapselt ist. Da .NET Framework objektorientiert ist, ist es nicht weiter überraschend, dass zum Erstellen eines Windows-Dienstes aus der System.ServiceControl-Baugruppe nur eine Klasse erstellt werden muss, die von ServiceBase erbt.

Struktur der Anwendung

Die wichtigste Klasse in der Anwendung ist ServiceControl, die Klasse, die von ServiceBase erbt. Mit der Vererbung von ServiceBase müssen außer den optionalen Methoden OnPause und OnContinue die Methoden OnStart und OnStop implementiert werden. Die Klasse wird eigentlich innerhalb der statischen Main-Methode erstellt:

using System; 
using System.ServiceProcess; 
public class ServiceControl: ServiceBase 
{ 
   // Wichtigster Einstiegspunkt, der das Dienstobjekt erstellt 
   public static void Main() 
   { 
      ServiceBase.Run(new ServiceControl()); 
   } 
   // Konstruktorobjekt, das die Dienstparameter definiert 
   public ServiceControl() 
   { 
      CanPauseAndContinue = true; 
      ServiceName = "MSDNMessageService"; 
      AutoLog = false; 
   } 
   protected override void OnStart(string[] args) {...} 
   protected override void OnStop() {...} 
   protected override void OnPause() {...} 
   protected override void OnContinue() {...} 
} 


Die ServiceControl-Klasse erstellt eine Reihe von CWorker-Objekten, wobei für jede Nachrichtenwarteschlange, die verarbeitet werden muss, eine Instanz einer CWorker-Klasse erstellt wird. Die CWorker-Klasse wiederum erstellt basierend auf der erforderlichen Anzahl der Threads, die für die Verarbeitung der Warteschlange definiert sind, eine Reihe von CWorkerThread-Objekten. Die CWorkerThread-Klasse erstellt einen Verarbeitungsthread, der die eigentliche Arbeit des Dienstes ausführt.

Die wichtigste Aufgabe der Klassen CWorker und CWorkerThread ist die Bestätigung der Befehle zum Starten, Beenden, Anhalten und Fortsetzen der Dienstkontrolle. Da diese Prozesse den Dienst nicht sperren dürfen, veranlassen die Befehlsaktionen letztlich eine Aktion auf einem Verarbeitungsthread im Hintergrund.

CWorkerThread ist eine abstrakte Klasse, von der CWorkerThreadAppSpecific, CWorkerThreadRoundRobin und CWorkerThreadAssembly erben. Diese Klassen verarbeiten Nachrichten auf verschiedene Arten. Die ersten beiden Klassen verarbeiten eine Nachricht, indem sie sie an eine andere Warteschlange senden (sie unterscheiden sich dadurch, wie der Pfad für empfangene Warteschlangen ermittelt wird), die dritte Klasse verwendet die Eigenschaften der Nachrichten, um eine Komponentenmethode aufzurufen.

Die Fehlerbehandlung in .NET Framework basiert auf einer Exception-Basisklasse. Wenn Fehler ausgegeben oder erfasst werden, müssen sie zu einer von Exception abgeleiteten Klasse gehören. Bei der CWorkerThreadException-Klasse handelt es sich um eine solche Implementierung, die die Basisklasse durch eine Eigenschaft erweitert, die definiert, ob der Dienst weiter ausgeführt werden soll.

Außerdem enthält die Anwendung zwei Strukturen. Diese Werttypen definieren die Laufzeitparameter eines Workerprozesses oder -Threads, um das Erstellen der Objekte CWorker und CWorkerThread zu vereinfachen. Durch die Verwendung einer Werttypstruktur anstelle einer Verweistypklasse wird gewährleistet, dass für diese Laufzeitparameter anstelle von Verweisen Werte verwaltet werden.

"IWebMessage"-Schnittstelle

Eine der bereitgestellten CWorkerThread-Implementierungen ist eine Klasse, die eine Komponentenmethode aufruft. Diese Klasse, CWorkerThreadAssembly, verwendet für die Definition des Vertrags zwischen dem Dienst und der Komponente die IWebMessage-Schnittstelle.

Anders als in der aktuellen Version von Microsoft Visual Studio® können C#-Schnittstellen in einer beliebigen Sprache explizit definiert werden, wodurch das Erstellen und Kompilieren der IDL-Dateien entfällt. Die Verwendung der C#-IWebMessage-Schnittstelle ist wie folgt definiert:

public interface IWebMessage 
{ 
   WebMessageReturn Process(string sMessageLabel, string sMessageBody, int iAppSpecific); 
   void Release(); 
} 


Die Process-Methode ist wie im ATL-Code für die Nachrichtenverarbeitung gekennzeichnet. Der Rückgabecode der Process-Methode wird als Enumerationstyp WebMessageReturn definiert:

public enum WebMessageReturn  
{ 
   ReturnGood, 
   ReturnBad, 
   ReturnAbort 
} 


Es gibt drei Enumerationsdefinitionen: Bei Good wird die Verarbeitung fortgesetzt, bei Bad wird die Nachricht in die Fehlerwarteschlange geschrieben, und bei Abort wird die Verarbeitung beendet. In der Release-Methode wird ein Mechanismus für den Dienst bereitgestellt, um die Klasseninstanz zu vernichten. Da der Destruktor der Instanz der Klasse nur während der Garbagecollection aufgerufen wird, ist dies ein gutes Verfahren, um zu gewährleisten, dass alle Klassen mit kostenaufwendigen Ressourcen (z.B. Datenbankverbindungen) über eine Methode verfügen, die vor der Vernichtung aufgerufen werden kann, um diese Ressourcen freizugeben.

Namespaces

An dieser Stelle müssen kurz Namespaces erwähnt werden. Durch Namespaces können Anwendungen für die interne und externe Darstellung in logischen Elementen gruppiert werden. Der gesamte Code in diesem Dienst ist im MSDNMessageService.Service-Namespace enthalten. Obwohl sich der Dienstcode in mehreren Dateien befindet (da sich diese im selben Namespace befinden), müssen Sie nicht auf die anderen Dateien verweisen.

Da der MSDNMessageService.Interface-Namespace die IWebMessage-Schnittstelle enthält, wird für die Threadklasse, die diese Schnittstelle verwendet, der Schnittstellennamespace importiert.

Dienstklassen

Ziel der Anwendung ist die Überwachung und Verarbeitung von Nachrichtenwarteschlangen, die für die empfangenen Nachrichten jeweils andere Prozesse verwenden. Die Anwendung wird als Windows-Dienst implementiert.

"ServiceBase"-Klasse

Wie bereits erwähnt, ist die Standardstruktur eines Dienstes eine Klasse, die von ServiceBase erbt. Die wichtigen Methoden sind OnStart, OnStop, OnPause und OnContinue, wobei jede überschriebene Methode direkt einer Dienstkontrollaktion entspricht. Zweck der OnStart-Methode ist es, CWorker-Objekte zu erstellen. Dabei erstellt die CWorker-Klasse CWorkerThread-Objekte, aus denen die Threads erstellt werden, die die Arbeit des Dienstes ausführen.

Die Laufzeitkonfiguration des Dienstes und damit die Eigenschaften der Objekte CWorker und CWorkerThread wird in einer XML-basierten Konfigurationsdatei verwaltet, die den Namen der erstellten EXE-Datei (jedoch mit der Erweiterung CFG) übernimmt. Ein Beispiel für die Konfiguration:

<?xml version="1.0"?> 
<configuration> 
<ProcessList> 
  <ProcessDefinition 
       ProcessName="Worker1" 
       ProcessDesc="Nachrichtenworker mit 2 Threads" 
       ProcessType="AppSpecific" 
       ProcessThreads="2" 
       InputQueue=".\private$\test_load1" 
       ErrorQueue=".\private$\test_error"> 
    <OutputList> 
      <OutputDefinition OutputName=".\private$\test_out11" /> 
      <OutputDefinition OutputName=".\private$\test_out12" /> 
    </OutputList> 
  </ProcessDefinition> 
  <ProcessDefinition 
       ProcessName="Worker2" 
       ProcessDesc="Baugruppenworker mit 1 Thread" 
       ProcessType="Assembly" 
       ProcessThreads="1" 
       InputQueue=".\private$\test_load2" 
       ErrorQueue=".\private$\test_error"> 
    <OutputList> 
      <OutputDefinition OutputName="C:\MSDNMessageService\MessageExample.dll" /> 
      <OutputDefinition OutputName="MSDNMessageService.MessageSample.ExampleClass" /> 
    </OutputList> 
  </ProcessDefinition> 
</ProcessList> 
</configuration> 


Der Zugriff auf diese Informationen wird durch die ConfigManager-Klasse aus der System.Configuration-Baugruppe verwaltet. Die statische Get-Methode gibt eine Informationsauflistung zurück, die enumeriert werden kann, um einzelne Eigenschaften abzurufen. Diese Eigenschaftensätze bestimmen die Laufzeiteigenschaften eines Workerobjekts. Neben dieser Konfigurationsdatei sollten Sie eine Metadatei erstellen, in der die Struktur der XML-Datei definiert wird und die auf die Metadatei verweist, die sich in der Konfigurationsdatei machine.cfg des Servers befindet:

<?xml version ="1.0"?> 
<MetaData xmlns="x-schema:CatMeta.xms"> 
   <DatabaseMeta InternalName="MessageService"> 
   <ServerWiring Interceptor="Core_XMLInterceptor"/> 
   <Collection  
         InternalName="Process" PublicName="ProcessList" 
         PublicRowName="ProcessDefinition" 
         SchemaGeneratorFlags="EMITXMLSCHEMA"> 
      <Property InternalName="ProcessName" Type="String" MetaFlags="PRIMARYKEY" /> 
      <Property InternalName="ProcessDesc" Type="String" /> 
      <Property InternalName="ProcessType" Type="Int32" DefaultValue="RoundRobin" > 
         <Enum InternalName="RoundRobin"  Value="0"/> 
         <Enum InternalName="AppSpecific" Value="1"/> 
         <Enum InternalName="Assembly" Value="2"/> 
      </Property> 
      <Property InternalName="ProcessThreads" Type="Int32" DefaultValue="1" /> 
      <Property InternalName="InputQueue" Type="String" /> 
      <Property InternalName="ErrorQueue" Type="String" /> 
      <Property InternalName="OutputName" Type="String" /> 
      <QueryMeta InternalName="All" MetaFlags="ALL" /> 
      <QueryMeta InternalName="QueryByFile" CellName="__FILE" Operator="EQUAL"  /> 
   </Collection> 
   <Collection  
         InternalName="Output" PublicName="OutputList" 
         PublicRowName="OutputDefinition" 
         SchemaGeneratorFlags="EMITXMLSCHEMA"> 
      <Property InternalName="ProcessName" Type="String" MetaFlags="PRIMARYKEY" /> 
      <Property InternalName="OutputName" Type="String" MetaFlags="PRIMARYKEY" /> 
      <QueryMeta InternalName="All" MetaFlags="ALL" /> 
      <QueryMeta InternalName="QueryByFile" CellName="__FILE" Operator="EQUAL"  /> 
   </Collection> 
   </DatabaseMeta> 
   <RelationMeta    
      PrimaryTable="Process" PrimaryColumns="ProcessName" 
      ForeignTable="Output"  ForeignColumns="ProcessName" 
      MetaFlags="USECONTAINMENT"/> 
</MetaData> 


Da die Service-Klasse eine Liste der erstellten Workerobjekte enthalten muss, wird die Hashtable-Auflistung verwendet, die eine Liste mit Name/Wert-Paar-Objekttypen enthält. Hashtable unterstützt Enumerierungen, und Werte können nach Schlüssel abgefragt werden. In der Anwendung ist der XML-Prozessname der eindeutige Schlüssel:

private Hashtable htWorkers = new Hashtable(); 
IConfigCollection cWorkers = ConfigManager.Get("ProcessList", new AppDomainSelector()); 
foreach (IConfigItem ciWorker in cWorkers) 
{ 
   WorkerFormatter sfWorker = new WorkerFormatter(); 
   sfWorker.ProcessName = (string)ciWorker["ProcessName"]; 
   sfWorker.ProcessDesc = (string)ciWorker["ProcessDesc"]; 
   sfWorker.NumberThreads = (int)ciWorker["ProcessThreads"]; 
   sfWorker.InputQueue = (string)ciWorker["InputQueue"]; 
   sfWorker.ErrorQueue = (string)ciWorker["ErrorQueue"]; 
   // Berechnen und Definieren des Verarbeitungstyps 
   switch ((int)ciWorker["ProcessType"]) 
   { 
      case 0: 
         sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessRoundRobin; 
         break; 
      case 1: 
         sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessAppSpecific; 
         break; 
      case 2: 
         sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessAssembly; 
         break; 
      default: 
         throw new Exception("Unbekannter Verarbeitungstyp"); 
   } 
   // Weitere Arbeit zum Lesen der Ausgabeinformationen 
   string sProcessName = (string)ciWorker["ProcessName"]; 
   if (htWorkers.ContainsKey(sProcessName)) 
      throw new ArgumentException("Prozessname muss eindeutig sein: " + sProcessName); 
   htWorkers.Add(sProcessName, new CWorker(sfWorker)); 
} 


Die wichtigste Information, die im Code fehlt, ist der Abruf der Ausgabedaten. In jeder Prozessdefinition gibt es eine Reihe entsprechender Ausgabedefinitionseinträge. Diese Information wird über eine einfache Abfrage eingelesen:

string sQuery = "SELECT * FROM OutputList WHERE ProcessName=" +  
   sfWorker.ProcessName + " AND Selector=appdomain://"; 
ConfigQuery qQuery = new ConfigQuery(sQuery); 
IConfigCollection cOutputs = ConfigManager.Get("OutputList", qQuery); 
int iSize = cOutputs.Count, iLoop = 0; 
sfWorker.OutputName = new string[iSize]; 
foreach (IConfigItem ciOutput in cOutputs) 
   sfWorker.OutputName[iLoop++] = (string)ciOutput["OutputName"]; 


Die Klassen CWorkerThread und CWorker verfügen über entsprechende Dienstkontrollmethoden, die aufgrund der Dienstkontrollaktion aufgerufen werden. Da in Hashtable auf jedes CWorker-Objekt verwiesen wird, wird der Inhalt von Hashtable enumeriert, um die geeignete Dienstkontrollmethode aufzurufen:

foreach (CWorker cWorker in htWorkers.Values) 
   cWorker.Start(); 


Auf ähnliche Weise basiert die Funktionsweise der implementierten Methoden OnPause, OnContinue und OnStop auf dem Aufrufen der entsprechenden Methoden von CWorker-Objekten.

"CWorker"-Klasse

Die wichtigste Funktion der CWorker-Klasse ist das Erstellen und Verwalten von CWorkerThread-Objekten. Mit den Methoden Start, Stop, Pause und Continue werden die entsprechenden CWorkerThread-Methoden aufgerufen. Die eigentlichen CWorkerThread-Objekte werden in der Start-Methode erstellt. Ebenso wie die Service-Klasse, die mit Hashtable die Verweise auf Workerobjekte verwaltet, verwendet CWorker für die Pflege einer Liste mit Threadobjekten ArrayList, ein einfaches Array dynamischer Größe.

In diesem Array erstellt die CWorker-Klasse eine der implementierten Versionen der CWorkerThread-Klasse. Bei der CWorkerThread-Klasse, die im nächsten Abschnitt erläutert wird, handelt es sich um eine abstrakte Klasse, die geerbt werden muss. Die abgeleiteten Klassen definieren, wie eine Nachricht verarbeitet wird:

aThreads = new ArrayList(); 
for (int idx=0; idx<sfWorker.NumberThreads; idx++) 
{ 
   WorkerThreadFormatter wfThread = new WorkerThreadFormatter(); 
   wfThread.ProcessName = sfWorker.ProcessName; 
   wfThread.ProcessDesc = sfWorker.ProcessDesc; 
   wfThread.ThreadNumber = idx; 
   wfThread.InputQueue = sfWorker.InputQueue; 
   wfThread.ErrorQueue = sfWorker.ErrorQueue; 
   wfThread.OutputName = sfWorker.OutputName; 
   // Definieren des Workertyps und Einfügen in die Workerthreadstruktur 
   CWorkerThread wtBase; 
   switch (sfWorker.ProcessType) 
   { 
      case WorkerFormatter.SFProcessType.ProcessRoundRobin: 
         wtBase = new CWorkerThreadRoundRobin(this, wfThread); 
         break; 
      case WorkerFormatter.SFProcessType.ProcessAppSpecific: 
         wtBase = new CWorkerThreadAppSpecific(this, wfThread); 
         break; 
      case WorkerFormatter.SFProcessType.ProcessAssembly: 
         wtBase = new CWorkerThreadAssembly(this, wfThread); 
         break; 
      default: 
         throw new Exception("Unbekannter Verarbeitungstyp"); 
   } 
   // Hinzufügen der Aufrufe zum Array 
   aThreads.Insert(idx, wtBase); 
} 


Sobald alle Objekte erstellt wurden, können sie durch Aufrufen der Start-Methode jedes Threadobjekts gestartet werden:

foreach(CWorkerThread cThread in aThreads) 
   cThread.Start();


Die Methoden Stop, Pause und Continue führen in jeder Foreach-Schleife ähnliche Operationen aus. Die Stop-Methode verfügt über die folgende Garbagecollectionoperation:

GC.SuppressFinalize(this);

Im Klassendestruktor wird die Stop-Methode aufgerufen, mit der die Objekte korrekt beendet werden, sofern nicht explizit die Stop-Methode aufgerufen wird. Wenn die Stop-Methode aufgerufen wird, wird der Destruktor nicht benötigt. Die SuppressFinalize-Methode verhindert, dass die Finalize-Methode des Objekts (die eigentliche Implementierung des Destruktors) aufgerufen wird.

Die abstrakte "CWorkerThread"-Klasse

CWorkerThread ist eine abstrakte Klasse, die von CWorkerThreadAppSpecific, CWorkerThreadRoundRobin und CWorkerThreadAssembly geerbt wird. Da unabhängig davon, wie die Nachricht verarbeitet wird, die Verarbeitung einer Warteschlange zu großen Teilen gleich ist, stellt die CWorkerThread-Klasse diese Funktionalität bereit. Die Klasse stellt abstrakte Methoden bereit, die außer Kraft gesetzt werden müssen, um Ressourcen zu verwalten und Nachrichten zu verarbeiten.

Die in der Klasse ausgeführte Arbeit wird wieder in den Methoden Start, Stop, Pause und Continue implementiert. Auf die Eingabe- und Fehlerwarteschlangen wird in der Start-Methode verwiesen. In .NET Framework wird das Messaging durch den System.Messaging-Namespace behandelt:

// Versuchen, die Warteschlange zu öffnen und die Standardlese- und 
// -Schreibeigenschaften festzulegen 
MessageQueue mqInput = new MessageQueue(sInputQueue); 
mqInput.MessageReadPropertyFilter.Body = true; 
mqInput.MessageReadPropertyFilter.AppSpecific = true; 
MessageQueue mqError = new MessageQueue(sErrorQueue); 
// Festlegen von ActiveX als Formatierer bei Verwendung von MSMQ COM 
mqInput.Formatter = new ActiveXMessageFormatter(); 
mqError.Formatter = new ActiveXMessageFormatter(); 


Wenn die Verweise der Nachrichtenwarteschlange definiert sind, wird ein Thread für die eigentliche Verarbeitungsfunktion erstellt, der ProcessMessages heißt. In .NET Framework kann Threading durch den System.Threading-Namespace erreicht werden:

procMessage = new Thread(new ThreadStart(ProcessMessages)); 
procMessage.Start(); 


Die ProcessMessages-Funktion ist eine auf einem booleschen Wert basierende Verarbeitungsschleife. Wenn dieser Wert False lautet, wird die Verarbeitungsschleife beendet. Aus diesem Grund legt die Stop-Methode des Threadobjekts lediglich diesen booleschen Wert fest, verknüpft den Thread mit dem Hauptthread und schließt geöffnete Nachrichtenwarteschlangen:

// Verknüpfen des Dienstthreads und Verarbeitungsthreads 
bRun = false; 
procMessage.Join(); 
// Schließen geöffneter Nachrichtenwarteschlangen 
mqInput.Close(); 
mqError.Close(); 


Die Pause-Methode legt nur einen booleschen Wert fest, durch den der Verarbeitungsthread eine halbe Sekunde aussetzt:

if (bPause)  
   Thread.Sleep(500); 


Die Methoden Start, Stop, Pause und Continue rufen jeweils die abstrakten Methoden OnStart, OnStop, OnPause und OnContinue auf. Diese abstrakten Methoden stellen die Hooks für implementierte Klassen bereit, um die erforderlichen Ressourcen zu erfassen und freizugeben.

Die ProcessMessages-Schleife hat die folgende Basisstruktur:

  • Empfangen eines Message-Objekts.

  • Aufrufen der abstrakten ProcessMessage-Methode, wenn die Receive-Methode für das Message-Objekt erfolgreich ist.

  • Senden des Message-Objekts an eine Fehlerwarteschlange, wenn Receive oder ProcessMessage fehlschlagen.

Message mInput; 
try 
{ 
   // Lesen aus der Warteschlange mit Wartezeit von 1 Sekunde 
   mInput = mqInput.Receive(new TimeSpan(0,0,0,1)); 
} 
catch (MessageQueueException mqe) 
{ 
   // Setzen der Nachricht auf Null (als nicht zu verarbeiten) 
   mInput = null; 
   // Prüfen des Fehlercodes und Überprüfen auf Zeitlimit 
   if (mqe.ErrorCode != (-1072824293) ) //0xC00E001B 
   { 
      // Ausgeben eines Fehlers und Protokollieren der Fehlernummer, wenn kein Zeitlimit vorliegt 
      LogError("Fehler : " + mqe.Message); 
      throw mqe; 
   } 
} 
if (mInput != null) 
{ 
   // Bei  zu verarbeitender Nachricht Aufrufen der abstrakten Methode für Nachrichtenverarbeitung 
   try 
   { 
      ProcessMessage(mInput); 
   } 
   // Erfassen des ausgegebenen Fehlers, wenn Ausnahmestatus bekannt 
   catch (CWorkerThreadException ex) 
   { 
      ProcessError(mInput, ex.Terminate); 
   } 
   // Erfassen unbekannter Ausnahme und Aufrufen der Terminate-Methode 
   catch 
   { 
      ProcessError(mInput, true); 
   } 
} 


Die ProcessError-Methode sendet die fehlerhafte Nachricht an die Fehlerwarteschlange. Außerdem wird für die unvorhergesehene Beendigung des Threads ggf. eine Ausnahme ausgegeben. Die Methode führt diese Aktion aus, wenn von der ProcessMessage-Methode ein Fehler beim Beenden oder der CWorkerThreadException-Typ ausgegeben werden.

Von "CWorkerThread" abgeleitete Klassen

Eine Klasse, die von CWorkerThread erbt, muss die Methoden OnStart, OnStop, OnPause, OnContinue und ProcessMessage bereitstellen. Die Methoden OnStart und OnStop rufen Verarbeitungsressourcen ab und geben sie frei. Mit den Methoden OnPause und OnContinue können diese Ressourcen vorübergehend freigegeben und neu abgerufen werden. Die ProcessMessage-Methode sollte eine Nachricht verarbeiten und eine CWorkerThreadException-Ausnahme ausgeben, wenn dabei ein Fehler auftritt.

Da der CWorkerThread-Konstruktor Laufzeitparameter definiert, müssen die abgeleiteten Klassen den Basisklassenkonstruktor aufrufen:

public CWorkerThreadDerived(CWorker v_cParent, WorkerThreadFormatter v_wfThread) 
   : base (v_cParent, v_wfThread) {} 


Abgeleitete Klassen werden für zwei Verarbeitungstypen bereitgestellt: Senden von Nachrichten an eine andere Warteschlange oder Aufrufen einer Komponentenmethode. Die beiden Implementierungen, die Nachrichten empfangen und senden, verwenden ein Round-Robin-Verfahren oder einen Anwendungsoffset, der in der AppSpecific-Eigenschaft der Nachricht steht und bestimmt, welche Warteschlange verwendet wird. Die Konfigurationsdatei in diesem Szenario sollte eine Liste mit Warteschlangenpfaden enthalten. Die implementierten Methoden OnStart und OnStop sollten einen Verweis auf diese Warteschlangen öffnen und schließen:

iQueues = wfThread.OutputName.Length; 
mqOutput = new MessageQueue[iQueues]; 
for (int idx=0; idx<iQueues; idx++) 
{ 
   mqOutput[idx] = new MessageQueue(wfThread.OutputName[idx]); 
   mqOutput[idx].Formatter = new ActiveXMessageFormatter(); 
} 


In diesen Szenarios ist die Nachrichtenverarbeitung einfach: Senden der Nachricht an die erforderliche Ausgabewarteschlange. Bei Round-Robin ist die Verarbeitung wie folgt:

try 
{ 
   mqOutput[iNextQueue].Send(v_mInput); 
} 
catch (Exception ex) 
{ 
   // Falls ein Fehler Beendigung der Ausnahme erzwingt 
   throw new CWorkerThreadException(ex.Message, true); 
} 
// Berechnen der nächsten Warteschlangennummer 
iNextQueue++; 
iNextQueue %= iQueues; 


Die zweite Implementierung (Aufrufen einer Komponente mit den Nachrichtenparametern) ist etwas interessanter. Die ProcessMessage-Methode ruft mit der IWebMessage-Schnittstelle eine .NET-Komponente auf. Die Methoden OnStart und OnStop rufen einen Verweis auf diese Komponente ab und geben ihn frei.

Die Konfigurationsdatei in diesem Szenario sollte zwei Befehle enthalten: den vollständigen Klassennamen und den Speicherort der Datei, in der sich die Klasse befindet. Die Process-Methode wird wie in der IWebMessage-Schnittstelle definiert auf der Komponente aufgerufen.

Zum Abrufen des Objektverweises wird die Activator.CreateInstance-Methode verwendet. Für die Funktion ist ein Baugruppentyp erforderlich, der in diesem Fall aus dem Dateipfad und Klassennamen der Baugruppe abgeleitet wird. Sobald ein Objektverweis abgerufen ist, wird er in die entsprechende Schnittstelle eingefügt:

private IWebMessage iwmSample; 
private string sFilePath, sTypeName; 
// Speichern des Baugruppenpfads und des Typnamens 
sFilePath = wfThread.OutputName[0]; 
sTypeName = wfThread.OutputName[1]; 
// Abrufen eines Verweises auf das erforderliche Objekt  
Assembly asmSample = Assembly.LoadFrom(sFilePath); 
Type typSample = asmSample.GetType(sTypeName); 
object objSample = Activator.CreateInstance(typSample); 
// Einfügen der erforderlichen Schnittstelle in das Objekt 
iwmSample = (IWebMessage)objSample; 


Wenn ein Objektverweis abgerufen wird, ruft die ProcessMessage-Methode die Process-Methode auf der IWebMessage-Schnittstelle auf:

WebMessageReturn wbrSample; 
try 
{ 
   // Definieren der Parameter für den Methodenaufruf 
   string sLabel = v_mInput.Label; 
   string sBody = (string)v_mInput.Body; 
   int iAppSpecific = v_mInput.AppSpecific; 
   // Aufrufen der Methode und Erfassen des Rückgabecodes 
   wbrSample = iwmSample.Process(sLabel, sBody, iAppSpecific); 
} 
catch (InvalidCastException ex) 
{ 
   // Falls beim Einfügen der Nachrichten ein Fehler auftritt, Erzwingen einer 
  //  nichtbeendenden Ausnahme 
   throw new CWorkerThreadException(ex.Message, false); 
} 
catch (Exception ex) 
{ 
   // Falls beim Aufrufen der Baugruppe ein Fehler auftritt, Erzwingen einer 
   // beendenden Ausnahme 
   throw new CWorkerThreadException(ex.Message, true); 
} 
// Falls kein Fehler auftritt, Überprüfen des Rückgabestatus des Objektaufrufs 
switch (wbrSample) 
{ 
   case WebMessageReturn.ReturnBad: 
      throw new CWorkerThreadException 
         ("Nachricht kann nicht verarbeitet werden: Nachricht als falsch gekennzeichnet", false); 
   case WebMessageReturn.ReturnAbort: 
      throw new CWorkerThreadException 
         ("Nachricht kann nicht verarbeitet werden: Prozess wird beendet...", true); 
   default: 
      break; 
} 


Die bereitgestellte Beispielkomponente schreibt den Nachrichtentext in eine Datenbanktabelle. Sie können die Verarbeitung abbrechen, wenn ein schwerwiegender Datenbankfehler festgestellt wird, die Nachricht aber ansonsten lediglich als fehlerhaft kennzeichnen.

Da die Instanz der für dieses Beispiel erstellten Klasse ggf. kostenaufwendige Datenbankressourcen abruft und besetzt, geben die Methoden OnPause und OnContinue den Objektverweis frei und rufen ihn erneut auf.

Instrumentierung

Wie in allen guten Anwendungen wird eine Instrumentierung bereitgestellt, um den Status der Anwendung zu überwachen. .NET Framework hat die Integration von Ereignisprotokollierung, Leistungszählern und Windows Management Instrumentation (WMI) in Anwendungen erheblich vereinfacht. Die Messaginganwendung verwendet Ereignisprotokollierung und Leistungszähler aus der System.Diagnostics-Baugruppe.

In der ServiceBase-Klasse können Sie die automatische Ereignisprotokollierung aktivieren. In dieser Klasse unterstützt das EventLog-Member Schreibvorgänge in das Ereignisprotokoll der Anwendung:

EventLog.WriteEntry(sMyMessage, EventLogEntryType.Information); 


Damit eine Anwendung in andere Ereignisprotokolle als das der Anwendung schreibt, kann sie wie in der CWorker-Klasse einfach einen Verweis auf eine EventLog-Quelle erstellen und abrufen und die WriteEntry-Methode zum Aufzeichnen von Protokolleinträgen verwenden:

private EventLog cLog; 
string sSource = ServiceControl.ServiceControlName; 
string sLog = "Anwendung"; 
// Überprüfen, ob Quelle vorhanden ist, ansonsten erstellen 
if (!EventLog.SourceExists(sSource)) 
   EventLog.CreateEventSource(sSource, sLog); 
// Erstellen des Protokollobjekts und Verweisen auf die jetzt definierte Quelle 
cLog = new EventLog(); 
cLog.Source = sSource; 
// Schreiben eines Eintrags zum Informieren über erfolgreiche Erstellung 
cLog.WriteEntry("Erfolgreich erstellt", EventLogEntryType.Information); 


Leistungszähler wurden durch .NET Framework erheblich vereinfacht. Diese Messaginganwendung stellt Zähler bereit, die die verarbeiteten Nachrichten (Gesamtanzahl und die Anzahl pro Sekunde) für jeden Verarbeitungsthread, den Worker, aus dem der Thread abgeleitet ist, und die gesamte Anwendung aufzeichnet. Wenn Sie diese Funktion bereitstellen möchten, müssen Sie die Leistungszählerkategorien definieren und dann die entsprechenden Zählerinstanzen inkrementieren.

In der OnStart-Dienstmethode werden Leistungszählerkategorien definiert. Diese Kategorien stellen zwei Zähler dar: einen für alle Nachrichten und einen für die pro Sekunde verarbeiteten Nachrichten:

CounterCreationData[] cdMessage = new CounterCreationData[2]; 
cdMessage[0] = new CounterCreationData("Nachrichten/Gesamt", "Insgesamt verarbeitete Nachrichten",  
PerformanceCounterType.NumberOfItems64); 
cdMessage[1] = new CounterCreationData("Nachrichten/Sekunde", "Pro Sekunde verarbeitete Nachrichten", 
PerformanceCounterType.RateOfChangePerSecond32); 
PerformanceCounterCategory.Create("MSDN Message Service", "MSDN Message Service-Zähler", cdMessage); 


Wenn die Leistungszählerkategorien definiert sind, wird für den Zugriff auf die Leistungszählerfunktion ein PerformanceCounter-Objekt erstellt. Für das PerformanceCounter-Objekt ist der Name der Kategorie und des Zählers und ein optionaler Instanzname erforderlich. Wenn Sie im Workerprozess den Prozessnamen aus der XML-Datei verwenden, lautet der Code:

pcMsgTotWorker = new PerformanceCounter("MSDN Message Service", "Nachrichten/Gesamt", sProcessName); 
pcMsgSecWorker = new PerformanceCounter("MSDN Message Service", "Nachrichten/Sekunde", sProcessName); 
pcMsgTotWorker.RawValue = 0; 
pcMsgSecWorker.RawValue = 0; 


Das Inkrementieren der Zähler lässt sich dann durch Aufrufen der geeigneten Methode durchführen:

pcMsgTotWorker.IncrementBy(1); pcMsgSecWorker.IncrementBy(1);

Wenn der Dienst angehalten wird, sollte die installierte Leistungszählerkategorie aus dem System gelöscht werden:

PerformanceCounterCategory.Delete("MSDN Message Service");


Damit Leistungszähler in .NET Framework funktionieren, muss ein bestimmter Dienst ausgeführt werden. Dieser Dienst, PerfCounterService, stellt gemeinsam genutzten Arbeitsspeicher bereit, in den die Zählerinformationen geschrieben werden und aus dem diese Informationen dann vom Leistungszählersystem gelesen werden.

Installation

Bevor wir zum Schluss kommen, sollten wir die Installation und ein Installationsdienstprogramm mit dem Namen installutil.exe erwähnen. Da diese Anwendung ein Windows-Dienst ist, muss sie mit installutil.exe installiert werden. Zur Vereinfachung der Installation ist eine Klasse erforderlich, die die Installer-Klasse aus der System.Configuration.Install-Baugruppe erbt:

public class ServiceRegister: Installer 
{ 
   private ServiceInstaller serviceInstaller; 
   private ServiceProcessInstaller processInstaller; 
   public ServiceRegister() 
   {       
      // Erstellen des Dienstinstallationsprogramms 
      serviceInstaller = new ServiceInstaller(); 
      serviceInstaller.StartType = ServiceStart.Manual; 
      serviceInstaller.ServiceName = ServiceControl.ServiceControlName; 
      serviceInstaller.DisplayName = ServiceControl.ServiceControlDesc; 
      Installers.Add(serviceInstaller); 
      // Erstellen des Prozessinstallationsprogramms 
      processInstaller = new ServiceProcessInstaller(); 
      processInstaller.RunUnderSystemAccount = true; 
      Installers.Add(processInstaller); 
   } 
} 


Wie diese Beispielklasse zeigt, ist bei einem Windows-Dienst für den Dienst und das Dienstprogramm je ein Installationsprogramm erforderlich, um das Konto zu definieren, unter dem der Dienst ausgeführt wird. Andere Installationsprogramme ermöglichen die Registrierung neuer Ressourcen, z.B. Ereignisprotokolle und Leistungszähler.

Schlussfolgerung

Wie aus dieser .NET Framework-Beispielanwendung ersichtlich, sind Aufgaben, die bisher im Verantwortungsbereich von Visual C++-Programmierern lagen, nun über ein einfaches objektorientiertes Programm durchführbar. Dieser Artikel bezieht sich zwar auf C#, der gesamte Code kann jedoch auch in Visual Basic und Managed C++ geschrieben werden. Durch das neue .NET Framework können Entwickler aus jeder Programmiersprache hochfunktionale, skalierbare Windows-Anwendungen und -Dienste erstellen.

Das neue .NET Framework hat nicht nur die Möglichkeiten der Programmierung vereinfacht und erweitert – häufig vergessene Tools der Anwendungsinstrumentierung wie Leistungszähler und Ereignisprotokollbenachrichtigungen können jetzt auch einfacher in die Anwendungen integriert werden. Dies gilt auch für Windows Management Instrumentation (WMI), obwohl es in dieser Anwendung nicht verwendet wird.

Referenzmaterial


Anzeigen: