Server-Check mit C#.NET

19.08.2003 von THOMAS WOELFER 
Mit C# und dem .NET-Framework lässt sich mit wenig Aufwand ein komplettes Tool zum Überwachen von Webservern programmieren. Diese müssen nicht unter Windows laufen, es funktioniert auch mit Apache und Linux.

Wer mehrere Webserver zu administrieren hat, kennt das Problem: Irgendwann bleibt immer einer der Server stehen. Sei es, weil der HTTP-Daemon schlicht und ergreifend keine Anfragen mehr beantwortet, da irgendwelche Logfiles übergelaufen sind oder weil ein anderer Defekt zum Ausfall des benötigten Prozesses oder Rechners geführt hat. Darum muss man die eigenen Server immer im Blick haben - zum Beispiel mit dem Statusprogramm aus diesem Beitrag.

Das im Folgenden entwickelte Beispielprogramm wird mit der .NET-Programmiersprache C# entwickelt. Dabei erfahren Sie nicht nur, wie man mit C# schnell hilfreiche Werkzeuge entwickelt, sondern lernen auch einige der nützlichen .NET-Kontrollelemente kennen. Zudem beschreiben wir, wie Sie Ihre C#-Programme mit Skins ausstatten können.

Das zu entwickelnde Programm soll im Normalfall einfach nur ein Icon in der Task-Leiste anzeigen: Ist das Icon grün, gibt es keine Probleme, wechselt es die Farbe und wird rot, gibt es bei mindestens einem der überprüften Webserver ein Problem. In diesem Fall kann man auf das Icon in der Statuszeile doppelklicken und erhält dann ein Fenster, in dem alle überwachten Server in tabellarischer Form angezeigt sind. Für jeden Server wird dabei der Hostname, die Domain, der Status in Form eines Fehlertextes sowie die Zeit des letzten Tests angezeigt. Außerdem hat jeder Server in der Tabelle nochmals ein Icon, das entweder grün oder rot ist: Server mit rotem Icon haben ein Problem.

Die Statusüberprüfung wird dabei mit Hilfe einer ASP-Seite bei ASP-Servern und mit Hilfe einer PHP-Seite bei PHP-Servern durchgeführt. Damit ein eventuell vorhandenes Caching den Statustest nicht durcheinander bringt, werden diese Seiten jeweils mit einem eindeutigen Parameter aufgerufen - der aktuellen "Tick"-Zahl am lokalen System. Das ASP- oder PHP-Script liest diesen Parameter aus und erhöht den übergebenen Wert um eins. Dieser wird dann zurückgeliefert und vom Programm ServerStatus ausgelesen. Dadurch kann das Programm erkennen, ob der Webserver antwortet oder irgendein Proxy.

Server-Status mit C# für ASP oder PHP testen

Antwortet der Server nicht oder liefert er ein falsches Ergebnis, wird sein Status auf "fehlerhaft" gesetzt und dadurch das rote Icon in der Statuszeile angezeigt. Die Tests finden in einem festen zeitlichen Abstand statt, der im Quellcode festgelegt ist. Hier bietet es sich an, das Beispielprogramm ein wenig zu erweitern, etwa um eine Dialogbox zum Einstellen der Optionen wie dem Update-Intervall.

Schließlich verfügt das Programm noch über eine Dialogbox, mit der Sie neue Server in die Liste der zu testenden Geräte aufnehmen können. Dazu ist der komplette Hostname anzugeben. Alle definierten Server werden in einer Liste abgelegt, die beim Beenden des Programms gespeichert wird. Dadurch steht sie beim nächsten Programmstart wieder zur Verfügung.

So weit zur Beschreibung der gewünschten Funktionalität, nun zur Implementierung. Diese erfolgt wie angekündigt so, dass Sie das Programm mit einem eigenen Skin ausstatten können. Das Programm wird also nicht wie ein normales Windows-Programm aussehen, sondern ein ganz eigenes Aussehen haben. Die Art des Aussehens können Sie dabei selbst festlegen, denn es wird einfach per Bitmap festgelegt.

Projektstart: Visual Studio anwerfen

Zunächst benötigen Sie ein neues C#-Projekt vom Typ "Windows Application", das Sie einfach mit Visual Studio anlegen.

Die Informationen über die einzelnen Server sind in einer eigenen Klasse mit dem Namen Server abgelegt. Da diese Informationen auf der Festplatte gespeichert werden sollen, muss diese Klasse als Serializable markiert werden:

[Serializable]
public class Server
{
}

Die einzelnen Server-Objekte werden später zur Laufzeit mit einer eigenen Dialogbox erzeugt. Dazu braucht es einen Konstruktor, dem man den Hostname und eine Information über die Art des Servers übergeben kann.

public Server( string name, bool isPhp)
{
this.servername = name;
this.status = false;
this.checktime = DateTime.Now;
this.isPhp = isPhp;
this.error = "Kein Fehler";
}

Der Konstruktor speichert diese Angaben in privaten Variablen der Klasse und initialisiert bei dieser Gelegenheit noch alle anderen. Dabei gibt es die Variable status, die den Status des Servers enthält. Nimmt die Variable den Wert false an, dann ist mit dem Server etwas nicht in Ordnung, bei true gibt es keine Probleme.

Ferner legen wir in der Variable checktime die Uhrzeit des letzten Server-Tests ab. Diese wird später im Userinterface angezeigt. Schließlich gibt es noch die Variable error, die den Fehlertext enthält, wenn der Status des Servers nicht in Ordnung ist.

Damit Sie die einzelnen Informationen später aus einem Server-Objekt ermitteln können, statten Sie alle privaten Variablen noch mit einem Property-Accessor aus. Dabei reichen in diesem Fall get-Accessors aus, denn alle Zustände des Server-Objekts werden durch Methoden des Objekts selbst und nicht von außen gesetzt.

Der Property-Accessor für den Server-Status hat also das folgende Aussehen:

/// <summary>
/// Erfragt ob der Server-Status ok ist.
/// </summary>
public bool Status
{
get
{
return status;
}
}

Auch für error, CheckTime und den Server-Namen definieren Sie noch get-Accessors. Diese haben aber den gleichen Aufbau wie der Accessor für die Statuseigenschaft und werden daher hier nicht weiter besprochen.

Konkret werden: Tests durchführen

Damit ist das Server-Objekt schon so gut wie fertig. Es fehlt aber noch eine Methode, die den tatsächlichen Test des Servers durchführt. Diese Methode trägt den Namen Check und hat die folgende Implementierung:

public void Check()
{
byte[] buffer = null;
string download = null;
Cursor.Current = Cursors.WaitCursor;
long lTicks = DateTime.Now.Second;
string ticks = lTicks.ToString();
checktime = DateTime.Now;

Zunächst initialisiert die Routine einen Byte-Puffer auf Null. Dieses Pufferobjekt brauchen Sie später bei der Abfrage des Webservers. Ferner wird der Cursor auf die Variante WaitCursor gesetzt, damit der Mauszeiger während des Server-Tests eine Form bekommt, die einen längerfristigen Vorgang signalisiert. Das ist sinnvoll, weil der zu untersuchende Server unter Umständen nicht sofort antwortet und es daher notwendig sein kann, etwas länger auf eine Antwort zu warten. Außerdem wird eine Variable vom Typ Long mit der aktuellen Uhrzeit in Sekunden gefüllt und zusätzlich als String gespeichert. Dieser String wird dann wie beschrieben an das ASP- oder PHP-Script übergeben.

Zuletzt merkt sich die Routine noch die aktuelle Uhrzeit - auch diese wird später für die Anzeige der Server-Liste benötigt.

PHP oder ASP: Scripts anfordern

Nun kommt der interessantere Teil der Check-Routine: Das Programm führt einen Request durch und fragt den zu testenden Webserver nach dem Script servertest. Das Script hat entweder die Erweiterung .asp oder .php und wird immer im Basisverzeichnis des Webservers erwartet. Mit einer eigenen Erweiterung könnten Sie diesen Umstand natürlich verändern:

try
{
WebClient wc = new WebClient();
if( isPhp) buffer = wc.DownloadData( "http://" + Name + "//serverstatus.php?p=" + ticks);
else buffer = wc.DownloadData( "http://" + Name + "//serverstatus.asp?p=" + ticks);
download = Encoding.ASCII.GetString( buffer);
}
catch( Exception ex)
{
Cursor.Current = Cursors.Default;
status = false;
error = ex.Message;
return;
}

Die Anfrage an den Webserver ist in einen Try/Catch-Block eingepackt. Tritt eine Exception auf, wird diese "gefangen" und das Server-Objekt auf den Status false gesetzt. Eine Exception bedeutet nämlich in diesem Zusammenhang ein Problem mit dem Server - zum Beispiel könnte ein Timeout aufgetreten sein, weil der Server nicht geantwortet hat.

Der Grund für den Fehler wird in error gespeichert - er liegt als Teil des Exception-Objekts ex vor und lässt sich daher leicht ermitteln.

CGI-Parameter nutzen, Caching umgehen

Ging die Anfrage problemlos vonstatten, ist zumindest eine Antwortseite vom Server geliefert worden. Das Programm kann zunächst normal weiterarbeiten. Man muss allerdings überprüfen, ob der Webserver auch den richtigen Text geliefert hat: Steht der Server beispielsweise hinter einem cachenden Proxy, könnte dieser Proxy durchaus eine Seite liefern, obwohl der Webserver selbst gar nicht mehr läuft.

Dafür verwendet das Programm den zuvor ermittelten "ticks"-Wert. Dabei wird davon ausgegangen, dass das PHP- oder ASP-Script auf dem Server den übergebenen Wert ausgelesen, um eins hochgezählt und dann zurückgeliefert hat. Das wird überprüft:

long rTicks = 0;
try
{
rTicks = long.Parse( download);
}
catch( Exception)
{
}

if( rTicks != lTicks+1)
{
status = false;
error = "Falsche Antwort";
}
else
{
status = true;
error = "Kein Fehler";
}

Entspricht der gelieferte Wert dem erwarteten, setzt die Routine den Zustand des Server-Objekts auf true. Im anderen Fall liegt ein Fehler vor, und das Server-Objekt erhält den Zustand false.

Das Script auf dem Server

Damit der Server in der Lage ist, die richtigen Werte zu liefern, benötigen Sie auch auf dem Server ein Script - serverstatus.asp beziehungsweise serverstatus.php eben. Das ist jedoch leicht gebaut. In PHP sieht das folgendermaßen aus:

<?php
$i = $p;
$i = $i + 1;
print $i;
?>

Die ASP-Variante dieses Scripts ist auch nicht weiter aufwendig:

<%
i = Request("p");
i = i + 1;
Response.Write( i);
%>

Damit ist das Server-Objekt eigentlich geklärt: Alle benötigten Funktionen sind vorhanden. Erwähnenswert ist vielleicht noch die Tatsache, dass Sie für das Server-Objekt keinerlei Code zum Speichern programmieren mussten, obwohl das ja eigentlich als Feature festgelegt war: Die Vergabe des Attributs Serializable reicht völlig aus, um das Objekt später speichern zu können. Die Details des Speicher- und Ladevorgangs und auch das tatsächliche Schreiben der Daten des Objekts übernimmt das Framework.

Das Hauptfenster: Windows Forms mit C#

Nun geht es an die Programmierung des Hauptfensters. Dazu können Sie einfach das Form verwenden, das vom Developer Studio per Default beim Erstellen des Projekts angelegt wurde.

Das Hauptfenster soll mit einem Skin ausgestattet werden, und das ist der erste Arbeitsschritt, denn der Rest der Arbeit wird durch die Anwesenheit des Skins beeinflusst.

Um ein Windows-Forms-Programm mit einem eigenen Skin auszustatten, ist nicht sonderlich viel zu tun: Man braucht eine Bitmap und muss im Form zwei Eigenschaften über den Forms-Editor setzen.

Das Bitmap dient dabei für den Skin: Es definiert das spätere Aussehen des Fensters. Sie malen also einfach ein Bild, das das Aussehen des Programmfensters widerspiegelt. Bereiche, die später durchsichtig sein sollen, füllen Sie dabei in einer beliebigen, aber gleich bleibenden Farbe. Wenn Sie beispielsweise einen gefüllten Kreis in eine rechteckige Bitmap malen und den Außenbereich mit grün füllen, dann können Sie hinterher ein rundes Fenster erhalten, bei dem der grüne Bereich komplett unsichtbar ist.

Im Forms-Editor legen Sie dann die Bitmap als Hintergrundbild fest. In der Eigenschaft Transparency Key stellen Sie dann die Farbe ein, die Sie als durchsichtige Farbe verwendet haben, und das war es auch schon. Jetzt hat Ihr Fenster ein eigenes Skin und hebt sich deutlich von normalen Windows-Fenstern ab.

Allerdings hat das Fenster noch einen ganz normalen Windows-Fensterrand. Den werden Sie los, indem Sie die Eigenschaft FormBorderStyle auf None stellen - dann ist wirklich nur noch der Teil der Bitmap sichtbar, der nicht in der unsichtbaren Farbe gefüllt wurde.

Fenster ohne Rahmen bewegen

Daraus resultiert aber ein kleines Problem: Weil Sie nun keinen Fensterrahmen mehr haben, können Sie das Fenster auch nicht mehr verschieben: Das ist ziemlich unhandlich. Diese Schwierigkeit ist aber leicht zu lösen, denn Sie brauchen bloß ein wenig Code, der auf die zugehörigen Mausereignisse reagiert.

Dazu legen Sie für die Events MouseDown und MouseMove zwei neue Eventhandler an. Diese verwenden eine gemeinsame Variable, in der die aktuelle Position mitgeführt wird:

private Point mouse_offset;
private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
mouse_offset = new Point(-e.X, -e.Y);
}

private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Point mousePos = Control.MousePosition;
mousePos.Offset(mouse_offset.X, mouse_offset.Y);
Location = mousePos;
}
}

Mit diesem Code können Sie nun irgendwo mit der linken Maustaste in das Fenster klicken und das Fenster dann bei gedrückter Maus am Bildschirm bewegen. Ein Fensterrahmen oder die Titelleiste sind nicht mehr notwendig.

Nun platzieren Sie die Kontrollelemente auf dem Form. Sie benötigen ein ListView-Control und drei Buttons. Die Liste soll die verwalteten Server enthalten, die Buttons dienen dem Aufruf von verschiedenen Funktionen.

Damit die Buttons zum Skin passen, verwenden Sie aber keine normalen Button-Controls, sondern Picturebox-Controls. Diese belegen Sie mit Bitmaps ganz nach Wunsch und passend zur Optik. Im Beispiel erfüllen die drei Buttons folgende Funktionen: Beenden des Programms, Minimieren des Programms und Hinzufügen eines neuen Servers zur Liste. Der Code steckt in Handlern für das Event ButtonClick der zugehörigen Picturebox. Für das Minimieren und Beenden des Programms wird nicht viel Code benötigt:

private void pictureBox1_Click(object sender, System.EventArgs e)
{
this.Close();
}

private void pictureBox2_Click(object sender, System.EventArgs e)
{
this.Hide();
}

Der Code zum Hinzufügen eines neuen Servers öffnet ein weiteres Form, in dem der Benutzer die benötigten Informationen über den neuen Server eingibt. Diese werden dann in der Serverliste abgelegt und gespeichert.

Server-Daten speichern: .NET-Serialisierung macht es einfach

Der Code zum Öffnen der Dialogbox ist uninteressant und wird daher hier nicht weiter besprochen. Interessant ist aber der Code, mit dem die Liste aus Server-Objekten gespeichert wird:

string p = Application.LocalUserAppDataPath + "\\\\servers.data";
Stream file = File.Open( p, FileMode.Create);
IFormatter formatter = new SoapFormatter();
formatter.Serialize(file, servers);
file.Close();

Hier wird zunächst der Pfad zu einer Datei zusammengesetzt. In dieser Datei mit dem Namen servers.dat sollen die Informationen über die zu überwachenden Server abgelegt werden. Beim Programmstart werden von hier wieder die Daten eingeladen.

Dazu verwenden Sie das Objekt Application aus dem Namespace Windows.Forms. Dieses Objekt hat eine Menge interessanter Informationen zu bieten, unter anderem auch den Pfad zum lokalen Verzeichnis für Daten des momentan angemeldeten Users: LocalUserAppDataPath.

Über diesen Pfad kann einfach eine Datei geöffnet werden. Danach wird ein SoapFormatter-Objekt für die Speicherung der Daten erzeugt: Der Formatter serialisiert die Daten aus der Server-Liste in die angegebene Datei.

Bei der Serialisierung von Daten in .NET sind Sie übrigens nicht auf den SoapFormatter beschränkt. Wenn Sie statt standardkonformen Daten mehr Augenmerk auf die Performance der Serialisierung legen, können Sie stattdessen auch einen BinaryFormatter verwenden.

Server-Status anzeigen

Jedesmal, wenn der Server-Status überprüft wurde, ist die Anzeige der Server-Informationen zu erneuern. Dazu braucht es eine eigene Funktion. Diese arbeitet eng mit dem Listview-Control zusammen, das Sie auf dem Form platziert haben:

private void UpdateList()
{
this.listView1.Items.Clear();
foreach( ServerStatus.Server s in servers)
{
ListViewItem item = new ListViewItem( new string[]{ s.Name, s.Error, s.Checktime });
item.Tag = s;
if( s.Status) item.ImageIndex = 0;
else item.ImageIndex = 1;
this.listView1.Items.Add( item);
}
}

Zunächst entfernen Sie mit Items.Clear() die bisher angezeigten Elemente aus der Listview. Danach iterieren Sie über die Liste der Server-Objekte und legen für jedes ein neues ListView-Item an. Dieses bestücken Sie mit einem Array aus Strings, das folgende Informationen enthält: den Namen des Servers, den Fehlertext und die Uhrzeit des letzten Tests. Damit diese Texte auch zu sehen sind, müssen Sie der ListView im Forms-Editor drei Spalten zuweisen und das ListView-Control in der Details-Ansicht anzeigen lassen.

Im Member Tag des ListView-Items merken Sie sich außerdem noch das Server-Objekt, das dieses Item anzeigt. Das brauchen Sie später für eine andere Funktion.

Dann setzen Sie den ImageIndex des Items, um das anzuzeigende Icon für den betroffenen Server festzulegen. Damit das auch klappt, müssen Sie im Forms-Editor ein ImageList-Control auf das Form ziehen und der ListView diese ImageList als Eigenschaft zuweisen. In der ImageList platzieren Sie zwei Icons: eines für den Zustand "OK" und ein weiteres für den Fehlerzustand. Das erste Icon bekommt den Index 0, das zweite den Index 1. Genau dieses Indizes verwenden Sie dann für das ListView-Element, und zwar abhängig vom Zustand des Servers. Schließlich fügen Sie das Item noch zur Sammlung der Items in der ListView hinzu.

Das Programm soll auch in der Notification-Area der Task-Leiste den Server-Zustand signalisieren können. Das geht am einfachsten, indem Sie ein NotifyIcon-Control auf das Form ziehen. Per Default weisen Sie diesem Icon ein Icon für den Zustand "OK" zu. Dieses Icon ändern Sie dann später nach dem ersten Server-Test.

Regelmäßige Updates: TimeControl verwenden

Damit das Programm überhaupt Sinn macht, muss der Server-Test regelmäßig durchgeführt werden. Das erledigen Sie mit einem TimeControl, das Sie ebenfalls auf das Form ziehen. Dieses Control hat ein Event Tick, für das Sie einen Handler definieren. Letzterer ist für die regelmäßige Durchführung des Server-Tests zuständig.

private void OnTick(object sender, System.EventArgs e)
{
bool fError = false;
if( servers != null)
{
foreach( ServerStatus.Server s in servers)
{
s.Check();
if( ! s.Status)
{
this.notifyIcon1.Icon = iconError;
fError = true;
this.notifyIcon1.Text = s.Error;
}
}
}

if( ! fError)
{
this.notifyIcon1.Icon = this.Icon;
this.notifyIcon1.Text = "Letzter Test " + DateTime.Now.ToString();
}
UpdateList();
}

Zunächst iteriert die Routine über alle Server-Objekte aus der Liste und führt für jeden Server den Check durch. Signalisiert der Server einen Fehler, dann ändert sie das Icon des NotifyIcon-Controls und setzt dessen Tooltip-Text auf den Fehlertext. Außerdem speichert sie in einer zusätzlichen Variable, ob ein Fehler aufgetreten ist.

Sind alle Server abgearbeitet, so überprüft sie, ob ein Fehler gefunden wurde. Ist das nicht der Fall, setzt die Routine das NotifyIcon-Element auf den Zustand "OK" zurück, indem sie das passende Icon und einen entsprechenden Text zuweist. Schließlich führen sie noch ein Update des ListView-Controls durch, damit die aktuellen Informationen über alle Server angezeigt werden.

Es fehlt nun nur noch eine Möglichkeit, einen Server aus der Liste zu entfernen. Dazu verwenden Sie ein ContextMenu-Objekt, das Sie mit nur einem Befehl ausstatten. Dieser sorgt für das Löschen des angeklickten Servers aus der Liste. Im Eventhandler zu diesem Befehl verwenden Sie das zuvor im Tag des ListView-Items gespeicherte Server-Objekt.

if( this.listView1.SelectedItems.Count > 0)
{
servers.Remove( this.listView1.SelectedItems[0].Tag);
Store();
UpdateList();
}

Dazu ermitteln Sie das momentan selektierte Item anhand der Eigenschaft SelectedItems. Ist der Server aus der Liste entfernt, speichern Sie die Liste einfach erneut ab und verwenden UpdateList(), um die Anzeige der Listview zu erneuern.

Form initialisieren: Event FormLoad

Schließlich muss das Form noch initialisiert werden. Das geschieht am einfachsten im Handler für das Event FormLoad.

string p = Application.LocalUserAppDataPath + "\\\\servers.data";
if( File.Exists( p))
{
Stream file = File.Open( p, FileMode.Open);
IFormatter formatter = new SoapFormatter();
servers = formatter.Deserialize(file) as ArrayList;
file.Close();
UpdateList();
}

Viel ist allerdings nicht zu initialisieren: Im Wesentlichen laden Sie die Datei mit der Server-Liste mit Hilfe eines SoapFormatter. Das ist einfach der umgekehrte Vorgang zur Serialisierung, die Sie in der Methode Store() implementiert haben.

In diesem Beitrag haben Sie erfahren, wie einfach Sie mit .NET und C# ein kleines administratives Tool für die Überwachung von Webservern programmieren können. Dabei sind Sie natürlich nicht auf Webserver beschränkt - das Tool kann leicht auf die Überwachung von Mailservern oder anderen Rechnern erweitert werden. (mha)