Englisch version   English version


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.

In­halts­ver­zeich­nis

Download

Verwendung

Verbindungsauf- und -abbau, Verbindungszustand

Daten versenden

Daten einlesen

Ereignisbehandlung

Zeichensatz

Verbindungsabbruch

1. Versand der Zeilenendekennung mit Verzögerung

2. Versand von Test-Nachrichten

Referenz

Eigenschaften

Remote-Endpoint

Zeitangaben

Verbindungszustand

Zeichensatz

Fehlerbehandlung

Methoden

Ereignisse

 

Shared Client (UrsAI2SharedTcpClient)

 

Beispiele

URS TCP Client Test

URS Shared TCP Client Test

Werkzeuge

Download

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.

Verwendung

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.

Verbindungsauf- und -abbau, Verbindungszustand

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.

Daten versenden

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.

Daten einlesen

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.

Ereignisbehandlung

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.

Zeichensatz

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.

Verbindungsabbruch

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.

1. Versand der Zeilenendekennung mit Verzögerung

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.

2. Versand von Test-Nachrichten

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.

Referenz

Eigenschaften

Remote-Endpoint

RemoteAddress
Hostname oder IP-Adresse des Servers.
RemotePort
Port für die TCP-Verbindung.
Hinweis: Die Vorbelegung ist leer. Wenn kein Wert eingetragen ist meldet AI2: "is not a legal integer".

Zeitangaben

ConnectionTimeout
Zeit in Millisekunden, die für den Verbindungsaufbau zur Verfügung steht. Der Standardwert ist 1000 ms.
IoTimeout
Zeit in Millisekunden, die für Ein- und Ausgabeoperationen zur Verfügung steht. Der Standardwert ist 1000 ms.
ConnectionAbortedDelay
Zeitverzug in Millisekunden zwischen dem Ereignis ConnectionStateChanged:Aborted und DelayedConnectionAborted. Der Standardwert ist 3000 ms.
CrLfDelay
Verzögerung zwischen dem Versand der Daten und der Zeilenendekennung (s. Verbindungsabbruch). Die Voreinstellung ist 200 Millisekunden.

Verbindungszustand

ConnectionState
Aktueller Verbindungszustand.
  0: Disconnected (getrennt)
  1: Connecting (Verbindungsaufbau)
  2: Connected (verbunden)
  3: Disconnecting (Verbindungsabbau)
  4: Aborted (Verbindungsabbruch)
ConnectionStateName
Bezeichnung des aktuellen Verbindungszustands.
IsConnected
Liefert true, wenn eine TCP-Verbindung zum Server besteht (ConnectionState == Connected).
IsDisconnected
Liefert true, wenn keine TCP-Verbindung zum Server besteht (ConnectionState == Disconnected oder ConnectionState == Aborted).
LineDelimiterCrLf
Legt fest, ob die versendete Nachricht durch ein einzelnes "Line Feed"-Zeichen ("\n", 0x0A) oder die Kombination "Carriage Return" + "Line Feed" ("\r\n", 0x0D+0x0A) abgeschlossen wird (s. Funktion Writeln). Bei true wird CRLF (0x0D+0x0A, für Server auf Windows-Basis) und bei false nur LF (0x0A) angehängt.
StateDisconnected
Konstante für den den Zustand Disconnected.
StateConnecting
Konstante für den den Zustand Connecting.
StateConnected
Konstante für den den Zustand Connected.
StateDisconnecting
Konstante für den den Zustand Disconnecting.
StateAborted
Konstante für den den Zustand Aborted.

Zeichensatz

Charset
Aktuell eingestellter Zeichensatz. Möglich sind "US-ASCII", "UTF-8", "ISO-8859-1" (ISO-Latin-1), "UTF-16BE", "UTF-16BL" und "UTF-16".
Diese Angabe wird nur beim Verbindungsaufbau ausgewertet und bleibt während der gesamten Dauer der Verbindung bestehen.
Hinweis: Die Zeichensatz wird über die Java-Methode Charset.forName ermittelt, die stark von den Implementierung im zugrunde liegenden Betriebssystem abhängt. Es kann also sein, dass andere Zeichensätze gültig sind. Es werden alle Zeichensätze akzeptiert, für die Charset.forName gültige Ergebnisse liefert.
Charset_USASCII
Konstante für den den Zeichensatz US-ASCII.
Charset_UTF8
Konstante für den den Zeichensatz UTF-8.
Charset_ISO_Latin_1
Konstante für den den Zeichensatz ISO-8859-1 (ISO-Latin-1).
Charset_UTF16BE
Konstante für den den Zeichensatz UTF-16BE.
Charset_UTF16BL
Konstante für den den Zeichensatz UTF-16BL.
Charset_UTF16
Konstante für den den Zeichensatz UTF-16.
IgnoreTestChar
Zu ignorierendes Zeichen bei Benutzung von TestConnection (s. Verbindungsabbruch). Anzugeben ist der numerische Zeichencode. Die Voreinstellung ist 6 (ACK, Acknowledge).

Fehlerbehandlung

LastAction
Bezeichnung der zuletzt ausgeführten Aktion, z.B. "Connect".
LastErrorCode
Fehler-Code des zuletzt aufgetretenen Fehlers (s.u.).
LastErrorMessage
Text zum Code, z.B. "Invalid State."
LastExecptionCause
Text der Java Exception, die den Fehler ausgelöst hat.
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.

Methoden

Connect()
Versucht eine TCP-Verbindung mit den bei den Eigenschaften RemoteAddress und RemotePort angegebenen Daten herzustellen.
ConnectTo(RemoteAddresse, RemotePort)
Versucht eine TCP-Verbindung mit den bei den Parametern RemoteAddress und RemotePort angegebenen Daten herzustellen.
Disconnect()
Baut eine bestehende Verbindung ab.
TestConnection (ClientID, Delay)
Prüft, ob die Verbindung zum angegeben Client noch besteht. Dazu wird im zeitlich Abstand von Delay (Angabe in Millisekunden) zweimal das unter IgnoreTestChar definierte Zeichen versendet. Besteht die Verbindung nicht mehr, wird beim Versand des zweiten Zeichens das Ereignis ClientDisconnected ausgelöst.
Ist Delay < 0 wird der Wert von CrLfDelay übernommen.
Write(Message)
Schreibt den angegebenen Text in den Ausgabepuffer. Das sofortige Versenden der Nachricht ist nicht garantiert.
Writeln(Message)
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. Das Versenden der Nachricht wird erzwungen.

Ereignisse

ConnectionStateChanged(State, StateName)
Der Verbindungszustand hat sich geändert.
DelayedConnectionAborted()
Wird nach einer einstellbaren Zeit (s. ConnectionAbortedDelay) nach ConnectionStateChanged:Aborted ausgelöst.
MessageReceived(Message)
Eine Nachrichtenzeile wurde empfangen.
ErrorOccured(ActionName, ErrorCode, ErrorMessage)
Es ist ein Fehler aufgetreten.

Shared Client (UrsAI2SharedTcpClient)

Englisch version   English version

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.


Beispiele

URS TCP Client Test

  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.

URS Shared TCP Client Test

  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.

Werkzeuge

Für die Erstellung eigener Extensions habe ich einige Tipps zusammengestellt: AI2 FAQ: Extensions entwickeln.

Die Diagramme wurden mit PlantUML erstellt.