Autor Beitrag
OlafSt
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Mo 11.09.17 11:51 
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:
ausblenden 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
ausblenden 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 ?

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Mo 11.09.17 12:08 
Hallo,

schon in die Doku geschaut: TcpClient.Connected? Wenn noch keine Nachricht gesendet oder empfangen wurde, ist diese Eigenschaft false.
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Mo 11.09.17 12:28 
Jop:

ausblenden 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
   }
}

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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:

ausblenden 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:
ausblenden 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();
  }
}

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Frühlingsrolle
Ehemaliges Mitglied
Erhaltene Danke: 1



BeitragVerfasst: Di 12.09.17 14:46 
- Nachträglich durch die Entwickler-Ecke gelöscht -
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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:

ausblenden volle Höhe 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:
ausblenden volle Höhe 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:

ausblenden 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.

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Frühlingsrolle
Ehemaliges Mitglied
Erhaltene Danke: 1



BeitragVerfasst: Di 12.09.17 17:37 
- Nachträglich durch die Entwickler-Ecke gelöscht -
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Di 12.09.17 18:26 
Der Fehler entsteht hier:
ausblenden C#-Quelltext
1:
c.Client.Send(GUIThreadData.SerializeToArray(GTD));					


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

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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:
ausblenden Error
1:
2:
Unbehandelte Ausnahme: System.ObjectDisposedException: Auf das verworfene Objekt kann nicht zugegriffen werden.
Objektname: "System.Net.Sockets.Socket".


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


wenn folgendes abläuft:


  • Client verbindet sich
  • 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, stellt sie in die ReceiveData-Queue und löst einen Event aus
  • Der Mainthread des Servers verarbeitet diese Antwort und erzeugt eine Gegen-Antwort
  • Diese Gegen-Antwort, die der MainThread erzeugt hat, wird in die GUIEventData-Queue gesteckt, die unser Problem-Thread dann abarbeitet
  • Der Thread möchte die Antwort aus der GUIEventData-Queue an den entsprechenden Client senden. Dann erfolgt diese Exception, weil der Socket nicht mehr existiert.


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:

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Mi 13.09.17 10:30 
In Zeile 18 deiner geposteten CheckReceive()-Methode schließt du doch den Stream - und damit die Verbindung:
ausblenden C#-Quelltext
1:
ns.Close();					

(bzw. zusätzlich noch durch das using in Zeile 14)
:roll:
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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.

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Frühlingsrolle
Ehemaliges Mitglied
Erhaltene Danke: 1



BeitragVerfasst: Mi 13.09.17 15:28 
- Nachträglich durch die Entwickler-Ecke gelöscht -