Entwickler-Ecke

Netzwerk - TcpClient ist geschlossen, wenn ich was senden will


OlafSt - Mo 11.09.17 11:51
Titel: TcpClient ist geschlossen, wenn ich was senden will
Hallo Freunde,

ich habe hier ein seltsames Problem.

Kurz umrissen habe ich einen Thread (kein Task), der mit Hilfe eines TcpListeners auf eingehende Verbindungen lauert. Wenn sich eine Verbindung aufbaut, wird der dazu passende tcpCLient in einer ConcurrentBag gespeichert. Die Anwendung, die diesen TcpListener erstellt, schickt nun nach gusto irgendwelche Daten an alle verbundenen tcpClients. Das funktioniert auch soweit ganz hervorragend.

Nun möchte diese Anwendung aber an einen spezifischen tcpClient etwas senden. Meine Idee war es nun, das tcpClient-Objekt umher zu reichen. Das tcpClient-Objekt wird auch nicht zerstört oder sonstwie modifiziert, es wird nur herumgereicht. Bis es zum Sendevorgang kommt.

Ich prüfe in der Sende-Warteschlange, ob diese Nachricht spezifisch ist. Ist dem So, wird in der ConcurrentBag der passende tcpClient gesucht. Ein Vergleich mittels "tcpClient.Client.Handle == " funktioniert und ich könnte nun Daten senden.

Aber:

C#-Quelltext
1:
2:
if (c.Connected)
   c.Client.Send(TheData.ToArray);


c.Connected ist immer false, die Verbindung also geschlossen.

Ich habe schon mit

C#-Quelltext
1:
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);                    


was versucht, aber ohne Erfolg.

Warum ist die Verbindung denn geschlossen ?!? Ich rufe nirgendwo ein Close auf (weder Client- noch Serverseitig), für ein Timeout ist die Anfangsphase des Handshaking zwischen den beiden viel zu aktiv, so das dort keines aufkommen kann.

Ich bin ratlos. Hat einer von euch eine Idee ?


Th69 - Mo 11.09.17 12:08

Hallo,

schon in die Doku geschaut: TcpClient.Connected [https://msdn.microsoft.com/de-de/library/system.net.sockets.tcpclient.connected(v=vs.110).aspx]? Wenn noch keine Nachricht gesendet oder empfangen wurde, ist diese Eigenschaft false.


OlafSt - Mo 11.09.17 12:28

Jop:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
private void Receive(CancellationToken ct)
{
   TcpClient tcpClient;

   listener.Start();
   while (!ct.IsCancellationRequested)
   {
     if (listener.Pending())
     {
       tcpClient = listener.AcceptTcpClient();
       //Vllt hilft uns das KeepAlive ?
       tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
       Clients.Add(tcpClient);
       //Sende Request raus
       tcpClient.Client.Send(Data.ToArray());
      }
      //Bissel anderer Verwaltungskram hier
   }
}


Th69 - Mo 11.09.17 13:39

OK, aber ist denn c.Connected == true direkt nach dem Senden?
Zusätzlich kannst du ja mal in einem eigenen Timer (oder Thread/Task) diese Eigenschaft sekündlich ausgeben lassen, ob und wann sich diese ändert.

Verbindet sich denn währenddessen ein anderer TcpClient (und wird dann vllt. die Eigenschaft der anderen TcpClients wieder zurückgesetzt)?


OlafSt - Di 12.09.17 13:23

Womöglich ist dieses Problem völlig anders gelagert.

Die Daten, die dort hin- und her gehen, werden durch einen XMLSerializer erzeugt. Folgende Klasse soll serialisiert / deserialisiert werden:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
    [Serializable()]
    public class GUIThreadData
    {
        //GUIThread EventTypes
        public enum GUIThreadEventType
        {
            GTET_NONE,
            GTET_DEBUG,
            GTET_LOGIN,
            GTET_REQUEST,
            GTET_INFO
        }
        //GUIThread EventSubTypes
        public enum GUIThreadEventSubType
        {
            GTST_NONE,
            GTST_STARTUP,
            GTST_SHUTDOWN,
            GTST_SERIAL
        }

        public GUIThreadEventType EventType;
        public GUIThreadEventSubType EventSubType;
        public byte EventDataByte;
        public int EventDataInt;
        public double EventDataDouble;
        public string EventInfo;
        [NonSerialized()] public object UserData;
        //der Rest sind Konstruktoren und Verwaltungskram
}


Beim Serialisieren wird nun eine Exception geworfen, das TcpClient ein unerwarteter Typ sei. Doch das entsprechende Feld ist doch mit NonSerialized gekennzeichnet ? Womöglich ist dieses Kennzeichen verkehrt ?

Der Serializer-Aufruf selbst ist unspektakulär:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
public static byte[] SerializeToArray(GUIThreadData GTD)
{
  using (MemoryStream ms = new MemoryStream())
  {
    XmlSerializer xml = new XmlSerializer(typeof(GUIThreadData));
    xml.Serialize(ms, GTD);
    return ms.ToArray();
  }
}


Delete - Di 12.09.17 14:46

- Nachträglich durch die Entwickler-Ecke gelöscht -


Ralf Jansen - Di 12.09.17 15:04

Das NonSerialized Attribut gehört zu den Formattern (z.B. dem BinaryFormatter für binäres serialisieren) der XmlSerializer reagiert auf andere Attribute. Hier zum Beispiel wäre XmlIgnore eher das Richtige.

Moderiert von user profile iconTh69: C#-Tags hinzugefügt


OlafSt - Di 12.09.17 17:21

Ich zeige mal den Code, ich sehe den Wald vor Bäumen nicht.

Das wesentliche dieser Assembly ist der Thread-Code:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
public void Run()
{
    GUIReceiverThread = new Thread(new ThreadStart(PreReceive));
    GUIReceiverThread.Start();
}

private void PreReceive()
{
    //Das CancellationTokenSource wird im Konstruktor erzeugt

    Receive(ct.Token);
}

private void Receive(CancellationToken ct)
{
    //Dies ist die eigentliche Thread-Methode
    TcpClient tcpClient;

    listener.Start();
    while (!ct.IsCancellationRequested)
    {
        //Falls einer was gesendet hat...
        CheckReceive();

        if (listener.Pending())
        {
            tcpClient = listener.AcceptTcpClient();
            tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
            Clients.Add(tcpClient);
            //Wir müßten eigentlich einen Event bzgl. Connect auslösen
            //Sende Request for Serial raus
            GUIThreadData GTD = new GUIThreadData();
            GTD.EventType = GUIThreadData.GUIThreadEventType.GTET_REQUEST;
            GTD.EventSubType = GUIThreadData.GUIThreadEventSubType.GTST_SERIAL;
            tcpClient.Client.Send(GUIThreadData.SerializeToArray(GTD));
            continue;
        }

        if (!GUIEvent.WaitOne(100))
            continue;

        //Ohne Clients brauchen wir weder was senden noch empfangen
        if (Clients.Count < 1)
            continue;

        //Wenn nix zu senden ist, weg hier
        if (GUIEventData.Count < 1)
            continue;

        //Send out all the Data to all the clients
        while (GUIEventData.Count > 0)
        {
            if (GUIEventData.TryDequeue(out GUIThreadData GTD))
            {
                foreach (TcpClient c in Clients)
                {
                    //Manche Dinge dürfen nur an einen speziellen Client gesendet werden
                    if (GTD.UserData != null)
                    {
                        TcpClient cc = GTD.UserData as TcpClient;
                        if (cc.Client.Handle != c.Client.Handle)
                        {
                            //Wenn es nicht der richtige ist, dann gleich weiter
                            //zum nächsten Client
                            continue;
                        }
                    }
                    c.Client.Send(GUIThreadData.SerializeToArray(GTD));
                }
            }
        }
        //else
        //    Thread.Sleep(100);
    }
    listener.Stop();
}


Nun die Methode CheckReceive, die für das Empfangen von Nachrichten zuständig ist:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
private void CheckReceive()
{
    byte[] buffer;
    List<GUIThreadData> GTL;

    //Prüfen, ob irgendeiner der Clients uns was zugeschickt hat
    foreach (TcpClient c in Clients)
    {
        if (!c.Client.Connected)
            continue;
        if (c.Available < 1)
            continue;
        buffer = new byte[c.Available];
        using (NetworkStream ns = c.GetStream())
        {
            ns.Read(buffer, 0, c.Available);
            string Message = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
            ns.Close();
            //GUIThreadData draus machen
            GTL = GUIThreadData.DeSerializeFromString(Message);
            //Die einzelnen GTDs in die OUT-Queue schieben und dann Event auslösen
            //So kann der Ersteller des Threads auf diese Daten reagieren - oder auch nicht.
            foreach (GUIThreadData gtd in GTL)
            {
                //Möglich, das der Theradersteller darauf antworten will, also geben wir
                //den TcpClient als object mit
                gtd.UserData = c;
                ReceiveData.Enqueue(gtd);
            }
            OnReceived(new EventArgs());
        }                
    }
}


Und zum Schluß noch das OnReceive-Event:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
private void OnReceived(EventArgs e)
{
    /*var tmp = OnReceiveData;
    if (tmp != null)
        tmp(this, e);
    */

    OnReceiveData?.Invoke(this, e);
}


Die Liste "Clients" ist eine einfache ConcurrentBag<TcpClient>, GUIEventData und ReceiveData sind simple ConcurrentQueue<GUIThreadData>.

Mit diesem simplen Thread nimmt der Dienst seine Kommunikation mit der Außenwelt auf. Das funktioniert hervorragend, bis ich auf etwas empfangenes eine Antwort senden will. Ergo:

- Server sendet einen Request (wie man direkt nach dem AcceptClient sehen kann)
- Dieser Request kommt am Client an und wird korrekt verarbeitet.
- Der Client sendet eine Antwort zurück (hier: Eine Nummer zur Prüfung)
- Der Server empfängt diese Antwort korrekt und verarbeitet diese
- Der Server will nun entweder sagen: "Jo, des is gut" oder "nee, lass ma lieber". Dann erfolgt diese Exception, weil der Socket nicht mehr existiert.

Oh, ehe ich es vergesse: Ja, das ist alles Public. Ich hatte zuerst mit einer Struct gedanklich gespielt, es dann aber doch mit einer Klasse gemacht. GUIThreadData ist eine reine Klasse zum Daten umhertransportieren, keine Logik. Vielleicht ist eine struct da wirklich besser geeignet ? Bin für Vorschläge offen.


Delete - Di 12.09.17 17:37

- Nachträglich durch die Entwickler-Ecke gelöscht -


OlafSt - Di 12.09.17 18:26

Der Fehler entsteht hier:

C#-Quelltext
1:
c.Client.Send(GUIThreadData.SerializeToArray(GTD));                    


Das Umstellen von object auf TcpClient hat nicht geholfen. War aber eine gute Idee.


Th69 - Mi 13.09.17 09:34

Geht es hier um die Serialisieren-Exception, dann hat Ralf Jansen das doch schon beantwortet?!

Ansonsten poste mal den genauen Text der Exception.


OlafSt - Mi 13.09.17 09:47

Ohje, da ist etwas durcheinander geraten.

Also: Die Exception durch den Serializer war nur eine mögliche Spur - durch eure Hilfe mit dem XmlIgnore-Attribut konnte diese Exception erfolgreich eliminiert werden. Hat aber mein Problem nicht gelöst.

Nach wie vor entsteht diese Exception:

Error
1:
2:
Unbehandelte Ausnahme: System.ObjectDisposedException: Auf das verworfene Objekt kann nicht zugegriffen werden.
Objektname: "System.Net.Sockets.Socket".


an dieser Stelle:

C#-Quelltext
1:
c.Client.Send(GUIThreadData.SerializeToArray(GTD));                    


wenn folgendes abläuft:



Man sieht, das die Kommunikation eigentlich prima funktioniert, so lange ich nicht an einen besonderen TcpClient senden will. Offenbar wird der Verweis auf den TcpClient in der GUIThreadData-Struktur irgendwann ungültig - ich weiß nur nicht, warum.

Ich hoffe, man versteht, was ich da fasel :oops:


Th69 - Mi 13.09.17 10:30

In Zeile 18 deiner geposteten CheckReceive()-Methode schließt du doch den Stream - und damit die Verbindung:

C#-Quelltext
1:
ns.Close();                    

(bzw. zusätzlich noch durch das using in Zeile 14)
:roll:


OlafSt - Mi 13.09.17 11:27

:!: :!: :idea: :evil:

Natürlich... Using ruft Dispose auf :oops: Wald, Bäume, bla.

Aber wir haben dann einen Fehler in der Dokumentation seitens Microsoft. Dort heißt es:

Zitat:
By default, closing the NetworkStream does not close the provided Socket. If you want the NetworkStream to have permission to close the provided Socket, you must specify true for the value of the ownsSocket parameter.

(Hervorhebung durch mich)

Offensichtlich stimmt das dann nicht so ganz. Oder der von TcpClient.getStream zurückgegebene NetworkStream ist anders vorbelegt.

Jetzt funktioniert es so, wie es soll. Danke für eure Hilfe, das hätte ich nie gefunden.


Delete - Mi 13.09.17 15:28

- Nachträglich durch die Entwickler-Ecke gelöscht -