PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Gute Netzwerkprogrammierung - wie per TCP?


Kennung Eins
2007-10-18, 18:47:01
Ich programmiere seit ein paar Jahren an einem System, welches aus mittlerweile 4 Rechnern besteht, die alle unterschiedliche Aufgaben haben: Davon 2 PDAs, 1x Win2k und 1xVista. Auf allen Rechnern laufen .NET Anwendungen.

Ich vermute schon eine Weile, dass die Art und Weise, mit der ich die Systeme miteinander kommunizieren lasse, nicht gerade perfekt ist - aber bisher war das egal (jetzt nicht mehr). Daher wollte ich hier mal ein bisschen Input holen, ob mein Vorgehen so korrekt ist, oder ob man das anders besser machen könnte.

Die Kurzform:
Ich habe Rechner A, B, C, D. Die Struktur sieht etwa so aus

Client Server Client Server Client
A - B - B - C - D

Auf Rechner B läuft sowohl ein Server-Objekt (Stellt Daten für A bereit) als auch ein Client (ruft Steuerinformationen von C ab, um Daten für A festzulegen).

Meine Clients und Server lasse ich folgendermaßen miteinander kommunizieren: Der Server wartet darauf, dass der Client einen Text-String schickt. Z.B. "Next" für den "nächsten Abruf". Empfängt der Server einen String, sucht er (switch-case) danach, ob er den String kennt. Kennt er ihn, ruft er Methoden auf und sendet deren Ergebnis an den Client zurück.

Zusätzlich dazu habe ich vorn in jedem String 4 Kontrollbytes, die die Länge des Strings angeben - denn ich habe des Öfteren am Client nicht alle Bytes empfangen, die der Server geschickt hat (was eigentlich durch TCP nicht passieren dürfte, oder?). Diese 4 Bytes werden natürlich vor der Auswertung des Strings abgeschnitten.


Server:

private void StartServer()
{
try
{
TcpListener server = new TcpListener(localAddr, port);
server.Start();
Byte[] bytes = new Byte[256];

while(runningServer) {
Thread.Sleep(1); if (server.Pending())
{
TcpClient client = server.AcceptTcpClient();
String data = null;
NetworkStream stream = client.GetStream();

int i;
while(((i = stream.Read(bytes, 0, bytes.Length))!=0) && (runningServer) && (startSending))
{
byte[] lengthByte = new byte[4];
if (i >= 4) // bevor ich etwas umwandle kann ich auch gleich überprüfen, ob überhaupt mindestens 4 byte angekommen sind (falls nicht: Übertragungsfehler)
{
for (int i1 = 0;i1<lengthByte.Length;i1++)
lengthByte[i1] = bytes[i1];
}
uint u = BitConverter.ToUInt32(lengthByte,0);
if ((i != u) || (i < 4)) // Datenverlust --> nochmal neu lesen
{
Thread.Sleep(100); // 100ms Wartepause um nicht 10Billionen rekursive Aufrufe zu generieren
data = "Unknown"; // dann nochmal abfragen.
}
else
{
System.Text.StringBuilder myCompleteMessage = new System.Text.StringBuilder();
myCompleteMessage.AppendFormat("{0}", System.Text.Encoding.ASCII.GetString(bytes, 0, i));
data = myCompleteMessage.ToString().Substring(4);
}

// hier wird jetzt überprüft, was für Daten empfangen wurden
switch (data)
{
default: respByte = respUnknown;
break; case "Next":
respByte = respTextByte;
break;
}

// jetzt noch unsere 4 Kontrollbytes davor setzen
byte[] respByteWithDatalength = prepareBytes(respByte);
// und abschicken
stream.Write(respByteWithDatalength, 0, respByteWithDatalength.Length);
}
stream.Close();
client.Close(); }
}
server.Stop();
}
catch(Exception e)
{
MessageBox.Show("Exception: "+e.ToString());
}
}


Und jetzt der Code für den Client:


Client:

private String sendData;

private byte[] send(NetworkStream stream)
{
byte[] data = prepareBytes(sendData); // Text in Byte-Array umwandeln und zuweisen
stream.Write(data, 0, data.Length); // Schreibe die Daten in den Datenstrom (Daten versenden)
return data;
}

private string receive(NetworkStream stream)
{
byte[] data = new Byte[255]; // daten-Array leeren

string responseData = String.Empty; // Antwort-String leeren

System.Text.StringBuilder myCompleteMessage = new System.Text.StringBuilder();
int bytes = 0;

// Incoming message may be larger than buffer size.
do
{
bytes = stream.Read(data, 0, data.Length);
myCompleteMessage.AppendFormat(ni,"{0}", System.Text.Encoding.ASCII.GetString(data, 0, bytes));

}
while(stream.DataAvailable);

responseData = myCompleteMessage.ToString(); // Empfangene Daten in String umwandeln

// Jeder String enthält vorn 4 Kontrollbytes, die die Länge des zu empfangenden Strings anzeigen
byte[] lengthByte = new byte[4];
for (int i = 0;i<lengthByte.Length;i++)
lengthByte[i] = data[i];
uint u = BitConverter.ToUInt32(lengthByte,0);

// wenn die Länge, die vorn im String steht, nicht der tatsächlichen Länge entspricht:
if ((bytes != u) || (responseData.Length < 4)) // Datenverlust --> nochmal neu lesen
{
Thread.Sleep(500); // 500ms Wartepause um nicht 10Billionen rekursive Aufrufe zu generieren
send(stream); // als erstes nochmal die Anfrage schicken
responseData = receive(stream); // jetzt erneut lesen
}
if (responseData == null)
return "(connection error)";

// Den empfangenen String abzüglich der ersten 4 Kontrollbytes übernehmen
responseData = responseData.Substring(4);
return responseData;
}

private void Connect()
{
byte[] data = new Byte[256];
try
{
if (this.running) // Flag zur Thread-Kontrolle
{
TcpClient client = new TcpClient(con, port );
NetworkStream stream = client.GetStream();
while (this.running)
{
sendData = "Next";
send(stream); // Schicke einen Text (siehe Methode "send")
responseData = receive(stream); // empfange vom stream (siehe methode "receive")

Thread.Sleep(1);
}
// Close everything.
stream.Close();
stream = null; // to make sure
client.Close();
client = null; // to make sure
}
}
catch (Exception e)
{
MessageBox.Show("Exception: "+e.ToString());
}
}



Jetzt meine Fragen:

- Macht man das so?

- Wie ginge es besser? (schneller / schöner)

Trap
2007-10-18, 18:56:44
Zusätzlich dazu habe ich vorn in jedem String 4 Kontrollbytes, die die Länge des Strings angeben - denn ich habe des Öfteren am Client nicht alle Bytes empfangen, die der Server geschickt hat (was eigentlich durch TCP nicht passieren dürfte, oder?).
TCP ist ein Datenstromprotokoll, kein paketorientiertes Protokoll. Das heißt es muss nicht immer alles was zusammen gesendet wird auch gleichzeitig ankommen.

Du versuchst aber sobald der erste Teil da ist sofort das komplette Paket zu verarbeiten. Man muss schon warten bis die benötigten Daten angekommen sind, dann gehen sie auch nicht verloren.

Kennung Eins
2007-10-19, 08:20:55
Ok, ich verstehe. Und wie mache ich das? Wie kriege ich raus, dass eine Nachricht komplett ist? Jedes Paket mit einem Index versehen geht ja nicht, weil die eigentliche "Paketverschickung" für mich transparent ist.

ScottManDeath
2007-10-19, 08:48:58
Könntest Du nicht mit .NET Remoting direkt Methoden auf den Server Objekten aufrufen, und Dir somit das manuelle parsen sparen?

Kennung Eins
2007-10-19, 08:53:35
Ui .. keine Ahnung, noch nie davon gehört - hast du nen (brauchbaren) Link oder Buchtipp?

Grestorn
2007-10-19, 09:37:27
Ui .. keine Ahnung, noch nie davon gehört - hast du nen (brauchbaren) Link oder Buchtipp?

Suche in der .NET Doku nach Remoting. Ist wirklich kinderleicht, auch wenn Anfangs etwas verwirrend.

Ansonsten: einfach mal bei Amazon nach .NET remoting suchen ;)

Trap
2007-10-19, 10:16:53
Ok, ich verstehe. Und wie mache ich das? Wie kriege ich raus, dass eine Nachricht komplett ist?
Üblicherweise so ähnlich wie du es schon gemacht hast: Zuerst die Länge verschicken, auf der Empfängerseite die Länge nachgucken und solange warten bis alle Daten angekommen sind.

Ich würd aber auch empfehlen sowas nicht selber zu schreiben.

Gast
2007-10-19, 10:21:06
Ok, ich verstehe. Und wie mache ich das?

Indem du z.B. auf Anwendungslogik (also die Strings, die du verschickst) deine Nachrichten mit einem Zeichen terminierst. Du kannst nicht davon ausgehen, dass beim Lesen des Socketstream alles drin steht (auch wenn das oft funktioniert). D.h. maximal nur, dass du eben den Stream komplett auslesen kannst und was dort drin steht in der richtigen Reihenfolge steht. Ob aber noch weitere Daten folgen, weiß der Socket ja nicht. Du musst also so lange aus dem Stream lesen (und inkrementell zusammenfügen, z.B. über einen StringBuilder) bis dann auch das Zeichen zur Terminierung deiner Nachricht angekommen ist.

Kennung Eins
2007-10-19, 10:22:05
Hmm .. in einem anderen Projekt kommunizieren zwei von den oben beschriebenen 4 Systemen mit einem anderen Rechner, der nicht .NET macht. D.h. hier würde ich nicht das .NET remoting benutzen können und muss doch auf herkömmliches TCP zurückgreifen.

Remoting schaue ich mir auf jeden Fall an, aber für den nicht-.NET-Fall muss ich (?) scheinbar bei TCP bleiben, richtig?

Kennung Eins
2007-10-19, 10:23:36
Indem du z.B. auf Anwendungslogik (also die Strings, die du verschickst) deine Nachrichten mit einem Zeichen terminierst. Du kannst nicht davon ausgehen, dass beim Lesen des Socketstream alles drin steht (auch wenn das oft funktioniert). D.h. maximal nur, dass du eben den Stream komplett auslesen kannst und was dort drin steht in der richtigen Reihenfolge steht. Ob aber noch weitere Daten folgen, weiß der Socket ja nicht. Du musst also so lange aus dem Stream lesen (und inkrementell zusammenfügen, z.B. über einen StringBuilder) bis dann auch das Zeichen zur Terminierung deiner Nachricht angekommen ist.
Das klingt gut :) danke

TheGamer
2007-10-19, 13:15:19
Das klingt gut :) danke

Du kannst wenn du willst auch vor den eigentlichen Daten einen "Header" mitsenden in dem Infos stehen wie z.B was ist es, wie gross ist es usw.

Da der header immer gleich gross ist z.B 64 byte, weisst du das du auf 64 byte warten musst. Wenn du deine 64 byte zusammen hast schaust du wie gross die nutzdaten sind (steht ja im header) und empfaengst dann solange bis die nutzdaten da sind und machst eben was damit. Dann wartest du wieder bis du 64 byte hast und schaust nach wie was du als naechstes bekommst (du darfst natuerlich nicht wenn du einen header erwartest und MEHR bekommst den rest nicht wegwerfen weil du einen teil der nutzdaten wegschmeisst ;)).

Und so weiter. Steuerung des ganzen hab ich jetzt nicht erklaert nur so ein wenig ein Beispielprinzip.


Wenn du willst kannst du auch noch einen Hashwert mitsenden so kannst du pruefen ob einer dir reinfunkt bzw die Daten wirklich vom richtigen Server/Client kommt.

Das ganze koenntest du ein wenig umgestalten und asynchron machen (dann geht das obige auch ein wenig komfortabler meiner Meinung nach). Stichwort

NetworkStream.BeginRead
NetworkStream.BeginWrite

Gast
2007-10-19, 16:48:11
Ok, ich verstehe. Und wie mache ich das? Wie kriege ich raus, dass eine Nachricht komplett ist? Jedes Paket mit einem Index versehen geht ja nicht, weil die eigentliche "Paketverschickung" für mich transparent ist.inwiefern ist die eigentliche Paketverschickung für dich transparent? Ich bin nicht ganz sicher, ob ich dein Problem verstanden habe, aber ich würde da einen Paket-Delimiter verwenden: an das Ende jedes zu sendenden Pakets hängst du einen Text an, z.B. "</Kennung Eins seine Paketversendung Paketende>", vielleicht noch mit nem Zeilenendezeichen dabei, und auf Empfängerseite liest du solange Bytes aus dem Stream, bis dieser Delimiter dabei gewesen ist.

Kennung Eins
2007-10-19, 17:14:37
Naja, ich meine damit, dass die einzelnen durch TCP verschickten "physischen" Pakete für mich nicht sichtbar sind. Ich schicke einen String mit z.B. 1KB Daten los - aber ob der Computer daraus nun 2 oder 10 Pakete macht, von denen das 1. vielleicht als letztes ankommt, das weiß ich nicht.

Gast
2007-10-19, 18:00:12
Naja, ich meine damit, dass die einzelnen durch TCP verschickten "physischen" Pakete für mich nicht sichtbar sind. Ich schicke einen String mit z.B. 1KB Daten los - aber ob der Computer daraus nun 2 oder 10 Pakete macht, von denen das 1. vielleicht als letztes ankommt, das weiß ich nicht.und genau für solche Zwecke bietet sich ein Delimiter an, den du an dein 1 KB Paket anhängst, damit auf Empfängerseite überprüft werden kann, ob das Paket da ist. Auf Empfängerseite kann man dazu folgenden Algorithmus verwenden (Socket-Programmierung in C# hab ich noch nicht so oft gemacht, deswegen kann ich dir keinen richtigen Code geben):

Initialisiere einen Puffer mit der Länge 0
Wiederhole:
lies ein Byte aus dem Stream
hänge das ausgelesene Byte an den Puffer an
überprüfe ob der Puffer den Delimiter enthält
bis Überprüfung positiv

Entferne den Delimiter aus dem Puffer, um das gewünschte Paket zu erhalten

Und so machst du das für jedes einzelne Paket.

Gast
2007-10-19, 19:05:38
und genau für solche Zwecke bietet sich ein Delimiter an,


Das wurde ja weiter oben schon gesagt.


den du an dein 1 KB Paket anhängst,


Es gibt hier in dem Sinn keine Pakete auf Anwendungsseite. Du kannst natürlich deine zu sendenden Daten als logisches Paket betrachten, die Rede war hier aber von Pakten der OSI Transportschicht. Ansonsten wurde ja alles schon gesagt. Auch die Sache mit dem Header ist interessant, wobei es etwas mehr Aufwand ist.

Gast
2007-10-19, 19:59:10
Es gibt hier in dem Sinn keine Pakete auf Anwendungsseite. Du kannst natürlich deine zu sendenden Daten als logisches Paket betrachten, die Rede war hier aber von Pakten der OSI Transportschicht.ja stimmt, Kennung Eins sprach von Text-Strings, nicht von Paketen. Irgendwie hatte ich aus Traps Posting den Paketbegriff aufgeschnappt und dann im Hinterkopf behalten. Also streiche einfach Paket und setze Text-String ;)

Gast
2007-10-19, 20:05:44
[...] aber ob der Computer daraus nun 2 oder 10 Pakete macht, von denen das 1. vielleicht als letztes ankommt, das weiß ich nicht.eine Frage tut sich mir bei der Gelegenheit noch auf: kann die Reihenfolge der von Senderseite gesendeten Bytes eine andere sein als die der empfangenen auf Empfängerseite? Ich habe nämlich durchaus schon Socketprogrammierung gemacht, und gar nicht daran gedacht, ob die Reihenfolge sich vielleicht ändern könnte, was dann ja fatal wäre...
Bitte sagt mir, daß sich die Reihenfolge nicht ändern kann, sonst kann ich nicht mehr ruhig schlafen ;(

Trap
2007-10-19, 20:27:26
Das kommt drauf an ob man TCP oder UDP Sockets benutzt:
TCP garantiert dass alles was gesendet wird in der Reihenfolge bei der Anwendung ankommt wie es gesendet wurde.
UDP gibt da keine Garantien, weder zum überhaupt ankommen noch zur Reihenfolge.

ScottManDeath
2007-10-19, 20:54:16
Für den anderen Rechner kannst Du auch unter .NET Remoting SOAP / WSDL als Transport Protokoll nehmen, über das die Remote Procedure Calls ausgeführt werden. Java sollte auch ein RMI über SOAP können. Notfalls baust Du es Dir in C++ selbst, falls Du keine C++ Implementation dafür findest. SOAP/WSDL sind XML basiert sind, d.h. Du könntest Dir Die Messages aus dem XML Stream parsen und nur das machen was Du willst.

Gast
2007-10-19, 22:41:56
Mit Java wär das nicht passiert!

Kennung Eins
2007-10-19, 23:44:21
Gut, das klingt alles recht brauchbar. Wenn ich Montag wieder auf Arbeit bin, werde ich mich daran setzen. Bis dahin schonmal danke an alle, die brauchbaren Input geliefert haben.

Mit Java wär das nicht passiert!

Wieso?

malte.c
2007-10-20, 21:36:12
Für den anderen Rechner kannst Du auch unter .NET Remoting SOAP / WSDL als Transport Protokoll nehmen, [...]. Notfalls baust Du es Dir in C++ selbst, falls Du keine C++ Implementation dafür findest.

http://www.cs.fsu.edu/~engelen/soap.html

Gast
2007-10-20, 21:50:05
Wieso?

Weil Java nicht so ein Rotz wie .NET ist.

Grestorn
2007-10-20, 21:57:16
Weil Java nicht so ein Rotz wie .NET ist.

Was soll denn der Schmarrn? Wo hätte ihm Java hier geholfen? Besonders wenn man über Sprachgrenzen hinweg operieren muss - wie hier - muss man auf jeden Fall selbst ein Protokoll für den TCP-Datenverkehr definieren.

Ist ja auch nicht wirklich was dabei.

Du hast ganz offensichtlich keine Ahnung von .NET, außer dass es von MS ist und damit sowieso Rotz sein muss, gell?

TheGamer
2007-10-20, 22:06:51
Weil Java nicht so ein Rotz wie .NET ist.

Sagt ein Gast ;)

@Grestorn lass den Troll, Troll sein und fuettere ihn nicht

Gast
2007-10-21, 02:59:40
Mampf, mmmh, ... mampf, lecker, noch mehr bitte!