TCP Client - Synchron - Asynchron

Apr 14, 2016 at 5:19 PM
Hallo Konrad,
Du konntest mir Ideen, Ansätze vermitteln.
Ich möchte es verstehen, und es sollte relativ einfach sein.

A) Synchron und Timeout
Dieser Ansatz wäre synchron, hier könnte die Features ReceiveTimout evtl. verwenden.
Ich sende etwas, wenn nach XXX Zeit keine Antwort zurückkommt, kommt es zur Exception.

B) Verbindung
Anforderung Kunden.
Wenn Client geschlossen, geöffnet wird, sollte die Verbindung automatisch erfolgen.
Daselbe gilt für den Server.
Client bleibt offen.
Anwender schließt den Server.
Anwender öffnet den Server und alles funktioniert.
Kurzum der Anwender sollte die Module Client Server gar nicht wahrnehmen.
Wie könnte ich das realisieren?

C) Logging
Dein Logger, wo schreibt dieser die Daten hin?
Nicht ins Ausgabefenster und nicht in eine Datei.

Vielen Dank.

Grüße Ulrich
public void SendMessageRequestResponse(byte[] message)
{
    // validate
    if (message == null)
        throw new ArgumentNullException(nameof(message));

    try
    {
        Logger.TraceInformation("SendMessageRequestResponse");

        var stream = _Client.GetStream();
        stream.Write(message, 0, message.Length);
        stream.Flush();

        //Byte[] bytesReceived = new Byte[8096];

        var rawMessage = new byte[BufferSize];
        var wholeMessage = new byte[BufferSize];
        bool lengthTelegram = false;
        int lengthTelegramMustBe = 0;
        int lengthTelegramIsBe = 0;
        int lengthIndexWholeMessage = 0;

        int bytesReceived = 0;

        Array.Clear(wholeMessage, 0, wholeMessage.Length);
        // Read from the IOStream
        do
        {
            bytesReceived = _IoStream.Read(rawMessage, 0, rawMessage.Length);
            if (bytesReceived > 0)
            {
                var received = rawMessage.Take(bytesReceived).ToArray();
                // To check Length MustBe = IsBe
                lengthTelegramIsBe += bytesReceived;

                if (bytesReceived > 4 && lengthTelegram == false)
                {
                    lengthTelegram = true;

                    Byte[] bytesReceiveForLength = new Byte[4];
                    System.Array.Copy(received, 0, bytesReceiveForLength, 0, 4);

                    int length = 0;   // BigEndian.
                    unchecked
                    {
                        length = (length << 8) | (byte)(received[0]);
                        length = (length << 8) | (byte)(received[1]);
                        length = (length << 8) | (byte)(received[2]);
                        length = (length << 8) | (byte)(received[3]);
                    }
                    lengthTelegramMustBe = length;

                    //lengthTelegramMustBe = BitConverter.ToInt32(bytesReceiveForLength, 0);
                    //lengthTelegramMustBe = IPAddress.HostToNetworkOrder(lengthTelegramMustBe);

                    lengthIndexWholeMessage = bytesReceived - 4;
                    System.Array.Copy(received, 4, wholeMessage, 0, lengthIndexWholeMessage);
                }
                else
                {
                    System.Array.Copy(received, 0, wholeMessage, lengthIndexWholeMessage, bytesReceived);
                    lengthIndexWholeMessage += bytesReceived;
                }

                if (lengthTelegramIsBe == lengthTelegramMustBe)
                    OnMessageReceived(new BytesReceivedEventArgs(wholeMessage));
            }
        }
        while (stream.DataAvailable);
    }
    catch (Exception ex)
    {
        // Exception while sending Message!
        Logger.TraceEvent(TraceEventType.Error, 0, "Exception when sending message. {0} / {1}", ex.Message, ex.StackTrace);
        throw;
    }
}
Coordinator
Apr 20, 2016 at 1:02 PM
Hallo Ulrich,

leider recht spät, aber ich will versuchen, auf Deine Punkte zu antworten:

a) Synchron bedeutet ja nur, dass Du Calls nutzt, die blockieren. Das ist aber aus Kundensicht doch gar nicht erkennbar und eine solche Anforderung ist eher untypisch. Wenn dies wirklich so erzwungen werden soll, dann müsste man einfach mehrere Threads verwenden, die dann halt synchrone / blockierende calls verwenden. Aber ich würde da ggf. erst einmal nachprüfen, in wie weit eine solche technische Spezifizierung wirklich existiert / sein muss.
Timeout ist relativ einfach umsetzbar.
  • Bei der asynchronen Vorgehensweise muss man bei den Verbindungen nur noch zusätzlich einen Zeitstempel ablegen. Wenn etwas empfangen wird, dann wird der auf die aktuelle Zeit gesetzt. Wenn nichts empfangen wurde, wird einfach geprüft, ob der Timeout erreicht wurde.
  • Bei der synchronen Vorgehensweise benötigt man einen zusätzlichen Thread, der die Timeouts prüft. Wenn ein Timeout angefallen ist, dann wird die Verbindung geschlossen und der blockierte Call müsste dann mit einer Exception zurück kommen.
b) Was beim Starten der Applikation passiert ist natürlich komplett offen. Den Server direkt zu öffnen oder sich direkt mit einem Server zu verbinden ist natürlich direkt möglich. Bei einer GUI Applikation sollte dies ggf. erst nach dem Aufbau der GUI erfolgen (falls Du direkt Dinge anzeigen willst / musst) oder aber gerne direkt parallel in einem eigenen Thread. Dabei aber bitte bedenken, dass bei vielen GUI Frameworks, die GUI nur aus dem eigentlichen UI Thread verändert werden kann. Aber dazu finden sich auch sehr viele Informationen (Einfach mal probieren und dann ggf. mit dem Fehler suchen wäre da mein Tipp).

c) Bezüglich Logger habe ich Dir bei Deiner anderen Nachricht geantwortet.

Viele Grüße,

Konrad
Apr 21, 2016 at 5:05 PM
Hallo Konrad,

ja Dein Projekt ist doch recht anspruchsvoll geworden.
Eine Datei fehlt.TraceSourceExtensions.cs (siehe Bild)
Kannst Du es evtl. beim nächsten Release mit hochladen.

WeifenLuo.WinFormsUI.Docking.dll Kannst Du dazu noch was sagen?

Um ein Gespür meiner Problematik zu bekommen.

Frage - Antwort Spiel.
Wenn eben keine Antwort innerhalb 1500ms soll die Maschine
 3 mal die Anfrage wiederholen 
 Sollte dann immer noch nicht eine Antwort erfolgt sein, dann auf Störung gehen.
Jetzt könnte ich das ja einfach synchron abhandeln (was ich auch machte)
Oder halt besser ;-)

Man merkt, dass Du gut programmieren kannst.

Wie würdest Du mit Deiner den Request senden, Response auswerten.
Ist ja prinzipiell egal wie die Telegramme aussehen.

Viele Grüße Ulrich.

Anlage - Ein Beispieltelegramm mit Request / Response und meine Form Problematik.
Forms, das meinte ich.
/// <summary>
/// List of all Forms.
/// </summary>
private static readonly List<Form> Forms = new List<Form>();

//Ok, das ist eine 'normale' Form
namespace System.Windows.Forms
{
    public class Form : ContainerControl
    {
       public Form();

// Da könnte ich dann eine 'normale' Form übergeben, denke, das sehe ich richtig.      
private void OnOpenFormClick(object sender, System.EventArgs e)
{
    var newForm = new ExampleForm();
    FormsApplication.AddForm(newForm);
    newForm.Show();
}

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<root>
    <header eventId="558" eventName="RequestToDo" version="1.0" timeStamp="2016-04-03T01:18:36.4359664+01:00" >
        <location lineNo="2" statNo="10" processNo="12" />
    </header>
    <event>
        <requestToDo identifier="000" typeNo="0285011400" />
    </event>
    <body>
        <arrays>
            <array name="components" dataType="8">
                <item value="SCADA_Storage" />
            </array>
        </arrays>
    </body>
</root>


<?xml version="1.0" encoding="UTF-8"?>
<root>
    <header version="2.0" eventId="558" eventName="ResponseToDo" timeStamp="2016-04-03T04:08:03+02:00">
        <location lineNo="2" statNo="10" application="APS" />
    </header>
    <event>
        <result returnCode="0" />
        <trace />
    </event>
    <body>
        <items>
            <item name="LocationState" value="0" dataType="2" />
            <item name="MaterialState" value="0" dataType="2" />
        </items>
        <structs>
            <workPart identifier="KK1YA5285600000"  batch="" passThrough="false" nProcessNo="12" />
        </structs>
        <structArrays>
            <array name="workFlowItems">
                <structDef>
                    <item name="pos" dataType="3" />
                    <item name="id" dataType="8" />
                    <item name="IsForMachine" dataType="11" />
                </structDef>
                <values>
                    <item pos="1" id="K5285611000" IsForMachine="true" />
                    <item pos="2" id="KM285612000" IsForMachine="true" />
                    <item pos="3" id="KM285612003" IsForMachine="true" />
                    <item pos="4" id="KM285612004" IsForMachine="true" />
                    <item pos="5" id="KM285612005" IsForMachine="true" />
                    <item pos="6" id="KM285612006" IsForMachine="true" />
                </values>
            </array>
            <array name="components">
                <structDef>
                    <item name="matID" dataType="8" />
                    <item name="state" dataType="2" />
                </structDef>
                <values>
                    <item matID="051822SLLL08314" state="0" />
                </values>
            </array>
        </structArrays>
    </body>
</root>
Coordinator
Apr 21, 2016 at 5:39 PM
Hallo Ulrich,

also die Datei TraceSourceExtension.cs ist mit dem letzten Checkin auch hochgeladen worden. Sorry, ich hatte die Datei schon im Projekt aber noch nicht mit hochgeladen. Musste da erst noch das Release 4.6.1.3 fertig machen und habe es dann alles komplett eingecheckt.

Die angesprochene Datei ist eine kompilierte Version der DockPanel Suite (Homepage / GIT)

Bezüglich der Logik bei der Server-Antwort: Ich würde das logisch trennen:
  • Ich würde auf der einen Seite die Kommunikation halten. Da kann man prinzipiell auch einen Timeout einbauen, so dass eine Verbindung bei Inaktivität geschlossen wird oder eben alternativ die Kommunikation geprüft wird. (beim IRC sendet der Server ein "PING" und erwartet vom Client darauf ein "PONG". Sowas in der Art, Kommt eben nichts, dann wird davon ausgegangen, dass die Verbindung tot ist und daher wird die dann geschlossen.
  • Auf der anderen Seite wäre dann die Spiel-Logik. Die würde ich komplett unabhängig vom Netzwerk-Code halten. Da könnte z.B. ein Timer sein, der alle 100 ms auslöst und dann für alle Clients:
  • schaut, ob eine Antwort da ist. Diese wird ggf. ausgewertet. Ist die Frage abgearbeitet, wird der Timeout Zähler auf 0 gesetzt und damit der Timeout abgebrochen.
  • den Timeout-Zähler reduziert, so dieser > 0 ist. Ist ein Zähler dann auf 0, wird der Fragen Zähler hochgezählt. Ist die Anzahl der Fragen Zähler noch nicht auf Maximum, wird die Frage erneut gestellt. Ansonsten wird der Client auf Störung gesetzt.
    Beim Stellen einer Frage muss dann halt immer der Timeout Zähler gesetzt werden (auf 15, wenn es 1500ms sein sollen) und der Fragen-Counter muss auf 0 oder 1 gesetzt werden.
Damit hast Du unterschiedliche Dinge, die Du unabhängig voneinander implementieren und testen kannst. (Viele kleine Bausteine mit möglichst guten Tests sind besser als komplexe Teile, die kaum testbar sein!)

Bezüglich der FormsApplication: Ja, da geht es um ganz normale Forms. Wenn Du eine Windows Forms Applikation erstellst, dann hat diese ja mindestens eine Form (Also eine Klasse, die von Form abgeleitet ist). In meiner Beispiel-Applikation ist das ja die ExampleForm - gut erkennbar am Code, denn die Klasse ist ja wie folgt definiert: "public partial class ExampleForm : Form"

Viele Grüße,

Konrad
Apr 22, 2016 at 3:38 PM
Hallo Konrad,

schön, die Datei TraceSourceExtension.cs ist drin.
Wo ich mir halt schwer tue, jetzt an einem konkreten Beispiel, Deine Klasse einzubringen.

Für meine Applikation reicht 1 Server, 1 Client aus.

Was wäre konkret zu tun, um nun die Frage zu senden, die Antwort zu erhalten.

Das wäre supernett und echt hilfreich.

Viele Grüße Ulrich
Coordinator
Apr 23, 2016 at 10:00 AM
Also ich habe jetzt einmal ein Beispiel mit in die Neitzel.Forms.Example eingebaut. Ein Server-Fenster kann geöffnet werden und es können beliebig viele Client Fenster geöffnet werden. Nachrichten an den Server werden einfach an alle verbundene Clients gesendet, die die Nachricht dann anzeigen.
Apr 24, 2016 at 2:22 PM
Hallo Konrad,

Danke, ich werde es mir anschauen.

Grüße Ulrich