Version | Adjustment |
---|---|
1.0 (2020-11-23) | Initiale Version |
-- 2020-12-02 | Extension UrsAI2SharedTcpClient added |
1.1 (2020-12-02) | In the event of a send error, the intermediate state 'Disconnecting' was removed. |
1.2 (2021-01-09) | Write and Writeln did not abort if there was no connection. |
1.3 (2021-04-03) | Default of RemotePort is 0. Avoids error messages in App Inventor. |
1.4 (2021-04-16) | TestConnection, CrLfDelay, IgnoreTestChar added. |
1.5 (2022-10-02) | Properties LocalIpAddress and Version added. |
Table of Contents
Connection establishment and termination, connection state
1. Sending the line end identifier with a delay
Character set
Shared Client (UrsAI2SharedTcpClient)
The UrsAI2TcpClient ZIP archive for download. The archive contains the source code, the compiled binary for uploading to the App Inventor and a sample application.
The extension UrsAI2TcpClient allows to set up and operate TCP connections to a server. Text messages can be sent via TCP. The transmit takes place line by line. A line is terminated by an end-of-line identifier (CR, LF, CRLF). If such an identifier is recognized upon receipt, the characters received up to that point are reported to the application via an event.
The Connect method establishes a connection to the server that is specified by the RemoteAddress and RemotePort properties. ConnectTo does not use these properties, but uses the endpoint data passed as parameters to this function.
Both methods first check that the connection state is Disconnected or Aborted. If this is the case, the extension changes to the Connecting state and the ConnectionStateChanged event is triggered. Now an internally attempt is made to establish the TCP connection and to start the thread that listens for incoming data. If the connection was established successfully, the extension changes to the Connected state, otherwise to the Aborted state. Finally, the ConnectionStateChanged event is triggered again.
If the state of the extension does not allow a new connection to be established, the ErrorOccurred event is triggered with error number 4 ("Already connected.").
The disconnection of a connection is started using the Disconnect method. The state of the extension must be Connected, otherwise the event ErrorOccurred is triggered with error number 3 ("Not connected to a server."). If there is an active connection, the extension first changes to the Disconnecting state. The existing TCP connection is then terminated and the extension changes to the Disconnected state. Finally, the ConnectionStateChanged event is triggered.
After a connection was broken down, the application will usually try to establish a new connection. If this is tried immediately and repeatedly fails, the result is a high network load. The attempt to re-establish the connection should therefore only be started after a delay. The DelayedConnectionAborted event can be used for this. It is only triggered after an adjustable time (see ConnectionAbortedDelay) after a connection breakdown.
Code | Zustand | Bedeutung |
---|---|---|
0 | Disconnected | The client is not connected to a broker. |
1 | Connecting | The client is currently trying to connect to the server. |
2 | Connected | The cleint is connected to a server. |
3 | Disconnecting | The cleint is currently clearing the connection. |
4 | Aborted | The connection could not be established due to an error or the connection was interrupted. The cause of the error can be looked up via the LastErrorCode and LastErrorMessage properties. |
The Write and Writeln methods are used for sending messages. The character encoding, ie the conversion of the character string into a byte sequence, is specified by the Charset property.
Write writes the specified text into the output buffer. It is not guaranteed that the message is transmitted immediately.
Writeln writes the specified text into the output buffer and adds a line end identifier. The type of the end-of-line identifier depends on the LineDelimiterCrLf property. If true, CRLF (0x0D + 0x0A, for Windows-based servers) is appended and if false, only LF (0x0A) is appended. Then the OutputStream.flush() method is called internally, which forces the pending data to be sent.
Both methods first check whether the state is Connected. If this is not the case, the instruction is ignored and the ErrorOccured event is triggered with ErrorCode 3 ("Not connected to a server.").
If an error occurs while sending, e.g. because the connection has been broken, the ErrorOccured event is triggered with ErrorCode 5 ("Connection fault."). The connection is then terminated with the event sequence ConnectionStateChanged:Disconnecting and ConnectionStateChanged:Aborted.
The data is read in in an (endless) loop in a separate thread. The incoming bytes are decoded into texts using the Charset property and collected until a line end identifier (CR, 0x0D, LF, 0x0A or CRLF, 0x0D + 0x0A).) is received. The LineDelimiterCrLf property is not evaluated here. If an end-of-line identifier has been received, the characters collected up to that point are reported to the app without the end-of-line identifier via the MessageReceived event.
Tritt in dem Thread ein Fehler auf, wird die Verbindung mit dem Ereignis ConnectionStateChanged:Aborted abgebaut.
If the receiving thread terminates because of an error, the connection is terminated with the ConnectionStateChanged:Aborted event.
In the Android environment, all network functions must be carried out in a separate thread. This is necessary so that the user interface does not freeze during long-term operations, e.g. because of bad connections. This has an impact on the triggering of events. They are not triggered directly, but via a Handler instance. The detour via Handler.post(...) shifts the execution from the separate thread to the GUI thread.
This has the consequence that the current AI2 block is executed to the end before the events are triggered. In the following constellation
the DoSomething procedure is called first before either of the two events is triggered, which report the success of the call to Connect. So the procedures HandleErrorOccured and HandleConnectionStateChanged are executed after DoSomething.
The same applies to all events of this extension.
The data is sent as a byte sequence. To do this, the text must be coded accordingly. The coding depends on the character set used. It is important that the sender and receiver use the same coding (the same character set). The character set used is determined by the Charset property. The name of the character set must be entered. To avoid spelling errors, the common names can be called up using the Charset_... properties.
With a TCP connection, it should actually be guaranteed that data is either sent correctly or an error is reported. Unfortunately, this is not the case with the Java implementation - at least the one on my smartphone (Java 7). Disconnections are only recognized after two write operations with a sufficiently long time interval (see java-detect-lost-connection). This also applies when the remote terminal properly closes the connection (disconnect, close,...). The time interval is necessary because the Nagle's algorithm cannot be switched off (see Java Socket Option TCP_NODELAY). The call to OutputStream.flush() does not change this beavior, thow it should trigger an immediate dispatch.
This extension offers two possibilities to detect disconnections with a high probability.
Mostly the Write method does not send the data immediately, but buffers the data. The transfer is forced with Writeln. If the CrLfDelay property has the value 0, the end-of-line identifier is appended and the message is sent immediately. If the value is greater than 0, initially only the message is sent. The end-of-line identifier is sent after a delay that is determined by CrLfDelay. If the connection is interrupted, the sending of the end-of-line identifier triggers an error and thus the ClientDisconnected event is triggered. With my smartphone, a time delay of 200 ms was sufficient.
This mechanism reduces the data rate and increases the latency times. The decision must be made between security and performance.
The TestConnection method sends two characters specified by the IgnoreTestChar property with a time delay (see above). The test character must be ignored by the recipient. Usually an invisible character is used. The character is specified as a numeric character code. The default setting is 6 (ACK, Acknowledge).
If the connection is interrupted, sending the second character triggers an error and thus the ClientDisconnected event.
Code | Bedeutung | Text |
---|---|---|
0 | No error. | |
1 | The specified port number is not valid. | Port number is invalid. |
2 | Connection establishment not possible. | Could not connect to server. |
3 | Not connected to a server. | Not connected to a server. |
4 | An active connection already exists. | Already connected. |
5 | Connection failure. | Connection fault. |
Version | Adjustment |
---|---|
1.0 (2020-12-02) | Initial version |
1.1 (2021-01-09) | Write and Writeln did not abort if there was no connection. |
1.2 (2021-04-16) | TestConnection, CrLfDelay, IgnoreTestChar added. |
Note: The use of UrsAI2SharedTcpClient is not possible in the Companion. Companion does not provide all functions that are necessary to operate this extension.
This variant of the extension (UrsAI2SharedTcpClient, included in the download) uses a static (Java static) connection component, i.e. all instances of the extension in different screens of an app use the same objects. This also means that there can only be one connection in the app. The use of several instances on the same screen leads to errors when triggering the events.
The advantage of this extension is that not every screen has to worry about establishing a connection. For example, you can establish a connection in Screen1 and use it in other screens. This is not possible when using the UrsAI2TcpClient extension described above. If another screen wants to communicate with the same end point (same IP and same port), the connection in the first screen would have to be disconnected before the second screen is opened. It must then be opened again on the second screen. Before closing the second screen, the connection must be disconnected and when you return to the first screen, it must be reopened. This is all unnecessary when using the UrsAI2SharedTcpClient. The connection is made available to the second screen in the state it was left in the first screen and vice versa.
If the first screen implements a method to restore a broken connection (see the example included in the download), this functionality does not need to be repeated in the second screen. The functionality in the first screen is also active after opening a second screen (unfortunately not the other way around).
There are no designer properties available. Designer properties don't make sense. A second instance of the extension (in a second screen) would overwrite the set values of the first instance. So you have to make sure that the same values are entered in the designer in all screens at all times. Since usually only the IP address (or the host name) of the server and the port to be used have to be set and that only in the first screen, the additional effort is minimal.
The ConnectionStateChanged and DelayedConnectionAborted events are forwarded to all instances of the extension on all screens. MessageReceived and ErrorOccured are only triggered by the instance on the currently visible screen.
With this little app you can connect to a server and send and receive text messages. |
Note: Appropriate endpoint data must be entered in the Designer.
With this little app you can connect to a server and send and receive text messages. It also demonstrates the use of the extension on multiple screens. |
This is a screenshot from the second screen. You can see that the screen was opened with the connection status Connected (first line of the log). When writing, however, the connection was disturbed (server was switched off). The extension reports this via the ErrorOccured event (lines 2 and 3 of the log). The connection is then cleared. This is reported via the ConnectionStateChanged event (lines 4 and 5 of the log).
The first screen implements a function to re-establish the connection in the DelayedConnectionAborted event. After a short time, the message "State: Connecting" (line 6) appears without any further action. In the meantime the server was switched on again, so the connection could be re-established (line 7).
To try out the example, the IP address or host name of the server and the port to be used must be entered in Screen1 at the Initialize event.
I have gathered some tips for creating your own extensions: AI2 FAQ: Developing extensions.
The diagrams were created with PlantUML.