communityWir suchen ständig neue Tutorials und Artikel! Habt ihr selbst schonmal einen Artikel verfasst und seid bereit dieses Wissen mit der Community zu teilen? Oder würdet ihr gerne einmal über ein Thema schreiben das euch besonders auf dem Herzen liegt? Dann habt ihr nun die Gelegenheit eure Arbeit zu veröffentlichen und den Ruhm dafür zu ernten. Schreibt uns einfach eine Nachricht mit dem Betreff „Community Articles“ und helft mit das Angebot an guten Artikeln zu vergrößern. Als Autor werdet ihr für den internen Bereich freigeschaltet und könnt dort eurer literarischen Ader freien Lauf lassen.

Client-Server-Modell PDF Drucken E-Mail
Benutzerbewertung: / 65
SchwachPerfekt 
Dienstag, den 24. August 2010 um 11:20 Uhr
Beitragsseiten
Client-Server-Modell
Ereignisorientierte Programmierung
Alle Seiten

Einführung

Dieser Artikel basiert im Wesentlichen auf dem Tutorial „TCP/IP Socket-Programmierung in C#“ und setzt die dort vermittelten Kenntnisse in ein fortschrittliches Client-Server-Modell um. Ziel ist es mithilfe eines Clienten Daten von einem entfernten Server abfragen zu können. Der Server soll diese Daten an einen oder mehrere Clienten gleichzeitig senden können und somit asynchron arbeiten. Die Daten liegen als XML-Dokument vor und werden vom Server nur bei Bedarf „on the fly“ übermittelt. Damit wird zum einen gezeigt, wie sich die Belastung für den Server im Upstream senken lässt und zum anderen eröffnet es die Möglichkeit die Funktion von einfachen Netzwerkprotokollen zu demonstrieren. Netzwerkprotokolle sind exakte Vereinbarungen, nach der Daten zwischen Computern bzw. Prozessen ausgetauscht werden, die durch ein Netz miteinander verbunden sind. Chatprogramme und auch Browser greifen auf Protokolle zurück, um im Internet zu kommunizieren.

Als Beispiel wurden in diesem Artikel Kontaktdaten von natürlichen Personen gewählt. Diese Daten werden in einem XML-Dokument gespeichert und vom Server an die Clienten übertragen. Da der Server im Zentrum des Netzwerks steht, kann er Ressourcen verwalten, die allen Usern gemeinsam sind, um Probleme der Redundanz und Widersprüchlichkeit zu vermeiden. Die Clienten können die Kontaktliste vom Server empfangen und auch mit verschiedenen Befehlen detaillierte Daten aus der Kontaktliste abfragen.

Das Projekt wurde mit Microsoft Visual Studio und dem .NET Framework 4.0 realisiert.

client-server-model-xml

XML

XML ist eine Auszeichnungssprache zur Darstellung hierarchisch strukturierter Daten in Form von Textdaten. XML wird u. a. für den plattform- und implementationsunabhängigen Austausch von Daten zwischen Computersystemen eingesetzt, insbesondere über das Internet. Die Extensible Markup Language wurde in einem Standard vom World Wide Web Consortium (W3C) spezifiziert.

Die vom W3C herausgegebene XML-Spezifikation (Recommendation, erste Ausgabe vom 10. Februar 1998, aktuell ist die fünfte Ausgabe vom 26. November 2008) definiert dabei eine Metasprache, auf deren Basis durch strukturelle und inhaltliche Einschränkungen anwendungsspezifische Sprachen definiert werden. Diese Einschränkungen werden durch Schemasprachen wie DTD oder XML Schema ausgedrückt. Beispiele für XML-Sprachen sind: RSS, MathML, GraphML, XHTML, XAML, Scalable Vector Graphics (SVG), aber auch XML-Schema.

Die Webtechnologie Ajax ist heute im Internet oft anzutreffen und basiert ebenfalls auf XML. Ajax ist ein Apronym für die Wortfolge „Asynchronous JavaScript and XML“. Es bezeichnet ein Konzept der asynchronen Datenübertragung zwischen einem Browser und dem Server. Dieses ermöglicht es, HTTP-Anfragen durchzuführen, während eine HTML-Seite angezeigt wird, und die Seite zu verändern, ohne sie komplett neu zu laden.

Kontaktliste

In dem Client-Server-Modell soll der entfernte Server Kontaktdaten übertragen, die in einem XML-Dokument lokal abgespeichert sind. In diesem Dokument kann eine Stelle zu beliebigen Zeitpunkten Kontakte hinterlegen und die Clienten können anschließend diese Kontakte von überall auf der Welt abfragen. Wichtig für die Übertragung der Daten ist das sowohl Server als auch Client wissen welche Daten überhaupt vorhanden sind. Die Beschreibung der Kontaktliste mit den Elementen ist daher der erste logische Schritt bei dem Entwurf der Applikation.

Typisch für ein XML-Dokument ist das es aus Textzeichen besteht, im einfachsten Fall ASCII, so dass es damit menschenlesbar – Binärdaten enthält es per Definition nicht - wird. XML-Dokumente besitzen einen physischen und einen logischen Aufbau. Der logische Aufbau entspricht einer Baumstruktur und ist damit hierarchisch organisiert. Als Baumknoten gibt es Elemente, deren physische Auszeichnung mittels einem passenden Paar aus Start-Tag (<Tag-Name>) und End-Tag (</Tag-Name>) erfolgen kann, aus Attributen, aus Verarbeitungsanweisungen (<?Ziel-Name Parameter ?>, engl. Processing Instruction), aus Kommentaren (<!-- Kommentar-Text -->) und aus Text.

Ein einzelner Kontakt in dem Programm besteht aus der Identifikationsnummer des Mitarbeiters, kurz Id, dem Namen, der Firma bei der der Mitarbeiter angestellt ist, seiner Adresse, der E-Mail, der Telefon-, Fax- und Handynummer, der Anrede, dem Titel und der Position innerhalb des Unternehmens. Nachfolgend ist so eine Kontaktliste mit drei Beispielkontakten aufgelistet.

<Contacts xmlns="http://tempuri.org/Contacts.xsd">
    <Kontakte>
        <Id>1</Id>
        <Name>Stefan Müller</Name>
        <Firma>Lufthansa AG</Firma>
        <Adresse>Elfenbeinstrasse 25, 80935 München</Adresse>
        <Email>stefanmueller@mail.de</Email>
        <Telefon>98362550</Telefon>
        <Handy>01746352423</Handy>
        <Fax>98362551</Fax>
        <Anrede>Herr</Anrede>
        <Titel>Dipl.-Ing. (FH)</Titel>
        <Position>Abteilungsleiter</Position>
    </Kontakte>
    <Kontakte>
        <Id>2</Id>
        <Name>Barbara Bauer</Name>
        <Firma>BMW</Firma>
        <Adresse>Burgstrasse 2, 14935 Berlin</Adresse>
        <Email>barbara@bmw.de</Email>
        <Telefon>82272550</Telefon>
        <Handy>017463764254</Handy>
        <Fax>0892272573</Fax>
        <Anrede>Frau</Anrede>
        <Titel>Dr.</Titel>
        <Position>Mitarbeiterin</Position>
    </Kontakte>
    <Kontakte>
        <Id>3</Id>
        <Name>John Doe</Name>
        <Firma>Doe Inc.</Firma>
        <Adresse>Evergreen 345, 10101 New York</Adresse>
        <Email>john@doe.de</Email>
        <Telefon>10001</Telefon>
        <Handy>10002</Handy>
        <Fax>10003</Fax>
        <Anrede>Herr</Anrede>
        <Titel>Prof.</Titel>
        <Position>Dozent</Position>
    </Kontakte>
</Contacts>

Damit wären die Daten beschrieben, die im Client-Server-Modell ausgetauscht werden sollen.

Netzwerkprotokoll

Ein Netzwerkprotokoll im Prinzip eine Vereinbarung aus einem Satz von Regeln und Formaten (Syntax), die das Kommunikationsverhalten der kommunizierenden Instanzen in den Computern bestimmen (Semantik). Das Internet basiert auf dem OSI-Schichtenmodell, das die Aufgaben der Kommunikation in sieben aufeinander aufbauende Schichten unterteilt. Für jede Schicht existiert eine Beschreibung, in welcher steht, was diese Schicht zu leisten hat. Diese Anforderungen müssen von den Kommunikationsprotokollen realisiert werden. Die konkrete Umsetzung wird dabei nicht vorgegeben und kann daher sehr unterschiedlich sein. Somit existieren mittlerweile für jede der sieben Schichten zahlreiche solcher Protokolle.

Das hier vorgestellte Client-Server-Modell nutzt das Transmission Control Protocol (TCP) für den Datenaustausch als Transportprotokoll. TCP ist Bestandteil der Transportschicht im OSI-Schichtenmodell. Anwendungsprogramme nutzen Transportprotokolle und definieren zusätzlich ein eigenes Protokoll, das in der sogenannten Anwendungsschicht verankert ist. So muss das Programm beim Datenaustausch die über TCP gesendeten Daten interpretieren können. Das Hypertext Transfer Protocol ist ein bekanntes Protokoll aus der Anwendungsschicht und wird von allen Browsern unterstützt. Alle Programme im Internet basieren auf dem Internet Protocol. In diesem Artikel wird das neue Internet Protocol Version 6 (IPv6) genutzt.

Die Entwicklung von eigenen Kommunikationsprotokollen kann sehr komplex und zeitaufwändig sein. Instant Messenger, wie ICQ oder Skype, basieren auf großen Protokollen, wie dem OSCAR-Protokoll (Open System for Communication in Realtime). OSCAR, welches von AOL urspründlich für den AOL Instant Messenger (AIM) entwickelt wurde und nach der Übernahme von ICQ für beide Messenger verwendet wird, arbeitet ebenfalls auf einer bestehenden TCP-Verbindung.

In .NET bietet sich für die Erstellung des Protokolls eine eigene Bibliothek an, die anschließend von Server und Client gleichermaßen verwendet werden kann. Dadurch ist eine Codeduplizierung ausgeschlossen und das Anwendungsprotokoll kann separat weiterentwickelt und verändert werden. Dabei muss immer berücksichtigt werden das jede signifikante Veränderung des Anwendungsprotokolls dazu führt, dass Programme, die auf diesem Protokoll basieren nicht mehr richtig funktionieren. Auch deshalb ist der erste Entwurf des Anwendungsprotokolls von elementarer Bedeutung.

Nachrichtentypen definieren

Vor Beginn der eigentlichen Programmierung müssen die Regeln für das Protokoll festgelegt werden. Welche Nachrichten soll der Client mit dem Server austauschen können und wie müssen die Nachrichten strukturiert sein, um von beiden Seiten richtig interpretiert und auch generiert werden zu können. Triviale Fragen, wie zum Beispiel der Verbindungsauf- und -abbau gehören in diesen Themenbereich.

In einer eigenen Klasse mit dem Namen PacketType legen wir deshalb zuerst eine Enumeration an. Der Aufzählungstyp soll die verschiedenen Nachrichtentypen beherbergen.
Als nächstes ist zu ermitteln, welche Pakete überhaupt in Frage kommen, die anschließend gesendet und empfangen werden sollen. Der Client soll beispielsweise die Kontaktliste abfragen können, dazu wird der Aufzählungstyp ContactListRequest definiert. Weiterhin soll ein Client in der Lage sein sich bei dem Server an- und abzumelden, einzelne Kontakte sollen abfragbar sein und der Server soll überprüfen können, ob ein bestimmter Client in einem IP-Netzwerk erreichbar ist und welche Zeit das Routing zu diesem in Anspruch nimmt.

/// <summary>
/// Stores all possible packet types in the protocol.
/// </summary>
public enum PacketType
{
    /// <summary>
    /// Client asked for a partial contact list. 
    /// </summary>
    ContactListRequest = 1,
    /// <summary>
    /// Client asked for a specific contact. 
    /// </summary>
    ContactRequest,
    /// <summary>
    /// Acknowledged client reply for contact list. 
    /// </summary>
    ContactListReply,
    /// <summary>
    /// Acknowledged client reply for specific contact. 
    /// </summary>
    ContactReply,
    /// <summary>
    /// Perform logout.
    /// </summary>
    ClientExit,
    /// <summary>
    /// A simple text message.
    /// </summary>
    Message,
    /// <summary>
    /// Broadcast a message to all clients.
    /// </summary>
    Broadcast,
    /// <summary>
    /// A login message.
    /// </summary>
    ClientLoginInformation,
    /// <summary>
    /// Client gone.
    /// </summary>
    ClientLogOffInformation,
    /// <summary>
    /// Request a full client list.
    /// </summary>
    SendClientList,
    /// <summary>
    /// Routes a message to another address.
    /// </summary>
    Route,
    /// <summary>
    /// A simple ping.
    /// </summary>
    Ping
}

Nachricht entwerfen

Pakete spielen im Internet eine zentrale Rolle. Das OSI-Referenzmodell ist ein Protokollstapel bestehend aus fortlaufend nummerierte Schichten. Jede Schicht benutzt dabei zur Erfüllung ihrer speziellen Aufgabe die jeweils tiefere Schicht im Protokollstapel.

Daten, die über ein Netz übertragen werden, werden von einem Netzprotokoll des Stapels nach dem anderen verarbeitet. Jedes Netzprotokoll entfernt beim Empfang aus den Daten diejenigen Steuerinformationen, die nur für dieses Protokoll selbst bestimmt sind, und übergibt die verbliebenen Daten dem nächsthöheren Netzprotokoll. In der Senderichtung werden die Steuerinformationen hinzugefügt, bevor sie dem nächsttieferen Netzprotokoll übergeben werden - eine Nachricht trägt also auf der Leitung sämtliche Header der darüberliegenden Schichten. Eine Nachricht, die via Ethernet versandt wird, lässt sich wie folgt veranschaulichen:

Ethernet-Frame
IP-Paket
TCP-Segment
Nachricht

Beim Verschicken einer Nachricht über TCP, wird die Nachricht in einem TCP-Segment verpackt, welches wiederum in einem IP-Paket verpackt wird, bevor es die Reise durch das Internet antreten kann. Die Struktur eines Protokollstapels ähnelt dem Aufbau der Materie im Universum. Körper bestehen aus vielen verschiedenen Atomen, deren Kerne Protonen und Neutronen beinhalten, die ihrerseits wieder aus up- und down-Quarks zusammengesetzt sind.

Nachrichten können als Datenpakete aufgefasst werden. Ein Datenpaket ist in der Datenverarbeitung ganz allgemein eine der Bezeichnungen für in sich geschlossene Dateneinheiten, die ein Sender oder auch ein sendender Prozess (z. B. ein Server) einem Empfänger sendet (vergl.: Rahmen (Nachrichtentechnik)). Ein solches Datenpaket – im Unterschied zu einem Datenstrom – hat eine wohldefinierte Länge und Form, es kann daher auf Vollständigkeit und Brauchbarkeit geprüft werden. Das Datenpaket besteht i.d.R. aus einem Header (von engl. head = Kopf) mit den Metadaten und einem Payload, das die Nutzdaten beherbergt.

Das in diesem Artikel vorgestellte Anwendungsprotokoll setzt, wie bereits erwähnt, auf TCP auf. Das Transmission Control Protocol (TCP) ist ein verbindungsorientiertes Protokoll, welches auf Zuverlässigkeit ausgelegt ist und einen bidirektionalen, byte-orientierten Datenstrom zwischen zwei Endpunkten etabliert. Datenpakete, die auf TCP basieren sind deshalb robust gegenüber Fehlern und müssen keine eigene Fehlerkorrektur bereitstellen. Aus den allgemeinen Anforderungen der Anwendung resultiert das Protokollpaket mit Header und Payload.

client-server-model-packet
Aufbau:
  • Version: Das Paket besitzt eine Nummer, die die Protokollversion festlegt. Die 8-Bit große Protokollversion wird bei Änderungen im Protokoll inkrementiert, so dass künftige Anwendungen darauf reagieren können.
  • Flags: Mit den Flags wird eine binäre Variable oder ein Statusindikator definiert, welcher als Hilfsmittel zur Kennzeichnung bestimmter Zustände benutzt werden kann. In dem hier vorgestellten Anwendungsprotokoll ist das die Kompression, Verschlüsselung und Kodierung von Zeichen.
  • Checksum: Eine Prüfsumme garantiert die Integrität der Nutzdaten, falls später ein unsicheres Transportprotokoll, wie UDP, verwendet wird.
  • Type: Ein Datenpaket im Client-Server-Modell hat einen bestimmten Typ, nach dem die Kommunikationspartner die Datenpakete zuordnen können.
  • Source Address: Die Quelladresse identifiziert den Absender des Datenpakets. Die Adresse wird als Endpunkt in IPv6 angesehen und ist 256-Bit groß.
  • Destination Address: Die Zieladresse identifiziert den Empfänger des Datenpakets.
  • Payload Length: Ein maximal 232-Bit großes Feld gibt die Anzahl der nachfolgenden Nutzdaten in Byte an.
  • Data: Am Ende der Nachricht können beliebige Nutzdaten eingefügt werden.

Nachdem die Struktur eines Datenpakets festgelegt wurde, kann in C# die betreffende Klasse mit dem Namen StreamPacket programmiert werden. StreamPacket implementiert das Interface IPacket. Diese Schnittstelle definiert eine Reihe von elementaren Methoden, die alle Paketklassen besitzen müssen, wie z.B. Serialize. Anschließend kann mit einer Fabrikmethode eine konkrete Instanz erzeugt werden ohne das der Benutzer der Schnittstelle das reale Paket kennen muss. Damit ist eine gewünschte Programmierung gegen eine Schnittstelle möglich. Die Fabrikmethode ermöglicht es Pakete eines bestimmten Typs zu erzeugen und darin beliebige Daten zu verpacken.

public sealed class StreamPacket : IPacket
{
    /// <summary>
    /// Creates a new protocol packet, with a header section and no 
    /// data. Some fields will be automatically set, like the protocol version.
    /// </summary>
    public StreamPacket()
    {
        ProtocolVersion = Version.PROTOCOL_VERSION;
        Flags = 0;
        Type = 0;
        Source = null;
        Destination = null;
        Encoding = null;
        Data = new byte[0];
    }
 
    // ...

Zu den wichtigsten Funktionen eines Pakets zählt die Serialisierung. Die Serialisierung ist eine Abbildung von Objekten auf eine externe sequenzielle Darstellungsform. Bevor das Paket über ein Netzwerk übertragen werden kann, muss es vollständig in einzelne Bytes zerlegt werden. Dazu wird eine Methode Serialize entworfen, die sequentiell und systematisch alle Daten des Pakets in einem byte[]-Array ablegt.

/// <summary>
/// Serializes the packet and returns the packed information
/// to the caller.
/// </summary>
/// <exception cref="System.ArgumentException">Thrown when the packet was corrupt</exception>
/// <returns>The serialized packet</returns>
public byte[] Serialize()
{
    // First check if this packet is corrupt
    if (Corrupted) {
        throw new ArgumentException("StreamPacket: Trying to serialize a corrupt packet.");
    }
 
    // Calculate the necessary packet size
    byte[] buffer = new byte[HEADER_BIT_SIZE / 8 + Data.Length];
 
    // ----------------------------------------------------
    // 1. Serialize the version
    // ----------------------------------------------------
    buffer[0] = ProtocolVersion; 
 
    // ----------------------------------------------------
    // 2. Serialize the flags
    // ----------------------------------------------------
    buffer[1] = Flags;
 
    // ----------------------------------------------------
    // 3. Serialize the checksum
    // ----------------------------------------------------
    Array.Copy(BitConverter.GetBytes(Checksum), 0, buffer, 2, 2);
 
    // ----------------------------------------------------
    // 4. Serialize the type
    // ----------------------------------------------------
    Array.Copy(BitConverter.GetBytes((int)Type), 0, buffer, 4, 4);
 
    // ----------------------------------------------------
    // 5. Serialize the source address
    // ----------------------------------------------------
    Array.Copy(AddrStructureConvert.GetBytes(Source), 0, buffer, 8, 32);
 
    // ----------------------------------------------------
    // 6. Serialize the destination address
    // ----------------------------------------------------
    Array.Copy(AddrStructureConvert.GetBytes(Destination), 0, buffer, 40, 32);
 
    // ----------------------------------------------------
    // 7. Serialize the size of the payload
    // ----------------------------------------------------
    Array.Copy(BitConverter.GetBytes(Data.Length), 0, buffer, 72, 4);
 
    // ----------------------------------------------------
    // 8. Serialize the data
    // ----------------------------------------------------
    Array.Copy(Data, 0, buffer, 76, Data.Length);
 
    return buffer;
}

Beim Empfang eines Datenpakets muss exakt in umgekehrter Reihenfolge das Datenpaket wieder deserialisiert, also der Datenstrom in ein Objekt umgewandelt, werden. Dafür wird eine entsprechende Methode Deserialize bereitgestellt, die aus einem Stream das Datenpaket erzeugt.

Ablauf der Kommunikation

Der übergeordnete Austausch der Datenpakete folgt zuvor festgelegten Kommunikationsregeln. So erwartet der Client auf Anfrage der Kontaktliste, ein Datenpaket mit der entsprechend formatierten Liste zu erhalten. Der Server muss also wissen, wie er sich zu verhalten hat, wenn er ein Datenpaket mit der betreffenden Anfrage erhält und vice versa. Nachfolgend ist ein Sequenzdiagramm abgebildet, welches einen möglichen Kommunikationsablauf illustriert.

client-server-model-sequence-diagram

Der Ablauf muss dabei nicht streng sequentiell erfolgen. Selbstverständlich können aber vor einem Login beim Server keine Kontaktdaten abgefragt werden.


Ereignisorientierte Programmierung

Um entsprechend auf eingehende Pakete reagieren zu können, bieten sich Ereignisbehandlungsroutinen (engl. Event Handler) an. Bei der ereignisorientierten Programmierung (engl. event-driven programming) wird das Programm nicht linear durchlaufen, sondern es werden spezielle Ereignisbehandlungsroutinen (engl. listener, observer, event handler) immer dann ausgeführt, wenn ein bestimmtes Ereignis auftritt. C# stellt für die ereignisorientierte Programmierung die sogenannten events und delegates zur Verfügung.

Delegate

Ein Delegate kann bis zu einem gewissen Grad mit einem Funktionszeiger in C und C++ verglichen werden. In C# sind Delegates Objekte erster Klasse und werden von der Sprache voll und ganz unterstützt. Technisch gesehen ist ein Delegate ein Referenztyp, der dazu dient, eine Methode mit einer bestimmten Signatur und einem bestimmten Rückgabetyp zu kapseln. Durch Delegates wird die Klasse, die das Delegate deklariert, von der Klasse, die das Delegate verwendet, entkoppelt.

public class TestDelegate
{
    // Declaration
    public delegate void SimpleDelegate();
 
    public static void MyFunc()
    {
        Console.WriteLine("I was called by a delegate...");
    }
 
    public static void Main(string[] args)
    {
        // Instantiation
        SimpleDelegate simpleDelegate = new SimpleDelegate(MyFunc);
 
        // Invocation
        simpleDelegate();
    }
}

Auf diese Weise kann das Protokoll Delegates bereitstellen, die dann vom Server und Client benutzt werden können.

Event

Grafische Benutzeroberflächen (GUIs) machen es erforderlich, dass Programme auf Events reagieren. Ein Event ist beispielsweise ein Tastendruck oder Button-Klick, ein Menübefehl, der Abschluss einer Packetübertragung usw.

Andere Klassen können auf diese Ereignisse reagieren. Jedes Objekt in C# kann eine Reihe von Events publizieren, die andere Klassen abonnieren können. Wenn die publizierende Klasse ein Event auslöst, werden alle Abonnentenklassen benachrichtigt. So kann ein Objekt eine beliebige Anzahl interessierter Beobachter benachrichtigen, wenn das Objekt angesteuert wird. Das Objekt wird dann als Publizierer (engl. Publisher) bezeichnet, denn er publiziert das Ereignis, und die anderen Klassen sind dann die Abonnenten (engl. Subscriber), denn sie abonnieren das Ereignis.

dotnet-event-driven-programming

In dem Beispiel aus der MSDN löst eine eigene Collection beim Hinzufügen von Elementen mit der Methode Add ein Ereignis aus, das von der Klasse EventListener abonniert wurde. Sobald das Ereignis ausgelöst wurde, wird beim Subscriber die Methode ListChanged aufgerufen.

using System;
 
namespace MyCollections
{
    using System.Collections;
 
    // A class that works just like ArrayList, but sends event
    // notifications whenever the list changes:
    public class ListWithChangedEvent : ArrayList
    {
        // An event that clients can use to be notified whenever the
        // elements of the list change:
        public event EventHandler Changed;
 
        // Invoke the Changed event; called whenever list changes:
        protected virtual void OnChanged(EventArgs e)
        {
            if (Changed != null)
                Changed(this, e);
        }
 
        // Override some of the methods that can change the list;
        // invoke event after each:
        public override int Add(object value)
        {
            int i = base.Add(value);
            OnChanged(EventArgs.Empty);
            return i;
        }
 
        public override void Clear()
        {
            base.Clear();
            OnChanged(EventArgs.Empty);
        }
 
        public override object this[int index]
        {
            set
            {
                base[index] = value;
                OnChanged(EventArgs.Empty);
            }
        }
    }
}
using System;
 
namespace TestEvents
{
    using MyCollections;
 
    public class EventListener
    {
        private ListWithChangedEvent List;
 
        public EventListener(ListWithChangedEvent list)
        {
            List = list;
            // Add "ListChanged" to the Changed event on "List":
            List.Changed += new EventHandler(ListChanged);
        }
 
        // This will be called whenever the list changes:
        private void ListChanged(object sender, EventArgs e)
        {
            Console.WriteLine("This is called when the event fires.");
        }
 
        public void Detach()
        {
            // Detach the event and delete the list:
            List.Changed -= new EventHandler(ListChanged);
            List = null;
        }
    }
 
    public class Test
    {
        // Test the ListWithChangedEvent class:
        public static void Main()
        {
            // Create a new list:
            ListWithChangedEvent list = new ListWithChangedEvent();
 
            // Create a class that listens to the list's change event:
            EventListener listener = new EventListener(list);
 
            // Add and remove items from the list:
            list.Add("item 1");
            list.Clear();
            listener.Detach();
        }
    }
}

Das ereignisorientierte Modell mit Events in C# wird sehr häufig in der asynchronen Programmierung verwendet. Das Client-Server-Modell ist prädestiniert für diese Art der Programmierung, da mit asynchronen Vorgängen üblicherweise Aufgaben ausgeführt werden, die längere Zeit in Anspruch nehmen, z. B. das Öffnen großer Dateien oder die Herstellung einer Verbindung und Übertragung von Daten mit Remotecomputern.

Eigene Ereignisbehandlungsroutinen

In der Klasse Server werden nun entsprechende Ereignisbehandler mittels Multicasting zugewiesen. So sollen bestimmte Methoden aufgerufen werden, wenn beispielsweise ein neues Datenpaket eintrifft oder der Client die Verbindung unterbricht.

public void CreateNewClient(Socket socket)
{
    ClientWorker newClient = new ClientWorker(socket);
    newClient.PacketReceivedEvent += new PacketReceivedEventHandler(PacketReceived);
    newClient.ClientDisconnectedEvent += new ClientDisconnectedEventHandler(ClientDisconnected);

Sobald ein Datenpaket empfangen wird, wird die Methode PacketReceived angesteuert. An dieser Stelle wird eine Unterscheidung der Pakete anhand der Nachrichtentypen durchgeführt. Handelt es sich um ein Login, werden die Daten des Clienten gespeichert. Hat der Client die Kontaktliste angefordert, wird ihm diese zugesendet. Damit lassen sich dank der ereignisorientierten Programmierung auf einfache Art und Weise Kommunikationsregeln erstellen. Sender und Empfänger arbeiten dabei vollständig asynchron. Der Server kann beliebig viele Clienten bedienen, da für jeden Clienten ein eigener Thread (BackgroundWorker) erstellt wird.

/// <summary>
/// Stores all possible packet types in the protocol.
/// </summary>
public void PacketReceived(object sender, PacketEventArgs e)
{
    // Switch on message types
    switch(e.Packet.Type) {
        case PacketType.ClientLoginInformation: 
            // On login
            SetClientData(e.Packet); 
            break;
        case PacketType.ContactListRequest:
            // Send a partial contact list
            SendContactList(e.Packet);
            break;
        case PacketType.ContactRequest:
            // Send detailled contact information
            SendContact(e.Packet);
            break;
        case PacketType.Route:
            // Route message
            Send(e.Packet);
            break;
        case PacketType.Broadcast:
            // Broadcast to all
            BroadCast(e.Packet);
            break;
        default:
            // Unknown type
            LogConsole("unknown message type.", e.Packet.Source);
            break;
    }
}

Der Server verwendet für jeden Clienten eine neue Instanz der Klasse ClientWorker. Dieser löst entsprechend das Ereignis aus, auf das die Klasse Server mit dem geeigneten Event-Handler reagieren kann.

/// <summary>
/// This method triggers an event, when we receive a new packet.
/// </summary>
/// <param name="e"></param>
protected virtual void OnPacketReceived(PacketEventArgs e)
{
    // Invoke the event; called whenever we receive a packet
    if (PacketReceivedEvent != null)
        PacketReceivedEvent(this, e);
}

Daten über Netzwerk transferieren

Das Microsoft .NET Framework bietet Klassen für die Netzwerkprogrammierung an, die sich in zwei Namespaces befinden: System.Net und System.Net.Sockets. Diese Klassen unterstützen jede Funktionalität, von der auf Sockets basierenden Programmierung mit TCP/IP bis hin zum Download von Dateien und HTML-Seiten aus dem Web oder über HTTP. Der Transfer von Daten basiert im Internet und auch in den meisten anderen Netzwerken auf Sockets, mithilfe derer sich Daten senden und empfangen lassen.

Datenpakete senden

Das Senden der Daten wird von einem separaten Thread durchgeführt, um ein Blockieren der Anwendung zu vermeiden. Das ist insbesondere bei Programmen mit einer grafischen Benutzeroberfläche notwendig. Bei der Programmierung mit vielen konkurrierenden Threads sind entsprechende Synchronisierungsmaßnahmen zu ergreifen, siehe „Multithreading in C#“.

Um sicherzustellen das immer nur ein aktiver Thread Daten über das Netzwerk sendet, wird der Codeabschnitt mithilfe eines Mutex geschützt. Auf diese Weise darf zu einem Zeitpunkt jeweils nur ein Thread Daten über den Socket senden. Dazu wird das Datenpaket serialisiert und anschließend mit der Methode Write versendet.

/// <summary>
/// Sends a packet to a client.
/// </summary>
/// <param name="packet"></param>
/// <returns></returns>
private bool SendPacketToClient(IPacket packet)
{
    try {
        // Allow only one thread to operate on the socket
        _mutex.WaitOne();
        NetworkStream networkStream = new NetworkStream(_socket);
        byte[] packed = packet.Serialize();
        networkStream.Write(packed, 0, packed.Length);
        return true;
    } catch {
        return false;
    } finally {
        _mutex.ReleaseMutex();
    }
}

Datenpakete empfangen

Um den Empfang der Daten kümmert sich stets nur ein einzelner Thread, eine Synchronisation ist daher nicht erforderlich. Die in der Schnittstelle IPacket definierte Methode Deserialize setzt das Paket mithilfe des Streams wieder zusammen. Falls das Paket korrupt ist, wird überprüft ob die Netzwerkverbindung noch aktiv ist. Anschließend werden die betreffenden Ereignisse angeregt.

/// <summary>
/// Receives a packet from a client.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ReceivePacketFromClient(object sender, DoWorkEventArgs e)
{
    bool blockingState = _socket.Blocking;
 
    try {
        while (true) {
            IPacket packet = Protocol.CreatePacket(SocketType.Stream); 
 
            packet.Deserialize(new NetworkStream(_socket));
 
            if (packet.Corrupted) {
                // If the packet is corrupted, check if we still are connected,
                // make a nonblocking, zero-byte Send call. If we aren't 
                // connected, it will throw a Exception.
                _socket.Blocking = false;
                _socket.Send(new byte[1], 0, 0);
            } else {
                // Packet successfully received
                OnPacketReceived(new PacketEventArgs(packet));
            }
        }
    } catch (SocketException ex) {
        Debug.WriteLine(ex.Message);
    } catch (ArgumentException ex) {
        Debug.WriteLine(ex.Message);
    } catch (IOException ex) {
        Debug.WriteLine(ex.Message);
    } finally {
        _socket.Blocking = blockingState;
    }
 
    // Socket was closed
    OnClientDisconnected(new ClientEventArgs(socket_));
    Disconnect();
}

Datenpakete komprimieren

Das Netzwerkprotokoll stellt eine rudimentäre Datenkompression zur Verfügung. Dabei können besonders große Datenpakete mit GZipStream komprimiert werden. GZip basiert auf dem Deflate-Algorithmus, der eine Kombination aus LZ77 und Huffman-Kodierung ist. Im Protokoll werden lediglich die Nutzdaten komprimiert, der Header im Datenpaket bleibt davon unberührt.

client-server-model-compression

Die Klasse StreamPacket komprimiert alle Pakete automatisch über eine Getter/Setter Methode. Der API-Nutzer muss allerdings selbst festlegen, ob er eine Datenkompression wünscht oder nicht. Die Kompression von Datenpaketen macht nur dann Sinn, wenn die Nutzdaten eine gewisse Größe aufweisen und die Daten keine sehr hohe Entropie besitzen. Die Entropie ist ein Maß für die Unordnung oder Zufälligkeit im Gebiet der Thermodynamik (Boltzmann Prinzip) und wurde von Claude Elwood Shannon in die Informationstheorie übernommen. In der Informatik bezeichnet die Entropie H den mittleren Informationsgehalt oder auch die Informationsdichte eines Zeichensystems. Der Informationsgehalt eines übermittelten Symbols ist umso höher, je unerwarteter sein Auftreten ist.

H  =  m

i = 1
 pi · I(pi)   =  – m

i = 1
pi · log2 (pi)

Wenn die Nutzdaten also sehr zufällig im Sinne von unerwartet verteilt sind, können die mit dem Deflate-Algorithmus komprimierten Daten unter Umständen sogar größer sein, als die nicht komprimierten Daten. Die Klasse GZip im Anwendungsprotokoll komprimiert die Nutzdaten.

/// <summary>
/// Compresses a raw byte array.
/// </summary>
/// <param name="raw">The raw data, which should be compressed</param>
/// <returns>Returns the compressed byte array</returns>
public static byte[] Compress(byte[] raw)
{
    using (MemoryStream memoryStream = new MemoryStream()) {
        using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress, true)) {
            gzipStream.Write(raw, 0, raw.Length);
        }
        return memoryStream.ToArray();
    }
}

Der Aufruf gestaltet sich denkbar einfach. Nach der Erzeugung eines neuen Datenpakets kann mit der Eigenschaft (engl. Property) Compressed über den Setter das Paket komprimiert und auch wieder dekomprimiert werden.

byte[] data = new byte[65536];
 
// Random data with a high entropy should not be compressed
//Random rand = new Random();
//rand.NextBytes(data);
 
// Fill with some data
for (int i = 0; i < data.Length; i++) {
    data[i] = (byte)i;
}
 
IPacket packet = Protocol.CreatePacket(SocketType.Stream);
packet.Type = PacketType.ClientLoginInformation;
packet.Source = myIPEndPoint;
packet.Destination = remoteIPEndPoint;
packet.Data = data;
 
// Compress packet
packet.Compressed = true;

Mit dem Getter lässt sich auch leicht abfragen, ob die Nutzdaten komprimiert wurden oder nicht. Sender legen selbst fest, ob ihre Pakete komprimiert werden. Die Überprüfung und Interpretation der Daten liegt in der Hand des Empfängers. Die Indikation der Kompression ist Bestandteil der Flags im Header. Anhand des Indikators kann der Empfänger jederzeit prüfen, ob die Nutzdaten komprimiert vorliegen.

Datenpakete verschlüsseln

Alle Nutzdaten in den Paketen können bei Bedarf auch verschlüsselt werden. Im Protokoll steht dafür der Advanced Encryption Standard (AES) zur Verfügung. Es handelt sich um ein symmetrisches Kryptosystem, das als Nachfolger für DES bzw. 3DES im Oktober 2000 vom National Institute of Standards and Technology (NIST) als Standard bekannt gegeben wurde. AES ist in den USA für staatliche Dokumente mit höchster Geheimhaltungsstufe zugelassen.

string data = "Never give up, keep your dreams alive.";
 
IPacket packet = Protocol.CreatePacket(SocketType.Stream);
packet.Type = PacketType.Message;
packet.Source = myIPEndPoint;
packet.Destination = remoteIPEndPoint;
packet.Encoding = Encoding.Unicode;
packet.Data = Encoding.Unicode.GetBytes(data);
packet.Password = "Secret";    // Set password
 
// Encrypt packet
packet.Encrypted = true;

Für die Verschlüsselung muss zuvor ein Passwort gesetzt werden. Anschließend werden die Daten mit dem Property Encrypted ver- und entschlüsselt. Clienten können auf diese Weise Daten sicher austauschen ohne das der Server oder ein Dritter die Paketinhalte einsehen kann. Das setzt, wie bei jedem anderen symmetrischen Kryptosystem voraus, dass das Passwort beiden Netzwerkteilnehmern bekannt ist. Wenn redundante Daten über einen unsicheren Kanal übertragen werden, ist es gebräuchlich diese zu komprimieren und dann zu verschlüsseln. Werden die Daten zuerst verschlüsselt, steigt die Entropie und die Kompression wird unwirksam.

Applikation

Die fertige Applikation mit weiteren nützlichen Klassen und Funktionen können Sie sich in gewohnter Weise in der Download-Rubrik herunterladen. Das Programm kann sehr leicht modifiziert und um weitere Nachrichtentypen erweitert werden. Auf diese Weise lässt sich auch schnell ein Chat-Server realisieren, der Nachrichten an alle eingeloggten Clienten weiterleitet, beliebige Dateien überträgt und direkte Verbindungen zwischen den Clienten herstellt. Das Anwendungsprotokoll ist dahingehend sehr flexibel konzipiert.

Highslide JS Highslide JS
Highslide JS
Zuletzt aktualisiert am Mittwoch, den 28. März 2012 um 18:02 Uhr
 
AUSWAHLMENÜ