Prechádzať zdrojové kódy

Set client to error state if disconnected. Add example for reconnect (#1172)

When the server side is closed while the client is connected,
the client should be set to error state, which causes a reconnect on the
next connect call.

Additionally the `UA_sleep_ms` macro is added.

Add unit test for client reconnect
Stefan Profanter 7 rokov pred
rodič
commit
bd97f9a482

+ 2 - 0
examples/CMakeLists.txt

@@ -56,6 +56,8 @@ add_example(server server.c)
 
 add_example(client client.c)
 
+add_example(client_connect_loop client_connect_loop.c)
+
 ####################
 # Feature Examples #
 ####################

+ 84 - 0
examples/client_connect_loop.c

@@ -0,0 +1,84 @@
+/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
+ * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */
+
+/**
+ * Client disconnect handling
+ * --------------------------
+ * This example shows you how to handle a client disconnect, e.g., if the server
+ * is shut down while the client is connected. You just need to call connect
+ * again and the client will automatically reconnect.
+ *
+ * This example is very similar to the tutorial_client_firststeps.c
+ */
+
+
+#include "open62541.h"
+#include <signal.h>
+
+#ifdef _WIN32
+# include <windows.h>
+# define UA_sleep_ms(X) Sleep(X)
+#else
+# include <unistd.h>
+# define UA_sleep_ms(X) usleep(X * 1000)
+#endif
+
+UA_Boolean running = true;
+UA_Logger logger = UA_Log_Stdout;
+
+static void stopHandler(int sign) {
+    UA_LOG_INFO(logger, UA_LOGCATEGORY_CLIENT, "Received Ctrl-C");
+    running = 0;
+}
+
+int main(void) {
+    signal(SIGINT, stopHandler); /* catches ctrl-c */
+
+    UA_ClientConfig config = UA_ClientConfig_default;
+    /* default timeout is 5 seconds. Set it to 1 second here for demo */
+    config.timeout = 1000;
+    UA_Client *client = UA_Client_new(config);
+
+    /* Read the value attribute of the node. UA_Client_readValueAttribute is a
+     * wrapper for the raw read service available as UA_Client_Service_read. */
+    UA_Variant value; /* Variants can hold scalar values and arrays of any type */
+    UA_Variant_init(&value);
+
+    /* Endless loop reading the server time */
+    while (running) {
+        /* if already connected, this will return GOOD and do nothing */
+        /* if the connection is closed/errored, the connection will be reset and then reconnected */
+        /* Alternatively you can also use UA_Client_getState to get the current state */
+        UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
+        if (retval != UA_STATUSCODE_GOOD) {
+            UA_LOG_ERROR(logger, UA_LOGCATEGORY_CLIENT, "Not connected. Retrying to connect in 1 second");
+            /* The connect may timeout after 1 second (see above) or it may fail immediately on network errors */
+            /* E.g. name resolution errors or unreachable network. Thus there should be a small sleep here */
+            UA_sleep_ms(1000);
+            continue;
+        }
+
+        /* NodeId of the variable holding the current time */
+        const UA_NodeId nodeId =
+                UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
+
+        retval = UA_Client_readValueAttribute(client, nodeId, &value);
+        if (retval == UA_STATUSCODE_BADCONNECTIONCLOSED) {
+            UA_LOG_ERROR(logger, UA_LOGCATEGORY_CLIENT, "Connection was closed. Reconnecting ...");
+            continue;
+        }
+        if (retval == UA_STATUSCODE_GOOD &&
+            UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) {
+            UA_DateTime raw_date = *(UA_DateTime *) value.data;
+            UA_String string_date = UA_DateTime_toString(raw_date);
+            UA_LOG_INFO(logger, UA_LOGCATEGORY_CLIENT, "string date is: %.*s", (int) string_date.length, string_date.data);
+            UA_String_deleteMembers(&string_date);
+        }
+        UA_sleep_ms(1000);
+    };
+
+    /* Clean up */
+    UA_Variant_deleteMembers(&value);
+    UA_Client_delete(client); /* Disconnects the client internally */
+    return UA_STATUSCODE_GOOD;
+}

+ 3 - 3
plugins/ua_network_tcp.c

@@ -27,12 +27,14 @@
 # define WIN32_INT (int)
 # define OPTVAL_TYPE char
 # define ERR_CONNECTION_PROGRESS WSAEWOULDBLOCK
+# define UA_sleep_ms(X) Sleep(X)
 #else
 # define CLOSESOCKET(S) close(S)
 # define SOCKET int
 # define WIN32_INT
 # define OPTVAL_TYPE int
 # define ERR_CONNECTION_PROGRESS EINPROGRESS
+# define UA_sleep_ms(X) usleep(X * 1000)
 # include <arpa/inet.h>
 # include <netinet/in.h>
 # include <sys/select.h>
@@ -77,13 +79,11 @@
 # define INTERRUPTED WSAEINTR
 # define WOULDBLOCK WSAEWOULDBLOCK
 # define AGAIN WSAEWOULDBLOCK
-# define sleepMs(X) Sleep(X)
 #else
 # define errno__ errno
 # define INTERRUPTED EINTR
 # define WOULDBLOCK EWOULDBLOCK
 # define AGAIN EAGAIN
-# define sleepMs(X) usleep(X * 1000)
 #endif
 
 /****************************/
@@ -753,7 +753,7 @@ UA_ClientConnectionTCP(UA_ConnectionConfig conf,
                     }
                     /* wait until we try a again. Do not make this too small, otherwise the
                      * timeout is somehow wrong */
-                    sleepMs(100);
+                    UA_sleep_ms(100);
                 } else {
                     connected = true;
                     break;

+ 6 - 1
src/client/ua_client.c

@@ -850,8 +850,13 @@ receiveServiceResponse(UA_Client *client, void *response,
         UA_StatusCode retval =
             UA_Connection_receiveChunksBlocking(&client->connection, &reply,
                                                 &realloced, timeout);
-        if(retval != UA_STATUSCODE_GOOD)
+        if(retval != UA_STATUSCODE_GOOD) {
+            if (retval == UA_STATUSCODE_BADCONNECTIONCLOSED) {
+                // set client to error state so that call to connect will force a new connection attempt
+                client->state = UA_CLIENTSTATE_ERRORED;
+            }
             return retval;
+        }
 
         /* ProcessChunks and call processServiceResponse for complete messages */
         UA_SecureChannel_processChunks(&client->channel, &reply,

+ 39 - 0
tests/check_client.c

@@ -98,6 +98,42 @@ START_TEST(Client_read) {
 }
 END_TEST
 
+
+START_TEST(Client_reconnect) {
+        UA_ClientConfig clientConfig = UA_ClientConfig_default;
+        clientConfig.timeout = 100;
+        UA_Client *client = UA_Client_new(clientConfig);
+        setup();
+        UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
+        ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
+
+        UA_Variant val;
+        UA_NodeId nodeId = UA_NODEID_STRING(1, "my.variable");
+        retval = UA_Client_readValueAttribute(client, nodeId, &val);
+        ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
+        UA_Variant_deleteMembers(&val);
+
+        // restart server to test reconnect
+        teardown();
+        setup();
+
+        retval = UA_Client_readValueAttribute(client, nodeId, &val);
+        ck_assert_uint_eq(retval, UA_STATUSCODE_BADCONNECTIONCLOSED);
+
+        retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
+        ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
+
+        retval = UA_Client_readValueAttribute(client, nodeId, &val);
+        ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
+        UA_Variant_deleteMembers(&val);
+
+        UA_Client_disconnect(client);
+        UA_Client_delete(client);
+
+        teardown();
+    }
+END_TEST
+
 static Suite* testSuite_Client(void) {
     Suite *s = suite_create("Client");
     TCase *tc_client = tcase_create("Client Basic");
@@ -105,6 +141,9 @@ static Suite* testSuite_Client(void) {
     tcase_add_test(tc_client, Client_connect);
     tcase_add_test(tc_client, Client_read);
     suite_add_tcase(s,tc_client);
+    TCase *tc_client_reconnect = tcase_create("Client Reconnect");
+    tcase_add_test(tc_client_reconnect, Client_reconnect);
+    suite_add_tcase(s,tc_client_reconnect);
     return s;
 }