Laden...

[Tutorial] Client-/Server-Komponente über TCP-Sockets

Erstellt von tom-essen vor 17 Jahren Letzter Beitrag vor 17 Jahren 127.845 Views
tom-essen Themenstarter:in
1.820 Beiträge seit 2005
vor 17 Jahren
[Tutorial] Client-/Server-Komponente über TCP-Sockets

Hallo!

INFORMATION: Ich habe zu diesem Tutorial einen Diskussions-Thread eröffnet:
Disskussion bzgl. der Client-Server-Komponente

Werde mich heute mal an meinem ersten Tutorial versuchen, habt also bitte ein wenig Nachsicht, falls nicht alles sofort deutlich wird, oder ich etwas übers Ziel hinausgeschossen bin. Für entsprechende Hinweise bin ich aber selbstverständlich empfänglich, sonst werden die nächsten Tutorials bestimmt nicht besser 😉

Anmerkung: Die Beispielanwendung im Anhang funktioniert noch nicht so, wie es unten beschrieben ist, aber die Client-/Server-Assemblies sind voll einsatzbereit!

  1. Einleitung
    In diesem Tutorial werde ich versuchen, die Grundlagen von verteilten Anwendungen auf Basis des Client-/Server-Modells (CS-Modell) zu vermitteln. Als Belohnung für die trockene Theorie gibt's den Quellcode für eine multiclient-fähige CS-Komponente.

  2. Begrifflichkeiten
    Zunächst einmal ist ein Server nicht, wie oft vermutet, ein Rechner, sondern nur ein einzelnes Programm, welches seine Dienste auf diesem Rechner zur Verfügung stellt. Auf diesem Rechner können mehrerer solcher Programme laufen, der Rechner selbst wird Host genannt.
    Programme und/oder Rechner, welche diese Dienste in Anspruch nehmen, werden Clients genannt.
    Ich werde mich im folgenden nur auf die Software-Komponenten beschränken, also Server und Clients.
    Server bieten ihre Dienste auf sogenannten Ports an. Ein PC-System hat von diesen Ports 65536 zur Verfügung, wobei diese von 0 bis 65535 durchnummeriert sind. Die Ports bis 1023 sind dabei international standardisiert belegt und sollten nur für ganz bestimmte Zwecke verwendet werden.
    Ein Port ist vergleichbar mit einer Hausnummer in einer Strasse. Als Beispiel könnte in Haus Nr. 25 jemand wohnen, der Post verteilt. Um diese Dienste in Anspruch nehmen zu können, muss man ebenfalls eine Strasse (PC) und Hausnr. (Port) angeben, damit die Leistungen entsprechend zugestellt werden können.
    Bezogen auf PC-Systeme wird der Port Nr. 25 dazu verwendet, damit Email-Programme die Emails vom Internet-Provider abholen können.
    Und genauso, wie in einem Haus nicht zwingend jemand wohnen muss, gibt es auch Ports, wo keine Dienste angeboten werden.

  3. Allgemeiner Verbindungsvorgang
    Wenn nun ein Programm existiert, welches Dienste anbieten möchte, muss es diesen "Wunsch" am System anmelden, d.h., es muss dem Host-Betriebsystem mitteilen, dass es nun alle Verbindungen von ausserhalb an einen bestimmten Port x annehmen möchte. Damit dies funktioniert, muss u.a. auch eine evtl. (hoffentlich) vorhandene Firewall entsprechend konfiguriert werden, sodass dieser Port von ausserhalb ansprechbar ist ( In unserer Strassen-Analogie wäre die Firewall z.B. ein Vorhängeschloss an der Tür ).
    Der Server wartet nun auf Verbindungen von ausserhalb.
    Nun versucht ein Programm auf einem anderen Rechner, die Dienste des Servers in Anspruch zu nehmen. Dazu teilt es dem Betriebsystem (OS) des Clients mit, dass es eine Verbindung zum Host auf Port x herstellen möchte.
    Das OS stellt dem Client dafür nun ebenfalls eine noch freie Port-Nummer ab 1024 zur Verfügung und leitet die Verbindung ein.
    Der Server seinerseits erkennt den Versuch eines Verbindungsaufbaus und nimmt die Verbindung an.
    Damit der Server in der Lage ist, auch mehrere gleichzeitige Verbindungen von Clients annehmen und verwalten zu können, erhält er zu jeder Verbindung über den Port x eine zusätzliche Verbindungsnummer (oft Handle genannt).
    Anschliessend erstellt der Server für die neue Verbindung einen zusätzlichen Prozess, welcher nur für diese Verbindung verantwortlich ist.
    Der Server bekommt zudem auch mitgeteilt, auf welchem PC und an welcher Port-Nummer der Client läuft.
    Ein Server ist an sich erstmal nur eine passive Komponente, welche durch die Client-Anfrage aktiviert wird. D.h. ein Server sendet nur Daten an den Client, wenn dieser zuvor eine Anfrage gestellt hat. Hier hinkt unsere Strassen-Analogie etwas, da wir in diesem Fall nur Post bekommen, wenn wir diese explizit anfordern.

Nach Abarbeitung können sowohl Client als auch Server die Verbindung beenden. Dies kann explizit erfolgen, oder aber durch sogenannte Timeouts, d.h. dass ausbleiben einer Reaktion innerhalb einer bestimmte Zeitspanne. So kann z.B. der Server eine Verbindung automatisch beenden, wenn nach beispielsweise 1 Minuten keine Anfrage mehr vom Client gekommen ist.

  1. Die Umsetzung
    Nach der grauen und trockenen Theorie nun die (hoffentlich) schmackhafte Umsetzung in Quellcode.

Allgemein sei hier noch bemerkt, dass es sich dabei um eine Komponente für kurzfristige CS-Verbindungen handeln wird, d.h. Client baut Verbindung auf, stellt eine Anfrage, Server antwortet (je nach Funktionalität) und fertig!

Zunächst einmal benötigen wir den entsprechenden Namespace, welcher uns die gewünschten Funktionalitäten zur Verfügung stellt:


// Allgemeine Funktionalität für Netzwerk-Programmierung
using System.Net;
// Funktionalitäten speziell für die Verbindungsverwaltung
using System.Net.Sockets;
// Funktionalitäten für die Prozess-Erstellung und -Verwaltung
using System.Threading;

Nun sollte man sich Gedanken machen, welchen Port man auf dem Host verwenden möchte, und welche Client-PC's akzeptiert werden sollen. Meistens akzeptiert man aber sowieso alle PC's, da entweder nur im lokalen Netzwerk getestet/gearbeitet wird, und bzgl. Internetanfragen ist eine Eingrenzung anhand der IP-Adresse auch nur in ganz bestimmten Fällen sinnvoll.
Bzgl. der Überlegung der Port-Adresse sollte man etwas sorgfältiger zu Werke gehen:

  • Welche Dienste arbeiten bereits auf dem Host?
  • Auf welchen Ports werden diese Dienste angeboten?

Mit "netstat -a" kann man sich auf der Windows-Console (Start, Ausführen, cmd) die laufenden Dienste anzeigen lassen. Wichtig bei der Ausgabe ist die Spalte "Lokale Adresse" (die 2. Spalte): Dort steht hinter dem Doppelpunkt die jeweilige Port-Nummer. Man sucht sich nun einfach eine freie Portnummer (am besten ab 10000) raus (also eine, die nicht in der Liste steht). Noch sicherer ist man, wenn man diese Prozedur auf allen Rechnern im Netz ausführt, um ganz sicher zu sein, dass kein Dienst im lokalen Netzwerk diese Port-Adresse verwendet. Bei vielen Rechnern ist dies natürlich ein entsprechend grösserer Aufwand.
Ich muss dabei zugeben, dass ich fast alle C/S-Anwendungen z.Zt. mit Port 10000 durchführe (Schande über mich 🙄).

Mit


public int serverListenPort = 10000;
TcpListener listener = new TcpListener(IPAddress.Any,serverListenPort);

erstellt man nun eine Klasseninstanz für die grundlegende TCP-Kommunikation, wobei IPAddress.Any angibt, dass jeder Rechner eine Verbindung aufbauen darf, und serverListenPort gibt die von uns ausgewählte Portnummer an (jaja, schon wieder 10000).

Alles folgende sollte nun in eine try-catch-Schleife gepackt werden, um Fehler sofort analysieren zu können.
Als nächstes müssen wir dem Betriebssystem nun mitteilen, dass wir Verbindungen unter den o.g. Angaben akzeptieren wollen:

listener.Start();

Um nun eine eingehende Verbindung anzunehmen, müssen wir erstmal darauf warten:

Socket newSocket = listener.AcceptSocket();

Die Methode AcceptSocket() blockiert solange, bis eine eingehende Verbindungsanfrage kommt. Sämtliche Daten zu dieser Verbindung ( z.B. IP des anfragenden Rechners ) stehen in dem übergebenen Socket-Objekt.

Da wir später das Ganze auf Thread-Basis durchführen wollen, ist eine blockierende Methode allerdings äusserst problematisch, da solange der komplette Thread blockiert wird. Also fügen wir davor noch ein

while (!listener.Pending()) {Thread.Sleep(200);}

ein. TcpListener.Pending gibt erst "true" zurück, wenn eine neue Verbindung anliegt.
Mit Thread.Sleep(200) sorgen wir dafür, dass der Prozessor nicht zu stark belastet wird, und 200ms sollten für neu anliegende Verbindungen nicht zu lang sein 😉.

Wenn man das Ganze nun noch ein wenig mit Consolen-Ausgaben schmückt, dann sieht die erste Server-Klasse ungefähr so aus:


using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace ClientServer.Server

	/// <summary>
	/// Zusammenfassung für den Server
	/// </summary>
	public class Server
	{
		/// <summary>
		/// Port, auf dem der Server auf Clientverbindungen wartet
		/// </summary>
		public const int serverListenPort = 10000;
		/// <summary>
		/// Anzahl Millisekunden in Warteschleifen
		/// </summary>
		public const int sleepTime = 200;
		/// <summary>
		/// IP-Adresse, an der auf Verbindungen gewartet werden soll.
		/// Standardmässig wird auf allen Schnittstellen gewartet.
		/// </summary>
		public IPAddress ipAddress = IPAddress.Any;

		/// <summary>
		/// Der Standardkonstruktor
		/// </summary>
		public Server()
		{
			// Alle Netzwerk-Schnittstellen abhören
			TcpListener listener = new TcpListener(ipAddress,serverListenPort);
			System.Console.WriteLine("Listening on port "+serverListenPort+"...");
			try
			{
				// Verbindungsannahme aktivieren
				listener.Start();
				// Warten auf Verbindung
				while (!listener.Pending()) {Thread.Sleep(sleepTime);}
				// Verbindung annehmen
				Socket newSocket = listener.AcceptSocket();
				// Mitteilung bzgl. neuer Clientverbindung
				System.Console.WriteLine("Neue Client-Verbindung ("+
							"IP: "+newSocket.RemoteEndPoint+", "+
							"Port "+((IPEndPoint)newSocket.LocalEndPoint).Port.ToString()+")");
			}
			catch (Exception ex)
			{
				throw new Exception("Fehler bei Verbindungserkennung",ex);
			}
		}
	}

Sollte ich jetzt hier keinen Fehler gemacht haben, sollte ein entsprechendes Programm beim Instanziieren dieser Klasse solange blockiert werden, bis eine Client-Verbindung eingeleitet wird.
Und genau um diese Client-Verbindung werden wir uns als nächstes bemühen 🙂.

Dazu muss allerdings ein zweites Projekt in der aktuellen Projektmappe hinzugefügt werden, da die Server-Anwendung ja bereits durch das Warten auf eine Verbindung blockiert ist. Es sollte sich hierbei um ein ausführbares Projekt handeln, d.h. in der Projektmappe sollten sich dann zwei ausführbare Projekte befinden, welche auch zusammen beim debuggen gestartet werden.

Auf Client-Seite muss eigentlich nur ein entsprechendes Socket-Objekt instanziiert und die Verbindung eingeleitet werden:


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Security.Permissions;
using System.Threading;

namespace ClientServer.Client
{
	/// <summary>
	/// Zusammenfassung für den Client
	/// </summary>
	public class Client
	{
		public Socket socket = null;

		/// <summary>
		/// Stellt eine Verbindung zum angegebenen Server her
		/// </summary>
		/// <param name="ipAddress">IP-Adresse oder Hostname des Servers</param>
		/// <param name="port">Port-Nummer des Servers</param>
		public Client(String Address,int port)
		{
			try
			{
				IPHostEntry hostInfo = Dns.GetHostByName(Address);
				System.Net.IPEndPoint ep = new System.Net.IPEndPoint(hostInfo.AddressList[0],port);
				socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
				socket.Connect(ep);
			}
			catch (SecurityException ex)
			{
				throw new Exception("Fehler beim Herstellen der Verbindung zum Server, evtl. verursacht durch eine Firewall oder ähnliche Schutzmechanismen",ex);
			}
			catch (Exception ex)
			{
				throw new Exception("Fehler beim Herstellen der Verbindung zum Server",ex);
			}
		}
	}
}

Ein Aufruf in einem Programm würde dann folgendermassen aussehen:


ClientServer.Client client = new Client("192.168.100.1",10000);

Dabei würde der Client versuchen, eine Verbindung zum Rechner mit der IP 192.168.100.1 auf Port 10000 aufzubauen (Für's lokale testen reicht auch die 127.0.0.1 aus).
Vom Server sollten nun entsprechende Consolen-Ausgaben erscheinen, oder Fehlermeldungen vom Client 😛.

Bei Erfolg wäre die erste Client-/Server-Verbindung fertig.

Folgendes steht nun noch auf unserer ToDo-Liste:
1.) Server soll in einem eigenen Prozess auf Verbindungen warten
Damit die Serveranwendung nicht komplett durch das Warten blockiert ist, sollte dieser Teil in einen eigenen Prozess ausgelagert werden.
2.) Neue Verbindungen sollen in einem seperaten Prozess verwaltet werden
Der Prozess aus 1. erzeugt für jede neue Verbindung einen seperaten Prozess, wodurch die Anwendung schon fast multiclient-fähig wird.
3.) Seperate Klasse für serverseitige Verbindungsverwaltung
Unsere Komponente soll lediglich vom Aufwand für die Verbindungen abstrahieren, die eigentliche Funktionalität muss der Programmierer implementieren.
Dazu sollte jede Verbindung in einer von einem Standard-Interface abgeleiteten Klasse verwaltet werden können, welche u.a. den Empfangs- und Sende-Prozess vereinfacht.
4.) Seperate Klasse für clientseitige Verbindungsverwaltung
Hier sollte die Klasse ebenfalls der Abstraktion der Verbindungsverwaltung sowie des Empfangs- und Sende-Prozesses dienen.

Diese Schritte werde ich nun nach und nach abarbeiten, abschliessend wird kurz das im Anhang beigefügte Beispiel kurz erläutert.

zu 1.) Server soll in einem eigenen Prozess auf Verbindungen warten
Zunächst erweitern wir unsere Klasse um eine entsprechende Eigenschaft für den Thread:


/// <summary>
/// Der Haupt-Thread
/// </summary>
private Thread mainThread;

Nun müssen wir die derzeitige Funktionalität vom Konstruktor in eine eigene Methode (void mainListener()) verschieben, und dafür im Konstruktor den Thread erzeugen:


/// <summary>
/// Der Standardkonstruktor richtet den Thread ein, welcher
/// anschliessend auf Client-Verbindungen wartet.
/// </summary>
public Server()
{
	// Hauptthread wird instanziiert ...
	mainThread = new Thread(new ThreadStart(this.mainListener));
	// ... und gestartet
	mainThread.Start();
}

/// <summary>
/// Haupt-Thread, wartet auf neue Client-Verbindungen
/// </summary>
private void mainListener()
{
	// Alle Netzwerk-Schnittstellen abhören
	TcpListener listener = new TcpListener(ipAddress,serverListenPort);
	System.Console.WriteLine("Listening on port "+serverListenPort+"...");
	try
	{
		// Verbindungsannahme aktivieren
		listener.Start();
		// Warten auf Verbindung
		while (!listener.Pending()) {Thread.Sleep(sleepTime);}
		// Verbindung annehmen
		Socket newSocket = listener.AcceptSocket();
		// Mitteilung bzgl. neuer Clientverbindung
		System.Console.WriteLine("Neue Client-Verbindung ("+
					"IP: "+newSocket.RemoteEndPoint+", "+
					"Port "+((IPEndPoint)newSocket.LocalEndPoint).Port.ToString()+")");
	}
	catch (Exception ex)
	{
		throw new Exception("Fehler bei Verbindungserkennung",ex);
	}
}

Somit bricht unsere Serveranwendung nicht mehr beim ersten Verbindungsaufbau ab, sondern informiert uns über jede neue Client-Verbindung.

zu 2.) Neue Verbindungen sollen in einem seperaten Prozess verwaltet werden
Da wir hierzu nun entwas mehr an Informationen verwalten müssen, erstellen wir für die interne Verwaltung eine neue Klasse:


/// <summary>
/// Serverseitige Verbindungsinstanz
/// </summary>
class serverInstance
{
	/// <summary>
	/// Zeit in Millisekunden in Warteschleifen
	/// </summary>
	const int SleepTime = 200;
	public Thread serverThread;
	public Socket socket;
	/// <summary>
	/// Standard-Konstruktor, welcher u.a. den 
	/// Thread für diese Verbindung erzeugt.
	/// </summary>
	/// <param name="socket">Verbindungssocket</param>
	public serverInstance(Socket socket)
	{
		this.socket = socket;
		// Thread erzeugen
		serverThread = new Thread(new ThreadStart(Process));
		serverThread.Start();
	}

	public void Process()
	{
		try
		{
			socket.Close();
			serverThread.Abort();
		}
		catch
		{
			System.Console.WriteLine("Verbindung zum Client beendet");
		}
	}
}

Diese Klasse erzeugt automatisch bei Instanziierung den entsprechenden Thread, wobei dieser sich sofort wieder beendet und dabei die Verbindung trennt.
Zusätzliche Funktionalität macht erst nach Punkt 3. unserer ToDo-Liste Sinn.

In unserer Server-Klasse brauchen wir nun noch eine Eigenschaft zur Verwaltung aller Verbindungen, sowie die entsprechende Funktionalität innerhalb unseres Haupt-Threads.

Unsere benötigten Eigenschaften:


/// <summary>
/// Anzahl der maximal möglichen Clients pro Server
/// </summary>
public const int maxServerConnections=   100;
/// <summary>
/// Array für die möglichen Verbindungen
/// </summary>
private System.Collections.ArrayList Clients = new ArrayList(maxServerConnections);

"maxServerConnections" findet später noch weitere Verwendung, wir wollen ja schliesslich nicht, dass unser Host durch zuviele gleichzeitige Verbindungen lahmgelegt werden kann!

Und hier die entsprechende Funktionalität im Hauptthread:


/// <summary>
/// Haupt-Thread, wartet auf neue Client-Verbindungen
/// </summary>
private void mainListener()
{
	// Alle Netzwerk-Schnittstellen abhören
	TcpListener listener = new TcpListener(ipAddress,serverListenPort);
	System.Console.WriteLine("Listening on port "+serverListenPort+"...");
	try
	{
		listener.Start();
		// Solange Clients akzeptieren, bis das 
		// angegebene Maximum erreicht ist
		while (true)
		{
			while (!listener.Pending()) {Thread.Sleep(sleepTime);}
			Socket newSocket = listener.AcceptSocket();
			if (newSocket != null)
			{
				// Mitteilung bzgl. neuer Clientverbindung
				System.Console.WriteLine("Neue Client-Verbindung ("+
					"IP: "+newSocket.RemoteEndPoint+", "+
					"Port "+((IPEndPoint)newSocket.LocalEndPoint).Port.ToString()+")");
				serverInstance newConnection = new serverInstance(newSocket);
				Clients.Add(newConnection);
			}
		}
	}
	catch (ThreadAbortException ex)
	{
		System.Console.WriteLine("Server wird beendet");
	}
	catch (Exception ex)
	{
		throw new Exception("Fehler bei Verbindungserkennung",ex);
	}
}

Als Test kann mal ja einfach mal die Client-Anwendung mehrmals starten, man sollte dann auf der Console entsprechende Meldungen erhalten (man beachte dabei die zusätzliche Zahlenangabe hinter "Neue Client-Verbindung (IP: ....)", welche sich bei jeder Verbindung erhöht.

zu 3.) Seperate Klasse für serverseitige Verbindungsverwaltung
Um nun dem Programmierer wieder die Kontrolle über die Verbindungen zu geben, brauchen wir zunächst ein Interface:


/// <summary>
/// Interface für Klassen zur Verwaltung von Verbindungen auf Client-Seite
/// </summary>
public interface serverInterface
{
	/// <summary>
	/// Teilt dem Interface die verwendeten Verbindungsinformationen mit
	/// </summary>
	/// <param name="socket">Socket-Verbindung zum Client</param>
	void setConnectionData(Socket socket);
	/// <summary>
	/// Automatisch registrierter Eventhandler
	/// </summary>
	/// <param name="Message"></param>
	void newMessage(string Message);
	/// <summary>
	/// Beenden der Verbindung zum Client
	/// </summary>
	void closeConnection();
	/// <summary>
	/// Wird aufgerufen, wenn Daten vom Client empfangen wurden
	/// </summary>
	/// <param name="data">Die Daten als Byte-Array</param>
	void Receive(byte[] data);
}

Eine von diesem Interface abgeleitete Klasse wird dann später automatisch bei einer neuen Verbindung vom Server instanziiert.
"setConnectionData(Socket socket);" wird dabei vom Server aufgerufen, um dieser Klasse das Socket mitzuteilen (falls dieses benötigt wird, z.B. zum Senden von Informationen an den Client).
"newMessage(string Message);" wird z.Zt. noch nicht benötigt, kommt als Abschluss noch als Schmankerl 😉.
"closeConnection();" wird aufgerufen, wenn die Verbindung beendet wird.
"Receive(byte[] data);" wird vom Server aufgerufen, wenn der Server vom Client Daten empfangen hat.
Der Konstruktor der Klasse "serverInstance" wird erweitert:


public serverInterface instanceOfClass;
public serverInstance(Socket socket,serverInterface instance)
{
	this.socket = socket;
	this.instanceOfClass = instance;
...

Die Server-Klasse erhält eine zusätzliche Eigenschaft


/// <summary>
/// Klasse, welche bei einer neuen Verbindung für die serverseitige
/// Verwaltung erstellt werden soll.
/// </summary>
private Type serverClass;

und der Konstruktor wird entsprechend angepasst:


/// <summary>
/// Der Standardkonstruktor richtet den Thread ein, welcher
/// anschliessend auf Client-Verbindungen wartet.
/// </summary>
public Server(Type serverClass)
{
	Type it = typeof(serverInterface);
	// Zentralen Eventhandler registrieren
	status.messageEvents+= new MessageEvent(Server_MessageEvents);
	// Klasse für serverseitige Verwaltung setzen
	Type[] ifs = serverClass.GetInterfaces();
	bool found = false;
	foreach(Type aType in ifs)
	if (it.ToString()==aType.ToString()) found = true;
	if (found)
        	this.serverClass = serverClass;
	// Hauptthread wird instanziiert ...
	mainThread = new Thread(new ThreadStart(this.mainListener));
	// ... und gestartet
	mainThread.Start();
}

Anmerkung: Falls hier jemand eine elegantere Lösung zur Prüfung auf eine korrekt abgeleitete Klasse hat, immer her damit 😄.

Ebenso wird der Haupt-Thread angepasst (unterhalb der Mitteilung "Neue Client-Verbindung..."):


// Instanz der serverseitigen Verwaltungsklasse erzeugen
serverInterface x = (serverInterface)Activator.CreateInstance(serverClass);
x.setConnectionData(newSocket);
serverInstance newConnection = new serverInstance(newSocket,x);
Clients.Add(newConnection);

Eine beispielhafte Klassenableitung würde so aussehen:


using System;
using System.Net.Sockets;
using ClientServer.Server;

namespace ServerGui
{
	/// <summary>
	/// Zusammenfassung für server_class.
	/// </summary>
	public class server_class : ClientServer.Server.serverInterface
	{
		Socket socket = null;
		public server_class() {}

		public void setConnectionData(System.Net.Sockets.Socket socket)
		{this.socket = socket;}

		public void closeConnection() {}

		public void Receive(byte[] data)
		{
		System.Console.WriteLine(System.Text.Encoding.ASCII.GetString(data));
		int bytesSend = socket.Send(data);
		}

		public void newMessage(string Message) {}
	}
}

Dabei werden lediglich alle vom Client gesendeten Daten ausgegeben und an den Client als Echo zurückgeschickt.

Damit der Server jedoch überhaupt Daten empfangen kann, muss nun noch die Process-Methode der Klasse "serverInstance" erweitert werden, wozu unter anderem auch ein weiterer Namespace benötigt wird:

using System.IO;

Hier der komplette Inhalt der Process-Methode:


MemoryStream mem = new MemoryStream();// Empfangspuffer
byte[] buffer = new byte[BufferSize];
int TimeOut = 0;
// 10 Sekunden Timeout
while (TimeOut<(10*1000/SleepTime))
{
	mem.Seek(0,SeekOrigin.Begin);
	mem.SetLength(0);
	while (socket.Available>0) 
	{
		//Byte[] buffer = new byte[bytesAvailable];
		int bytesRead = socket.Receive(buffer,buffer.Length,SocketFlags.None);
		if (bytesRead<=0) continue;
		mem.Write(buffer,0,bytesRead);
		// Alles zurücksetzen
	}
	if (mem.Length>0)
	{
		if (mem.Length==4)
			if (System.Text.Encoding.ASCII.GetString(mem.ToArray(),0,4)=="quit")
			{
				instanceOfClass.closeConnection();
				break;
			}
		instanceOfClass.Receive(mem.ToArray());
		mem.Seek(0,SeekOrigin.Begin);
		mem.SetLength(0);
		TimeOut=0;
	} 
	else 
	{
		TimeOut++;
		Thread.Sleep(SleepTime);
	}
}
instanceOfClass.closeConnection();
socket.Close();
socket = null;
serverThread.Abort();

Der Thread wartet nach Erstellung nun max. 10 Sekunden auf Daten vom Client. Nach dieser Zeit wird der Thread und die entsprechende Verbindung getrennt.
Auch wenn bereits Daten gesendet wurden, wird die Verbindung nach 10 Sekunden Inaktivität getrennt.
Hier muss evtl. noch berücksichtig werden, dass diese Zeitspanne beim Senden von Daten vom Server an den Client ebenfalls neu gestartet wird.
Zusätzlich hat der Client die Möglichkeit, die Verbindung durch Senden der Zeichenfolge "quit" die Verbindung explizit zu beenden.

zu 4.) Seperate Klasse für clientseitige Verbindungsverwaltung
Die Client-Klasse aus den Anfängen (vor der ToDo-Liste) wird nun zum Senden, Empfangen und Schliessen der Verbindung entsprechend erweitert:


/// <summary>
/// Sendet die übergebenen Daten.
/// Fehler werden z.Zt. still akzeptiert, als Rückgabewert
/// erfolgt dann 0
/// </summary>
/// <param name="data">Die zu übermittelnden Daten</param>
/// <returns>Anzahl der übertragenen Bytes</returns>
public int send(Byte[] data)
{
	if (socket==null) return 0;
	if (data==null) return 0;
	if (data.Length<=0) return 0;
	if (receiving) return 0;
	try
	{
		//int offset = 0;// Quelldaten-Offset
		//int lastsend = 0;// Anzahl der zuletzt übertragenen Bytes
		//int toSend = 0;// Anzahl der zu sendenden Bytes
		return socket.Send(data);
	}
	catch (SocketException ex)
	{
		socket.Close();
		socket = null;
		return 0;
	}
	catch (ObjectDisposedException ex)
	{
		socket.Close();
		socket = null;
		return 0;
	}
	catch (Exception ex)
	{
		socket.Close();
		socket = null;
		return 0;
	}
}

/// <summary>
/// Schliesst eine offene Verbindung, falls nicht grade
/// ein Empfang läuft.
/// </summary>
public void close()
{
	if (receiving) return;
	if (socket==null) return;
	send(System.Text.Encoding.ASCII.GetBytes("quit"));
	socket.Close();
	socket = null;
}

/// <summary>
/// Wartet auf Daten vom Server.
/// Wurden innerhalb der Zeitspanne "TimeOut" (in ms) keine
/// Daten empfangen, wird null zurückgegeben, ansonsten ein
/// Byte-Array mit den empfangenen Daten.
/// </summary>
/// <returns>null oder ein Byte-Array mit empfangenen Daten</returns>
public byte[] receive()
{
	try
	{
		int cnt = 0;
		receiving=true;
		MemoryStream mem = new MemoryStream();// Empfangspuffer
		byte[] buffer = new byte[BufferSize];
		while (cnt<(TimeOut/SleepTime))
		{
			while (socket.Available>0) 
			{
				int bytesRead = socket.Receive(buffer,buffer.Length,SocketFlags.None);
				if (bytesRead<=0) continue;
				mem.Write(buffer,0,bytesRead);
			}
			Thread.Sleep(SleepTime);
			if (mem.Length>0 && socket.Available==0)
			{
				receiving = false;
				return mem.ToArray();
			} 
			else 
			{
				cnt++;
			}
		}
		receiving=false;
		return null;
	}
	catch
	{
		receiving=false;
		return null;
	}
}

Damit wären Client- und Server-Klassen soweit fertig.

Die Beispielanwendung wird der Form sein, dass man eingegeben Text an den Server sendet, dieser ihn in einer ListBox anzeigt, und als Echo wieder zurückschickt, was der Client entsprechend überprüft.

Anmerkung: Die Beispielanwendung im Anhang funktioniert noch nicht so, wie oben beschrieben, aber die Client-/Server-Assemblies sind voll einsatzbereit!

Es handelt sich übrigens um ein VS2003.NET-Projekt!

ToDo:*Grössere Flexibilität bzgl. Wahl der Port-Nummer *Möglichkeit dauerhafter Verbindungen *Vergleich mit Remoting

Nobody is perfect. I'm sad, i'm not nobody 🙁