Laden...

Zuverlässige und dauerhafte Netzwerkverbindung (TCP/IP)

Erstellt von TTopsecret vor 9 Jahren Letzter Beitrag vor 9 Jahren 4.949 Views
T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren
Zuverlässige und dauerhafte Netzwerkverbindung (TCP/IP)

Hallo Community

Ich besitze einen Service auf einem Server und mehrere Clients in einem lokalen Netzwerk, die über TCP/IP eine dauerhafte Verbindung zum Server besitzen. Dies funktioniert auch weitgehend(wieso nicht vollständig siehe unten) und wenn der Client zum Beispiel ausgeschaltet wird, merkt das der Service sofort.

Regelmässig wird auch vom Client mit einer kurzen Nachricht überprüft, ob der Client diese noch empfangen kann. Leider wird sie aber regelmässig verschluckt(kommen nie an), trotz aktiver Verbindung oder der Service glaubt plötzlich, er habe keine Verbindung mehr, der Client jedoch schon und werden dann erst getrennt, wenn die Testnachricht nicht beantwortet wird um kurz danach wieder erfolgreich eine Verbindung aufzubauen. Verbindungsunterbrüche im Lan-Netzwerk kann weitgehend ausgeschlossen werden.

Gibt es Best Practices um eine zuverlässige und dauerhafte Verbindung über TCP/IP aufzubauen? Tipps und Tricks damit keine Nachrichten verloren gehen? Leider habe ich im Internet kaum eine brauchbare Lösungen gefunden, die das verhindern konnten.

Danke & Gruss

4.221 Beiträge seit 2005
vor 9 Jahren

Läuft der Service auf einem echten PC oder in einer VirtualMachine ?

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Der Service läuft auf einem echten PC, der sich im lokalen Netzwerk und im gleichen Gebäude befindet

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Wie setzt ihr eine solche Verbindung um, wenn eine gebraucht wird? Also konzeptionell.

Erstellt ihr für jede Nachricht eine Antwort(wenn der Server diese nicht erhält dann noch mehrfach versucht wird die gleiche Nachricht zu senden)? Ist die Wahrscheinlichkeit grösser eine Nachricht zu empfangen, wenn die Nachricht länger ist? Wird die Priorität des Thread mit der Zeit so tief gesetzt, dass er über eine halbe Minute wartet, bis diese einmal ausgeführt wird und eine Antwort erstellt?

Im Moment braucht es drei fehlgeschlagnene Testnachrichten bis der Client die Verbindung abbricht(kommt immer wieder einmal vor) und erneut versucht eine Verbindung aufzubauen, was dann auch ohne Problem erfolgreich funktioniert. Der Dienst ist im Moment noch in einer Testphase und ist mit nicht mehr als max vier Clients verbunden.

W
872 Beiträge seit 2005
vor 9 Jahren

Dein Vorgehen hört sich vernünftig an - welches Interval benutzt Du für Deine Ping Messages?

TCP ist ein zuverlässiges/reliable Protokoll - daher kann ich nicht verstehen, wie Du im LAN Nachrichten verlieren kannst. Außerdem sollte es Fehler geben, wenn eine Nachricht verschluckt wird, da TCP die Reihenfolge und garantiert, daß die Nachricht ankommt.

Hast Du ansonsten mal mit WireShark geschaut, was genau passiert? Welche Fehler bekommst Du genau? Vielleicht ist etwas falsch im Server synchronisiert.

4.221 Beiträge seit 2005
vor 9 Jahren

Bei TCP kannst Du nur genau einen Send versemmeln (der geht in's Nirvana wenn die Gengenstelle nicht mehr verfügbar ist)... wenn Du dann nochmal was sendest, dann kriegst Du einen Fehler.

Also muss das was Du beschrieben hast einen anderen Grund haben (deshalb auch meine Frage nach virtuellen Maschinen... so etwas habe ich bisher nur auf virtuellen Maschinen gesehen).

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Danke für die Antworten. Mit WireShark habe ich noch nicht nachgeprüft, aber gute Idee, werde ich einmal tun wenn mir wieder der Datenverlust auffällt. Der Ping habe ich zur Zeit auf 10 Sekunden definiert.

Ev. tritt bei der Antwort ein Fehler auf, dann werde ich ein Log erstellen müssen, der alle Netzwerkfehler(oder Programmfehler..) protokolliert. Aber sollte, wenn der Dienst die Verbindung beendet, nicht auch sofort ein Fehler beim Client auftreten?
Merkwürdig ist dabei einerseits der Client, der glaubt den Ping erfolgreich versendet zu haben und keinen Netzwerkfehler auslöst(und erst nach dreimaligen Versuch ohne Antwort dann auch die Verbindung beendet), anderseit auch, dass es immer Phasen gibt. Es kann mehrere Stunden ordentlich laufen und dann plötzlich mehrfach nacheinander den Fehler auftreten.

Edit: Könnte es noch an einem nicht beendeten Stream liegen? Muss ich einmal überprüfen

4.221 Beiträge seit 2005
vor 9 Jahren

Wie liest Du die Daten Synchron in einem eigenen Thread oder Asynch ?

Ich vermute eher, dass Du beim AUSLESEN ein Problem hast (dein Thread abstürzt oder Du keinen neuen Asynch startest)... das hätte genau den Effekt... Socket bleibt offen (deshalb merkt der Client nichts)... Und Du erhälst zwar die Daten... merkst es aber nicht ...

Nachtrag:
Wenn der Socket auf der Empfängerseite zu wäre... dann könntest Du je nach Konstellation 1 oder gar kein Paket mehr senden ohne auf der Senderseite eine Exception zu erhalten

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Lesen und schreiben wird auf beiden Seiten in eigenen Threads ausgeführt, weil beim BinaryWriter/-Reader, die benutzt werden, die Asynchronmöglichkeit nicht zur Verfügung steht. Auch wenn Fehler auftreten sollten ist die Anwendung aber Intelligent genug nicht einfach abzustürzen, sondern es wird im Ping Intervall erneut versucht eine Verbindung aufzubauen.

Welchen Writer/Reader würdet ihr empfehlen? StreamReader?

4.221 Beiträge seit 2005
vor 9 Jahren

Ich schrieb auch nicht dass die Anwendung abstürzt... sondern dein Lese-Thread...

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Entschuldige. Falls tatsächlich der Lesethread abstürzen sollte, wird der Stream und Socket des betreffenden Clients geschlossen und wartet bis der Client erneut versucht eine neue Verbindung zum Dienst aufzubauen(der Client bemerkt dabei jedoch nicht sofort vom Unterbruch und wartet bis die drei Ping-Versuche fehlschlagen). Ich habe nun eine Log-Datei erstellt und beobachte, wo sich der entscheidende Fehler ereignen könnte, falls er sich im Dienst befindet.

4.221 Beiträge seit 2005
vor 9 Jahren

TCP verschluckt nichts !

Es gibt folgende Möglichkeiten:

  1. Du meinst nur dass Du was sendest 😃 (z.:B bleibt alles lokal im Puffer)
  2. Auf der Empfängerseite ist der Socket offen aber niemand interessierts

Ich wiederhole nochmal:

Wenn auf der Empfängerseite kein offener Socket mehr ist, dann kannst Du entweder gar nichts mehr senden und kriegst sofort eine Exception... oder Du kannst genau 1 mal senden und beim 2 ten senden kriegst du eine Exception

Dies ist davon abhängig ob ein Socket ordentlich geschlossen wurde... oder ob nur unterwegs irgendwo etwas abgeraucht ist...

Ich habe schon x grosse Server mit TCP geschrieben welche in hoher Frequenz mit verschiedenen Clients / SPS-Steuerungen Daten austauscht... ich habe also ein klein wenig Erfahrung in dem Gebiet.

Poste doch mal ein wenig Code von der Sende und Empfangsseite... ev. ist es ja ein offensichtlicher Fehler 😃

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

Habe das Problem durch den Logger gefunden, es lag nicht am Socket oder der Netzwerkklasse sondern hat sich woanders in der Anwendung eingeschlichen. Hatte jedoch dann Einfluss auf den Ablauf der Netzwerkklassen. Trotzdem danke für die Hilfe 👍

4.221 Beiträge seit 2005
vor 9 Jahren

Also blieb im Endeffekt der Socket offen (aber niemand hat mehr die Daten verarbeitet) ?

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

T
TTopsecret Themenstarter:in
34 Beiträge seit 2014
vor 9 Jahren

(Können den hier keine Spoiler gesetzt werden?)

Im Endeffekt, ja. Im weitergehenden Ereignis Add-/RemoveMachine in der newClient Methode gab es in einigen Fällen eine Exception zurück, die dann es erst dort behandelt wurde. Dadurch wurde das Hinzufügen abgebrochen, wenn der Client eine neue Verbindung aufbauen wollte. Ich hatte leider nicht mehr dran gedacht, die Verbindung im catch wieder richtig zu schliessen.

Der Dienst überprüft am Abend, ob noch ein Client angemeldet ist, wenn nein wird der Strom im Gebäude abgeschaltet, wenn ja wird eine Shutdown-Meldung an den Client gesendet, die er dann abbrechen kann, wenn er weiterarbeiten möchte. Deshalb den Projekttitel ShutdownService.

Programmierhans meinte auf Anfrage, es mache nichts aus wenn ich hier den Netzwerk-Code poste. Ich bin offen für Kritik, Verbesserungsvorschläge usw, nur drauf los. Bisher gab es leider Niemand, der mir mitteilen konnte, auf was ich noch alles achten sollte.

using ShutdownService.Helper;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace ShutdownService.Server
{
    /// <summary>
    /// Verwaltet die Verbindungen zu allen Clients.
    /// </summary>
    public class TcpServer
    {
        #region Variable
        /// <summary>
        /// Gibt die dazugehörende Connection zur IP zurück.
        /// </summary>
        /// <param name="machineIp">Die IP-Adresse zu der Verbindung.</param>
        /// <returns>Die Connection zur IP.</returns>
        public TcpConnection this[IPAddress machineIp]
        {
            get
            {
                return connectionList.Find(x => x.GetIpAddress().ToString() == machineIp.ToString());
            }
        }

        /// <summary>
        /// IP Adresse des Servers.
        /// </summary>
        public IPAddress ServerIP { get; private set; }

        /// <summary>
        /// Port des Servers.
        /// </summary>
        public int ServerPort { get; set; }

        /// <summary>
        /// True/False ob der Server läuft.
        /// </summary>
        public bool ServerActive { get; private set; }

        /// <summary>
        /// Die Liste speichert alle aktuelle Verbindungen.
        /// </summary>
        private List<TcpConnection> connectionList;

        /// <summary>
        /// Der Hörer als Thread.
        /// </summary>
        private Thread thrListener;

        /// <summary>
        /// Der Listener nimmt alle neue eingehende Verbindungen ab.
        /// </summary>
        private TcpListener tcpListener;

        /// <summary>
        /// Event: Wenn eine neue Verbindung hergestellt wurde und kann hier weitergeleitet werden.
        /// </summary>
        public event EventHandler<ObjectEventArgs<TcpConnection>> AddedMachine;

        /// <summary>
        ///Event: Wird ausgelöst, wenn eine Verbindung getrennt wurde und kann hier weitergeleitet werden.
        /// </summary>
        public event EventHandler<ObjectEventArgs<TcpConnection>> RemovedMachine;

        /// <summary>
        /// Event: Wird ausgelöst, wenn sich ein neuer Computer beim Server meldet.
        /// </summary>
        private event EventHandler<ObjectEventArgs<TcpClient>> OnClientConnected;

        /// <summary>
        /// Event: Nachricht eines Clients weitergeben.
        /// </summary>
        public event EventHandler<ObjectEventArgs<string[]>> ReceivedMessage;

        #endregion

        #region Basic

        /// <summary>
        /// Konstruktor: Erstellt einen neuen TcpServer.
        /// </summary>
        /// <param name="port">Der Port, auf dem der Server laufen soll.</param>
        public TcpServer(int port)
        {
            ServerPort = port;
            ServerIP = IPAddress.Any;
            connectionList = new List<TcpConnection>();
            OnClientConnected += newClient;
            ServerActive = false;
            thrListener = new Thread(listen);
        }

        /// <summary>
        /// Startet den Server.
        /// </summary>
        public void Start()
        {
            if(!ServerActive)
            {
                ServerActive = true;
                tcpListener = new TcpListener(IPAddress.Any, ServerPort);
                tcpListener.Start();
                thrListener.Start();
                Logger.LogMessage("TcpServer gestartet");
            }
        }

        /// <summary>
        /// Alle Verbindungen des Servers trennen.
        /// </summary>
        public void Stop()
        {
            ServerActive = false;
            tcpListener.Stop();
            foreach(TcpConnection con in GetAllConnection())
            {
                RemoveMachine(con);
            }
            Logger.LogMessage("TcpServer gestoppt");
        }

        /// <summary>
        /// Beendet den Server, falls er läuft und startet ihn neu. Falls dies fehlschlägt, wird der Port auf Standard gesetzt (53456)
        /// </summary>
        public void Restart()
        {
            try
            {
                if(ServerActive)
                    Stop();
                Start();
            }
            catch(Exception e)
            {
                ServerPort = 53456;
                Logger.LogMessage(e);
                Logger.LogMessage("Server konnte nicht mit den Einstellungen neu gestartet werden. Port wird auf " + ServerPort + " gesetzt.");
                Start();
            }
        }

        #endregion

        #region ConnectionsManage

        /// <summary>
        /// Fügt einen neuen Computer in der Liste dazu.
        /// </summary>
        /// <param name="con">Die Verbindungsdaten der Machine.</param>
        private void AddMachine( TcpConnection con )
        {
            if(con == null)
                return;
            connectionList.Add(con);
            if(AddedMachine != null)
                AddedMachine(this, new ObjectEventArgs<TcpConnection>(con));
            Logger.LogMessage(con.GetIpAddress() + " hinzugefügt");
        }

        /// <summary>
        /// Löscht ein Computer aus der Liste, wenn entweder die Verbindung verloren geht oder der Computer abgeschalten wird.
        /// </summary>
        /// <param name="con">Die Verbindungsdaten der Machine.</param>
        private void RemoveMachine( TcpConnection con )
        {
            if(con == null)
                return;
            try
            {
                connectionList.RemoveAll(x => x.GetIpAddress().ToString() == con.GetIpAddress().ToString());
                if(RemovedMachine != null)
                    RemovedMachine(this, new ObjectEventArgs<TcpConnection>(con));
                Logger.LogMessage(con.GetIpAddress() + " entfernt");
                con.Close();
                con = null;
            }
            catch(Exception e)
            {
                Logger.LogMessage(e);
            }
        }

        /// <summary>
        /// Sendet eine Nachricht an einen bestimmten Computer.
        /// </summary>
        /// <param name="machineName">Die Maschine, an die die Nachricht gehen soll.</param>
        /// <param name="message">Die Nachricht, welche übertragen werden soll.</param>
        public void SendMessageToMachine( IPAddress machineIp, string message )
        {
            if(String.IsNullOrEmpty(message) || machineIp == null)
                return;
            TcpConnection con = this[machineIp];
            if(con != null)
                con.SendMessage(message);
            Logger.LogMessage(message + " wurde an " + con.GetIpAddress() + "gesendet");
        }

        /// <summary>
        /// Sendet eine Nachricht an alle Computer.
        /// </summary>
        /// <param name="message">Die Nachricht, welche übertragen werden soll.</param>
        public void SendMessageToAllMachine( string message )
        {
            if(String.IsNullOrEmpty(message))
                return;
            foreach(TcpConnection con in GetAllConnection())
                SendMessageToMachine(con.GetIpAddress(), message);
        }

        /// <summary>
        /// Nachricht vom Client auswerten.
        /// </summary>
        private void evaluationMessage( object sender, ObjectEventArgs<string> e )
        {
            string message = e.Obj;
            TcpConnection con = (TcpConnection)sender;
            if(String.IsNullOrEmpty(message) || con == null)
                return;
            string[] temp = message.Split(new Char [] { '|' });
            // Hostname wird gespeichert
            if(temp[0] == "HOSTNAME")
            {
                TcpConnection tempCon = this[con.GetIpAddress()];
                tempCon.HostName = temp[1];
                int index = connectionList.FindLastIndex(x => x.GetIpAddress().ToString() == tempCon.GetIpAddress().ToString());
                if(index != -1)
                    connectionList[index] = tempCon;
                if(AddedMachine != null && RemovedMachine != null)
                {
                    RemovedMachine(this, new ObjectEventArgs<TcpConnection>(con));
                    AddedMachine(this, new ObjectEventArgs<TcpConnection>(tempCon));
                }
            } else if(ReceivedMessage != null)
                ReceivedMessage(sender, new ObjectEventArgs<string[]>(temp));
        }

        /// <summary>
        /// Wartet auf eingehende Verbindungen.
        /// </summary>
        private async void listen()
        {
            while(ServerActive)
            {
                try
                {
                    TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
                    if(OnClientConnected != null)
                        OnClientConnected(this, new ObjectEventArgs<TcpClient>(tcpClient));
                } 
                catch
                {
                    if(ServerActive)
                        Restart();
                }
            }
        }

        /// <summary>
        /// Verwaltet die neu eingegangene Verbindung.
        /// </summary>
        private void newClient( object sender, ObjectEventArgs<TcpClient> e )
        {
            try
            {
                if(this[((IPEndPoint)e.Obj.Client.RemoteEndPoint).Address] != null)
                    RemoveMachine(this[((IPEndPoint)e.Obj.Client.RemoteEndPoint).Address]);
                TcpConnection newConnection = new TcpConnection(e.Obj);
                newConnection.ReceiveMessage += evaluationMessage;
                newConnection.ConnectionLost += clientConnectionLost;
                AddMachine(newConnection);
            }
            catch(Exception b)
            {
                e.Obj.Close();
                Logger.LogMessage(b);
            }
        }

        /// <summary>
        /// Die Verbindung zum Client ist abgebrochen.
        /// </summary>
        private void clientConnectionLost(object sender, EventArgs e)
        {
            RemoveMachine((TcpConnection)sender);
        }

        #endregion
    }
}

ObjectEventArgs ist ein von mir erstelltes EventArgs, der einfach eine einzige beliebige typsichere Variable mit übergeben kann


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ShutdownService.Helper;
using System.Threading.Tasks;

namespace ShutdownService.Server
{
    /// <summary>
    /// Alle Verbindungsdaten und Streams zu einem bestimmten Client.
    /// </summary>
    public class TcpConnection
    {
        /// <summary>
        /// Der Client der Verbindung.
        /// </summary>
        private TcpClient tcpClient;

        /// <summary>
        /// Über diesen Kanal kommen die eingehenden Nachrichten.
        /// </summary>
        private BinaryReader strReader;

        /// <summary>
        /// Über diesen Kanal wird am Benutzer eine Nachricht gesendet.
        /// </summary>
        private BinaryWriter strWriter;

        /// <summary>
        /// Stream, über den die Verbindung läuft.
        /// </summary>
        private NetworkStream netStream;

        /// <summary>
        /// Lese-Thread.
        /// </summary>
        private Thread thrReader;

        /// <summary>
        /// Event: Die eingehenden Nachrichten werden weitergegeben.
        /// </summary>
        public event EventHandler<ObjectEventArgs<string>> ReceiveMessage;

        /// <summary>
        /// Event: Wird ausgelöst, wenn der Client keine Verbindung mehr zum Service besitzt.
        /// </summary>
        public event EventHandler ConnectionLost;

        /// <summary>
        /// Der Computername der Verbindung.
        /// </summary>
        public string HostName = "Unbekannt";

        /// <summary>
        /// Konstruktor: Erstellt die Verbindung.
        /// </summary>
        /// <param client="client">Der neue Client.</param>
        public TcpConnection(TcpClient client)
        {
            tcpClient = client;
            netStream = tcpClient.GetStream();
            strReader = new BinaryReader(netStream);
            strWriter = new BinaryWriter(netStream);
            thrReader = new Thread(clientListening);
            thrReader.Start();
        }

        /// <summary>
        /// Gibt die IP-Adresse zurück.
        /// </summary>
        public IPAddress GetIpAddress()
        {
            return ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
        }

        /// <summary>
        /// Schliesst die Verbindung.
        /// </summary>
        public void Close()
        {
            try
            {
                if(netStream != null)
                    netStream.Close();
            }
            finally
            {
                netStream = null;
            }
            tcpClient.Close();
        }

        /// <summary>
        /// Sendet am Benutzer eine Nachricht.
        /// </summary>
        /// <param name="message">Die Nachricht, welche an den Client gesendet werden soll.</param>
        public void SendMessage(string message)
        {
            try
            {
                if(!String.IsNullOrEmpty(message))
                {
                    // $ wird als Trennzeichen zwischen den Nachrichten genutzt, damit auch wenn die Nachrichten dann zusammen gesendet werden, am Ziel noch getrennt werden können.
                    message = message.Replace("$", "") + "$";
                    strWriter.Write(message);
                    strWriter.Flush();
                }
            }
            catch
            {
                if(ConnectionLost != null)
                    ConnectionLost(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Der Stream wird auf eingehende Nachrichten abgehört und die Daten dann weitergegeben.
        /// </summary>
        private void clientListening()
        {
            try
            {
                while(true)
                {
                    string returnData = strReader.ReadString();
                    if(returnData == null)
                        throw new Exception("Verbindung verloren");
                    string[] testData = returnData.Split(new Char[] { '$' });
                    if(testData[0] == "1")
                    {
                        SendMessage("3");
                        continue;
                    }
                    foreach(string i in testData)
                        if(!string.IsNullOrEmpty(i))
                            if(ReceiveMessage != null)
                                ReceiveMessage(this, new ObjectEventArgs<string>(i));
                }
            }
            catch
            {
                if(ConnectionLost != null)
                    ConnectionLost(this, EventArgs.Empty);
            }
        }
    }
}