Laden...

[Erledigt] TCP Verlust von Paketen oder falsche Reihenfolge möglich?

Erstellt von DerKami vor 7 Jahren Letzter Beitrag vor 7 Jahren 3.313 Views
D
DerKami Themenstarter:in
9 Beiträge seit 2016
vor 7 Jahren
[Erledigt] TCP Verlust von Paketen oder falsche Reihenfolge möglich?

Moin,

für eine Schulung wollte ich ein TCP Server/Client Beispiel programmieren.

Prinzipiell läuft der Server in einer Schleife der Prüft ob's Daten gibt, die dann in einen Buffer geschrieben wird (Bytearray).

Die ersten 4 Bytes sind immer die Länge der Nachricht, danach der Index und die Nachricht selbst.
Das Funktioniert auch soweit super solange man einzelne Nachrichten schickt.

Jetzt hab ich einfach zum Testen eine Schleife im Clienten gebaut der random 50k Nachrichten in einem 1 ms Abstand schickt.

Irgendwann geht das ganze Hops...
Manchmal gehen die 50k Nachrichten durch und manchmal nicht.
Da ist dann die Länge falsch oder der ausgelesene Index passt nicht mehr.
Und am ende habe ich weniger Bytes als minimal erfordert in Buffer die nicht mehr zu einer Nachricht zusammenzukriegen sind.

Weiß jemand vielleich was das sein könnte?
Ich arbeite nur mit TCPListener und TCPClient.

PS: Falls code benötigt wird müsste ich ggf. umschreiben oder ihr müsst mit VB vorlieb nehmen 😕

Gruß
Niko

6.911 Beiträge seit 2009
vor 7 Jahren

Hallo DerKami,

ein paar Tipps wie du das Problem lokalisieren kannst:

  1. mit dem Dubugger Haltepunkte setzen und prüfen ob dort alles korrekt ist -> siehe [Artikel] Debugger: Wie verwende ich den von Visual Studio?
  2. Tracing einbauen und markanten Stellen wichtige Daten (wie Länge, etc) ausgeben
  3. Wireshark, o.ä. verwenden um den Netzwerktraffic zu verfolgen

So kannst du das Problem (mit etwas Glück und Gespür) eingrenzen.

Ein Versuch ist eine Vorgehensweise gem. [Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden

Wenn das alles nicht zum Erfolg führt, so poste den Code.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

W
872 Beiträge seit 2005
vor 7 Jahren

TCP sichert immer die Reihenfolge ab.
Bei größeren Mengen ist es wichtig, dass Du den Buffer in einem eigenen Thread in eine Queue schreibst, da ein voller Buffer zu Requeues führt und dann schnell eine Situation entstehen kann, wo gar nix mehr geht, da die Requeues quasi alles lahmlegen.

D
DerKami Themenstarter:in
9 Beiträge seit 2016
vor 7 Jahren

Moin,

While _client.Connected
	' Prüfen ob daten da sind
	While _client.GetStream().DataAvailable
		_lastAction = Now
		b = New Byte(2048) {}
		i = _client.GetStream().Read(b, 0, b.Length)
		Dim bn(_dataIn.Length + i - 1) As Byte
		Buffer.BlockCopy(_dataIn, 0, bn, 0, _dataIn.Length)
		Buffer.BlockCopy(b, 0, bn, _dataIn.Length, i)
		_logger.WriteLine(_dataIn.Length & ";" & i & ";" & bn.Length & ";IN")
		_dataIn = bn
	End While

	' Prüfen ob das letzte Lebenszeichen vom Clienten länger als 10 Sekunden her ist und wenn nicht mal n Heartbeat senden...
	If Now.Subtract(_lastAction).TotalSeconds > 10 Then
		SendHeartbeat()
	End If

	' Falls wir an den Partner schreiben müssen, tun wir das hier.
	While _dataOut.Length > 0 And Not _isWriting
		_sw.Write(_dataOut)
		_sw.Flush()
		_dataOut = New Byte() {}
	End While

	'Code zum Abarbeiten von dataIn
	If _dataIn.Length > 3 Then
		Dim l As Integer = BitConverter.ToInt32(_dataIn, 0)
		If _dataIn.Length >= l And l > 3 Then
			ReadMsg(l)
		Else
			Console.WriteLine(Now.ToString("HH:mm:ss") & ": Not enough data in packet.")
			_logger.WriteLine("Not enough data in packet. | " & _dataIn.Length & " | " & l)
		End If
	ElseIf _dataIn.Length > 1 Then
		Console.WriteLine("Not enough data to read.")
	End If

	If _dataIn.Length = 0 And _dataOut.Length = 0 Then
		Threading.Thread.Sleep(1)
	End If

End While

Entweder bleibt der Code irgendwann beim "Not enough data in packet" hängen.
Oder stürzt wegen arithmetischer Überlauf bei "Dim l As Integer = sr.ReadInt32()" ab.

Wenn ich das Durchsteppe und jeden schritt einzeln beobachte bei manuellem senden der Pakete läuft alles wie ich es erwarte... Manuell kriege ich keine Geschwindigkeit hin um diesen Fehler überhaupt zu provozieren. Heißt wenn ich z.B. alle 250ms etwas sende, kann ich das Stundenlang machen ohne das was passiert. Ich bin auch noch am suchen... Irgendwo muss ich ja einen Fehler haben..

Edit:

Ich habe den Code etwas modifiziert und mal n Log bekommen der mich auf eine Idee gebracht hat:

0;1419;1419;IN
1419;33;1386;READ
1386;33;1353;READ
1353;33;1320;READ
1320;33;1287;READ
1287;33;1254;READ
1254;33;1221;READ
1221;33;1188;READ
1188;33;1155;READ
1155;33;1122;READ
1122;33;1089;READ
1089;33;1056;READ
1056;33;1023;READ
1023;33;990;READ
990;33;957;READ
957;33;924;READ
924;33;891;READ
891;33;858;READ
858;33;825;READ
825;33;792;READ
792;33;759;READ
759;33;726;READ
726;33;693;READ
693;33;660;READ
660;33;627;READ
627;33;594;READ
594;33;561;READ
561;33;528;READ
528;33;495;READ
495;33;462;READ
462;33;429;READ
429;33;396;READ
396;33;363;READ
363;33;330;READ
330;33;297;READ
297;33;264;READ
264;33;231;READ
231;33;198;READ
198;33;165;READ
Not enough data in packet. | 165 | 2

Die Werte repräsentieren immer wie viel grade im Inputbuffer ist, wie viel dazugekommen oder gelesen wurde und den wert nach dem Lesen oder Schreiben.
Interessanterweise ist in der Letzten zeile die Länge des Nächsten Paketes 2 (was zufällig genau der Index der eigentlich Nachricht ist).
Es sieht also so aus, als würde die Länge des Paketes fehlen...

Der Client der Sendet läuft in einem Thread und ich gebe von außen die Befehle zum schreiben...

Die Whileschleife sieht genauso aus bis auf folgende Kleinigkeit:

While _dataOut.Length > 0 And Not _isWriting
	_isWriting = True
	_sw.Write(_dataOut)
	_sw.Flush()
	_dataOut = New Byte() {}
	_isWriting = False
End While

Und das zum schreiben sieht so aus:

Public Sub WriteMessage(message As String)
	While (_isWriting)
		Threading.Thread.Sleep(1)
	End While
	_isWriting = True

	Dim ms As New MemoryStream()
	Dim sw As New BinaryWriter(ms)

	Dim index As Int32 = 2
	sw.Write(index)
	sw.Write(message)
	sw.Flush()
	sw.Dispose()

	DataOutAppend(BitConverter.GetBytes(Convert.ToInt32(ms.ToArray().Length + 4)))
	DataOutAppend(ms.ToArray())

	ms.Dispose()

	_isWriting = False
End Sub

Private Sub DataOutAppend(arr() As Byte)
	Dim bn(_dataOut.Length + arr.Length - 1) As Byte
	Buffer.BlockCopy(_dataOut, 0, bn, 0, _dataOut.Length)
	Buffer.BlockCopy(arr, 0, bn, _dataOut.Length, arr.Length)
	_dataOut = bn
End Sub

Hab ich da irgendein Denkfehler da drin?
Die Länge des Restbuffers lässt sich perfekt durch 33 teilen... Sieht nicht so aus als wäre das was futsch gegangen...

Gruß
Niko

W
872 Beiträge seit 2005
vor 7 Jahren

Du solltest Dein Lesen so abändern, daß Du explizit 4 Byte Länge liest und dann die genaue Länge Bytes liest. Wenn Du schnell hintereinander sendest, liest Du irgendwann mit Deinem Code mehr als eine Nachricht in Deinen 2K Buffer ein und dann ist alles kaputt. TCP ist ein Stream - wenn Du schnell hintereinander sendest, dann werden aus mehreren Write ein Receive.

D
DerKami Themenstarter:in
9 Beiträge seit 2016
vor 7 Jahren

Moin,

hab ich jetzt auch umgebaut.
Ist zwar ziemlich hässlich geworden aber macht im großen genau das was es soll:


While _client.GetStream().DataAvailable
	_lastAction = Now
	If nextRead = 0 Then
		b = New Byte(3) {}
		i = _client.GetStream().Read(b, 0, b.Length)
		If i < 4 Then
			Threading.Thread.Sleep(100)
			_client.GetStream().Read(b, i, b.Length - i)
		End If
		nextRead = BitConverter.ToInt32(b, 0) - 4
		Dim bn(_dataIn.Length + 4 - 1) As Byte
		Buffer.BlockCopy(_dataIn, 0, bn, 0, _dataIn.Length)
		Console.WriteLine(_dataIn.Length & ";" & i & ";" & bn.Length & ";IN")
		Buffer.BlockCopy(b, 0, bn, _dataIn.Length, 4)
		_dataIn = bn
	ElseIf nextRead > 0 Then
		b = New Byte(nextRead - 1) {}
		i = _client.GetStream().Read(b, 0, b.Length)
		nextRead -= i
		Dim bn(_dataIn.Length + i - 1) As Byte
		Buffer.BlockCopy(_dataIn, 0, bn, 0, _dataIn.Length)
		Buffer.BlockCopy(b, 0, bn, _dataIn.Length, i)
		Console.WriteLine(_dataIn.Length & ";" & i & ";" & bn.Length & ";IN")
		_dataIn = bn
	Else
		Console.WriteLine("NextRead: " & nextRead & "; Impossible")
		Throw New Exception("Faulty packet...")
	End If
End While

Das Handling für den Fall dass ich nicht die vollen 4 Bytes lesen kann ist Mist, ich weiß. Aber das ist auch nicht mal mein Problem, denn das Läuft ja.

Ergebnis ist wieder (mal nach 10k Paketen mal nach 50k Paketen...):
Irgendwann bin ich an so einer Stelle:

0;4;4;IN
4;29;33;IN
33;4;37;IN
NextRead: -4; Impossible
Connection to Client terminated: Faulty packet...
Client removed...

Da war _dataIn wieder leer, wurde dann einmal mit 4 Bytes gefüllt (Größe des Paketes).
Dann wurde nextRead auf 29 gesetzt. Diese wurden dann ebenfalls gelesen.
Danach wurden wieder 4 Bytes gelesen.
Und diesmal war die Größe von nextRead -4 in den 4 Bytes, was schlicht unmöglich ist.

Es sei denn dieser Code hier (Wobei ich grad bemerke dass ich da unnötig doppelt konvertiere...)


DataOutAppend(BitConverter.GetBytes(Convert.ToInt32(ms.ToArray().Length + 4)))

Ergibt irgendwann -4... Damit müsste aber ms.ToArray().Length bereits -8 ausspucken, was meines Wissens nach nicht geht...

W
872 Beiträge seit 2005
vor 7 Jahren

Da ich kein VB kann, poste ich Dir mal funktionierenden C# Code zum Lesen:


                var length = binaryReader.ReadInt32();
                var messageType = binaryReader.ReadInt32();
                if (messageType < 0)
                {
                    var logMessage = "read bas message type:" + messageType;
                    Log.Fatal(logMessage);
                    throw new Exception(logMessage);
                }

                var bytes = binaryReader.ReadBytes(length)

Das Senden schaut dann so aus:


                var body = message.ToProtobuf();
                var lengthBytes = BitConverter.GetBytes(body.Length);
                var typeBytes = BitConverter.GetBytes(message.MsgType);
                lock (sendLocker)
                {
                    binaryWriter.Write(lengthBytes);
                    binaryWriter.Write(typeBytes);
                    binaryWriter.Write(body);
                    binaryWriter.Flush();
                }

Vielleicht hilft Dir das.

D
DerKami Themenstarter:in
9 Beiträge seit 2016
vor 7 Jahren

Moin,

Ich hab jetzt jedenfalls ein paar Dinge begriffen.
Ich hab den Server komplett in C# übersetzt für den Fall da würde weiterhin was schief laufen.
Dann habe ich mir nochmal den Clienten vorgenommen und der Fehler lag Tatsächlich am Clienten.

Der TCPClient ist ja standardmäßig im Blocking-Mode.
Dann kann ich den Thread mit den Lesen beauftragen und das schreiben völlig außen vorlassen.
Das übernimmt dann ja unter Umständen ein anderer Thread, dazu muss ich nur den Lock korrekt setzen...


while (_client.Connected)
{
	_lastAction = DateTime.Now;
	var length = _sr.ReadInt32() - 4;
	_dataIn = _sr.ReadBytes(length);

	ReadMsg();
}

Vom Code ist also nicht viel übrig geblieben und er macht genau das was er soll... Ich muss nur nochmal die Länge anpassen. Die ersten 4 Bytes brauche ich nicht als Info..
Das ganze zwischenspeichern mit den Bytearrays wird dadurch komplett überflüssig.

Eine Frage hätte ich noch zu deinem Code...
was ist "sendLocker"?
Wenn ich jetzt den Code oben in einen eigenen Thread habe und von außen ein anderen Thread schreiben soll, möchte ich ja super ungern dass 2 Threads gleichzeitig schreiben. Geht ja nur in die Hose.
Muss ich dem Clienten dann ein public Objekt geben welches gelockt wird beim schreiben oder wie mache ich das am Besten?

Gruß
Niko

4.931 Beiträge seit 2008
vor 7 Jahren

sendLocker dürfte (bzw. sollte) einfach


private object sendLocker = new Object();

sein (d.h. ein privater Member derselben Klasse).

D
DerKami Themenstarter:in
9 Beiträge seit 2016
vor 7 Jahren

Dann vielen Dank für die Hilfe, ich hab meine Programme zum laufen gebracht jetzt kann ich mich um's multithreading kümmern.
Das mit dem Lock kannte ich noch gar nicht. Das macht in Zukunft einiges einfacher.

Gruß
Niko

W
872 Beiträge seit 2005
vor 7 Jahren

Der sendLocker dient dazu, dass man aus verschiedenen Threads heraus senden kann.
Damit ist der Server dann multithreading fähig.
Der Client hat bei mir einen Thread zum Lesen und schreibt dann auf eine BlockingCollection.
Der Consumer der BlockingCollection schreibt die Ergebnisse in verschiedene Rx Observables.