Version | Anpassungen |
---|---|
1.0 (2020-11-23) | Initiale Version |
-- 2020-12-02 | Extension UrsAI2SharedTcpClient hinzugefügt |
1.1 (2020-12-02) | Bei einem Sendefehler wurde der Zwischenzustand 'Disconnecting' entfernt. |
1.2 (2021-01-09) | Write und Writeln haben nicht abgebrochen, wenn keine Verbindung besteht. |
1.3 (2021-04-03) | Vorbelegung von RemotePort mit 0. Vermeidet Fehlermeldungen im App Inventor. |
1.4 (2021-04-16) | TestConnection, CrLfDelay, IgnoreTestChar hinzugefügt. |
1.5 (2022-10-02) | Eigenschaften LocalIpAddress und Version hinzugefügt |
Inhaltsverzeichnis
Verbindungsauf- und -abbau, Verbindungszustand
1. Versand der Zeilenendekennung mit Verzögerung
2. Versand von Test-Nachrichten
Shared Client (UrsAI2SharedTcpClient)
Das ZIP-Archiv UrsAI2TcpClient zum Download. Das Archiv enthält den Quellcode, das kompilierte Binary zum Upload in den App Inventor und eine Beispiel-Anwendung.
Die Extension UrsAI2TcpClient ermöglicht es, TCP-Verbindungen zu einem Server zu errichten und zu betreiben. Es können Textnachrichten per TCP versendet werden. Der Versand erfolgt zeilenweise. Ein Zeile wird durch eine Zeilenende-Kennung (CR, LF, CRLF) abgeschlossen. Wird beim Empfang eine solche Kennung erkannt, werden die bis dahin empfangenen Zeichen per Ereignis an die Applikation gemeldet.
Die Methode Connect stellt eine Verbindung mit dem Server her, der über die Eigenschaften RemoteAddress und RemotePort festgelegt ist. ConnectTo verwendet diese Eigenschaften nicht enthält, sondern verwendet die als Parameter übergebenen Endpunktdaten.
In beiden Methoden wird zunächst geprüft, dass der Verbindungszustand Disconnected oder Aborted ist. Ist dies der Fall, geht die Extension in den Zustand Connecting über und das Ereignis ConnectionStateChanged wird ausgelöst. Intern wird nun versucht die TCP-Verbindung herzustellen und den Thread zu starten, der auf eingehende Daten lauscht. War der Verbindungsaufbau erfolgreich, geht die Extension in den Zustand Connected über andernfalls in den Zustand Aborted. Zum Schluss wird wieder das Ereignis ConnectionStateChanged ausgelöst.
Lässt der Zustand der Extension nicht zu, eine neue Verbindung aufzubauen, wird das Ereignis ErrorOccurred mit der Fehlernummer 4 ("Already connected.") ausgelöst.
Die Trennung einer Verbindung wird über die Methode Disconnect gestartet. Der Zustand der Extension muss Connected sein, ansonsten wird das Ereignis ErrorOccurred mit der Fehlernummer 3 ("Not connected to a server.") ausgelöst. Besteht eine Verbindung geht die Extension zunächst in den Zustand Disconnecting über. Danach wird die bestehende TCP-Verbindung abgebaut und die Extension geht in den Zustand Disconnected über. Zum Schluss wird das Ereignis ConnectionStateChanged ausgelöst.
Nach dem Abbruch einer Verbindung wird von der Anwendung i.d.R. versucht werden, eine neue Verbindung aufzubauen. Wenn dies sofort versucht wird und wiederholt scheitert ergibt sich eine hohe Netzwerkbelastung. Man wird den Versuch, die Verbindung wieder herzustellen, deshalb erst nach einer gewissen Zeit starten. Das Ereignis DelayedConnectionAborted kann hierfür genutzt werden. Es wird erst nach einer einstellbaren Zeit (s. ConnectionAbortedDelay) nach einem Verbindungsabbruch ausgelöst.
Code | Zustand | Bedeutung |
---|---|---|
0 | Disconnected | Der Client ist nicht mit einem Broker verbunden. |
1 | Connecting | Der Client versucht eine Verbindung zum Server herzustellen. |
2 | Connected | Es besteht eine Verbindung zu einem Server. |
3 | Disconnecting | Der Client baut die Verbindung zum Server ab. |
4 | Aborted | Die Verbindung konnte auf Grund eines Fehlers nicht hergestellt werden oder wurde unterbrochen. Fehlerursache kann über die Eigenschaft LastErrorCode und LastErrorMessage abgerufen werden. |
Zum Versenden stehen die Methoden Write und Writeln zur Verfügung. Die Zeichen-Codierung, d.h. die Umwandlung der Zeichenkette in eine Bytefolge, wird über die Eigenschaft Charset festgelegt.
Write schreibt den angegebenen Text in den Ausgabepuffer. Das sofortige Versenden der Nachricht ist nicht garantiert.
Writeln schreibt den angegebenen Text in den Ausgabepuffer und fügt eine Zeilenendekennung an. Diese ist abhängig von der Eigenschaft LineDelimiterCrLf. Bei true wird CRLF (0x0D+0x0A, für Server auf Windows-Basis) und bei false nur LF (0x0A) angehängt. Anschließend wird intern die Methode OutputStream.flush() aufgerufen, die einen Versand der bis dahin noch nicht gesendeten Daten erzwingt.
Bei beiden Methoden wird zunächst überprüft, ob der Zustand Connected ist. Ist dies nicht der Fall, wird die Anweisung ignoriert und das Ereignis ErrorOccured mit dem ErrorCode 3 ("Not connected to a server.") ausgelöst.
Tritt beim Versenden ein Fehler auf, z.B. weil die Verbindung abgebrochen ist, wird zunächst das Ereignis ErrorOccured mit dem ErrorCode 5 ("Connection fault.") ausgelöst. Danach wird die Verbindung mit der Ereignisfolge ConnectionStateChanged:Disconnecting und ConnectionStateChanged:Aborted abgebaut.
Das einlesen der Daten erfolgt in einer (Endlos-) Schleife in einem separatem Thread. Die eingehenden Bytes werden anhand der Eigenschaft Charset in Texte decodiert und gesammelt, bis eine Zeilenendekennung (CR, 0x0D, LF, 0x0A oder CRLF, 0x0D+0x0A).) empfangen wird. Die Eigenschaft LineDelimiterCrLf wird hier nicht ausgewertet. Wenn eine Zeilenendekennung empfangen wurde, werden die bis dahin gesammelten Zeichen ohne die Zeilenendekennung über das Ereignis MessageReceived an die App gemeldet.
Tritt in dem Thread ein Fehler auf, wird die Verbindung mit dem Ereignis ConnectionStateChanged:Aborted abgebaut.
In der Android-Umgebung müssen sämtliche Netzwerk-Funktionen in einem separatem Thread ausgeführt werden. Dies ist notwendig, damit die Benutzeroberfläche bei lange andauernden Operation, z.B. wegen schlechter Verbindungen, nicht einfriert. Das hat Auswirkungen auf das Auslösen von Ereignissen. Sie werden nicht direkt, sondern über ein Handler-Instanz ausgelöst. Der Umweg über Handler.post(...) verlagert die Ausführung aus dem separatem Thread in den GUI-Thread.
Das hat hat zur Folge, das der aktuelle AI2-Block zu Ende ausgeführt wird, bevor die Ereignisse ausgelöst werden. In der folgenden Konstellation
wird also zuerst die Prozedur DoSomething aufgerufen, bevor eines der beiden Ereignisse ausgelöst wird, das den Erfolg des Aufrufs von Connect zurück meldet. Die Prozeduren HandleErrorOccured bzw. HandleConnectionStateChanged werden also nach doSomething ausgeführt.
Das Analoge gilt für alle Ereignisse dieser Extension.
Der Versand der Daten erfolgt als Byte-Folge. Dazu muss der Text entsprechend umcodiert werden. Je nach verwendetem Zeichensatz wird eine andere Codierung verwendet. Wichtig ist, dass Sender und Empfänger die gleiche Codierung (den gleichen Zeichensatz) verwenden. Der verwendete Zeichensatz wird über die Eigenschaft Charset festgelegt. Es ist die Bezeichnung des Zeichensatzes anzugeben. Zur Vermeidung von Schreibfehlern die gängigen Bezeichnungen über die Eigenschaften Charset_... abgerufen werden.
Eigentlich sollte bei einer TCP-Verbindung garantiert sein, dass Daten entweder korrekt versendet werden oder ein Fehler gemeldet wird. Dies ist bei der Java-Implementierung -zumindest der auf meinem Smartphone (Java 7)- leider nicht der Fall. Verbindungsabbrüche werden erst dann erkannt, wenn zwei Schreiboperationen mit genügend großen zeitlichen Abstand ausgeführt werden (siehe java-detect-lost-connection). Dies gilt auch für den Fall, dass die Gegenstelle die Verbindung ordnungsgemäß schließt (disconnect, close, ...). Der zeitliche Abstand ist notwendig, weil der sich der Nagle-Algorithmus nicht abschalten lässt (s. Java Socket Option TCP_NODELAY). Daran ändert auch der Aufruf von OutputStream.flush() nichts, der eigentlich den sofortigen Versand auslösen soll.
Diese Extension bietet zwei Möglichkeiten, Verbindungsabbrüche mit hoher Wahrscheinlichkeit zu erkennen.
Die Methode Write sendet i.d.R. die übergebenen Daten nicht sofort, sondern puffert die Daten. Erst bei Writeln wird die Übertragung erzwungen. Hat die Eigenschaft CrLfDelay den Wert 0, werden keine weiteren Maßnahmen getroffen. Die eingestellte Zeilenendekennung wird angehängt und die Nachricht versendet. Ist der Wert größer als 0, wird zunächst nur die Nachricht versendet. Die Zeilenendekennung wird erst mit einer Verzögerung versendet, die durch CrLfDelay bestimmt ist. Dadurch löst bei unterbrochener Verbindung der Versand der Zeilenendekennung einen Fehler und damit das Ereignis ClientDisconnected aus. Bei meinem Smartphone war eine Zeitverzögerung von 200 ms ausreichend.
Dieser Mechanismus reduziert natürlich den Datendurchsatz und erhöht die Latenzzeiten. Die Entscheidung muss zwischen Sicherheit und Performance getroffen werden.
Die Funktion TestConnection sendet mit zeitlicher Verzögerung (s.o.) zwei Zeichen, die über die Eigenschaft IgnoreTestChar spezifiziert wurden. Das Testzeichen muss vom Empfänger ignoriert werden. Üblicherweise nimmt man als Testzeichen ein unsichtbares Zeichen. Das Zeichen wird als numerischer Zeichencode angegeben. Die Voreinstellung ist 6 (ACK, Acknowledge).
Der Versand des zweiten Zeichens löst bei unterbrochener Verbindung einen Fehler und damit das Ereignis ClientDisconnected aus.
Code | Bedeutung | Text |
---|---|---|
0 | Kein Fehler | |
1 | Die angegebene Port-Nummer ist nicht gültig. | Port number is invalid. |
2 | Verbindungsaufbau nicht möglich. | Could not connect to server. |
3 | Nicht mit einem Server verbunden. | Not connected to a server. |
4 | Es besteht bereits eine Verbindung. | Already connected. |
5 | Verbindungsfehler | Connection fault. |
Version | Anpassungen |
---|---|
1.0 (2020-12-02) | Initiale Version |
1.1 (2021-01-09) | Write und Writeln haben nicht abgebrochen, wenn keine Verbindung besteht. |
1.2 (2021-04-16) | TestConnection, CrLfDelay, IgnoreTestChar hinzugefügt. |
Hinweis: Die Benutzung von UrsAI2SharedTcpClient ist im Companion nicht möglich. Companion stellt nicht alle Funktionalitäten zur Verfügung, die zum Betrieb dieser Extension notwendig sind.
Diese Variante der Extension (UrsAI2SharedTcpClient, im Download enthalten) benutzt eine statische (Java static) Verbindungskomponente, d.h. alle Instanzen der Extension in verschiedenen Screens einer App nutzen die selben Objekte. D.h. auch, es kann in der App nur eine einzige Verbindung geben. Die Verwendung von mehreren Instanzen auf dem selben Screen führt zu Fehlern beim Auslösen der Ereignisse.
Der Vorteil dieser Extension ist, dass sich nicht jeder Screen um den Verbindungsaufbau kümmern muss. Man kann z.B. in Screen1 eine Verbindung aufbauen und diese in weiteren Screens nutzen. Bei Verwendung der oben beschrieben Extension UrsAI2TcpClient ist das nicht möglich. Wenn ein weiterer Screen mit dem gleichen Endpunkt (gleiche IP und gleicher Port) kommunizieren will, müsste, bevor der zweite Screen geöffnet wird, die Verbindung im ersten Screen getrennt werden. Im zweiten Screen muss sie dann erneut geöffnet werden. Vor dem Schließen des zweiten Screen muss die Verbindung wieder getrennt und bei der Rückkehr in den ersten Screen wieder geöffnet werden. Dies alles ist bei der Verwendung des UrsAI2SharedTcpClient nicht notwendig. Die Verbindung wird dem zweiten Screen in dem Zustand zur Verfügung gestellt, wie sie im ersten Screen verlassen wurde und umgekehrt.
Wenn der erste Screen eine Methode zur Wiederherstellung einer abgebrochenen Verbindung implementiert (siehe das im Download enthaltenen Beispiel), ist keine Wiederholung dieser Funktionalität im zweiten Screen notwendig. Die Funktion im ersten Screen ist auch nach dem Öffnen eine zweiten Screens aktiv (leider jedoch nicht umgekehrt).
Es stehen keine Designer-Eigenschaften zur Verfügung. Designer-Eigenschaften sind nicht sinnvoll. Eine zweite Instanz der Extension (in einem zweiten Screen) würde die eingestellten Werte der ersten Instanz überschreiben. Man müsste also sicher stellen, dass in allen Screens zu jeder zeit die gleichen Werte im Designer eingetragen wären. Da i.d.R. nur der die IP-Adresse (oder der Hostname) des Servers sowie der zu verwendende Port eingestellt werden müssen und das auch nur im ersten Screen, ist der Zusatzaufwand gering.
Die Ereignisse ConnectionStateChanged und DelayedConnectionAborted werden an alle Instanzen der Extension auf allen Screens weiter geleitet. MessageReceived und ErrorOccured werden nur von der Instanz auf dem aktuell sichtbaren Screen ausgelöst.
Mit dieser kleinen App kann man sich mit einem Server verbinden und Textnachrichten versenden und empfangen. |
Hinweis: Im Designer müssen noch passende Endpunktdaten eingetragen werden.
Mit dieser kleinen App kann man sich mit einem Server verbinden und Textnachrichten versenden und empfangen. Sie demonstriert außerdem die Verwendung der Extension auf mehreren Screens. |
Dies ist ein Screenshot vom zweiten Screen. Man sieht, der Screen wurde mit dem Verbindungszustand Connected eröffnet (erste Zeile des Logs). Beim Schreiben war die Verbindung jedoch gestört (Server war ausgeschaltet). Die Extension meldet dies über das Ereignis ErrorOccured (Zeilen 2 und 3 des Logs). Daraufhin wird die Verbindung abgebaut (aufgeräumt). Dies wird über das Ereignis ConnectionStateChanged gemeldet (Zeilen 4 und 5 des Logs).
Der erste Screen implementiert zum Ereignis DelayedConnectionAborted eine Funktion zur Wiederherstellung der Verbindung. Nach kurzer Zeit erscheint deshalb ohne weiteres Zutun die Meldung "State: Connecting" (Zeile 6). Der Server war wieder eingeschaltet, weshalb die Verbindung wieder hergestellt werden konnte (Zeile 7).
Zum Ausprobieren des Beispiels muss in Screen1 beim Ereignis Initialize noch die IP-Adresse bzw. der Hostname des Servers und der zu verwendende Port eingetragen werden.
Für die Erstellung eigener Extensions habe ich einige Tipps zusammengestellt: AI2 FAQ: Extensions entwickeln.
Die Diagramme wurden mit PlantUML erstellt.