High Performance OPC UA Server SDK  1.1.1.177
Network Module

Basic Principles

Backend Architecture

The architecture of the SDK allows the use of different implementations of the network interface adaption, called backends. Only one implementation may be active at a time. The default backend uses the Berkeley Socket application programming interface. Alternative backends may be based on epoll, lwIP, or Microsoft Windows IOCP, for example. The provided Berkeley backend includes a small internal platform adaption layer, which allows it to be used on Linux and Windows. This is necessary, because the Windows API has minor differences to the Linux API version, like different types for the socket type or error functions.

The SDK defines the interface of the network API in the file “[SDK_ROOT]/src/network/uanetwork.h”. New backends have to implement this interface according to the documentation and by using the provided network unit tests “test_net_addr”, “test_net_connections”, and “test_net_special_scenarios”. Each backend also has to provide a file “[SDK_ROOT]/src/network/[BACKEND]/uanetwork_types.h” containing the type definitions. Please see the file provided by the default backend (see folder berkeley) for the necessary type definitions. Types included in this file may be extended with backend specific members. In case of the default implementation, these members in the structure ua_socket_op are prefixed with “x_” to prevent accidential use in backend independent code. These members may be removed, reused, or replaced if the backend is altered. The other parameters may be accessed by the calling code. This does not apply to the structures ua_socket and ua_net_base. Of course, the helper macro ua_socket_op_init must be updated according to changes to the operation structure.

Backend alternatives may be included in the delivery, but only the Berkeley backend is usable in this version of the SDK.

Asynchronous IO

The network API uses the concept of asynchronous IO for reading and writing data from and to the network, as well as accepting new network connections. This means these operations are not only non-blocking, but also take all necessary parameters like buffers and sockets at the beginning of the operation and notify the caller when the operation completed.

Example: Instead of registering a socket for readability and reading the available or required number of bytes when the Berkeley API marks the socket as readable, the SDKs network API takes the buffer and amount of bytes to read and notifies the caller when either the requested amount of bytes has been received or an error occured.

Each asynchronous operation is represented by the structure ua_socket_op, which contains the IO buffer of type ua_buffer, the notification information, and a set of Operation Control Flags. Exclusively for UDP, the operation structure also includes a network address member used by ua_socket_recvfrom().

Memory Concept

All network module functions take the memory for parameters and elements from the caller and do no internal allocations. Therefore, all types are published in “[SDK_ROOT]/src/network/[BACKEND]/uanetwork_types.h”. Users of this API can inline these types into their own network types and minimize the number of allocations. Also, the network module needs less resource configuration options.

Network Base

The network module defines a type called ua_net_base, which manages all sockets in use and their events. Each socket is created in the context of a network base instance. All socket events are handled from within the call context of ua_net_do_events() and the referred network base instance. The Berkeley backend uses this object to hold a list of platform socket handles and use them for creating the file descriptor lists for the select() call. Other backend types may use this object to place some type of event queue in it. When a sockets is closed, it will be removed from the network base and, hence, from the event processing. ua_net_do_events() is called from within the main loop by the SDK and may block up to the supplied amount of milliseconds to suspend the task. Returning earlier wastes CPU time. Returning later delays the event handling of the Timer Module.

Protocols

The basic protocol to be supported is TCP/IPv4. TCP/IPv6 is optional and can be requested by the CMake configuration through the option “UA_NET_SUPPORT_IPV6”. UDP is supported and currently mandatory.

IP Filter

The network module includes an optional IP filter mechanism, which limits the number of connections from the same IP address to a configurable amount. This mechanism is meant to defend servers with very limited resources against clients trying to open lots of TCP connections (non-DDOS effort or clients with bad resource management). This filter can be activated through the CMake option “UA_NET_SUPPORT_IP_FILTER” and the limit set in “g_appconfig.net.max_connections_per_ip”. The implementation of the IP filter is backend and platform independent and, hence, can be used by all backends.

Addressing

The network module defines its own network address type ua_net_addr together with conversion functions from and to the type string.

An optional extension is the API ua_net_resolve(). This function resolves a hostname to a list of ua_net_addr structures. The provided default implementation is implemented synchronously, although the function definition is asynchronous. The reason are the used platform APIs gethostbyname[_r]() or getaddrinfo(), which are not asynchronous. Since name resolving may take a considerable amount of time, an asynchronous implementation is preferred.

Functions

This sections takes a more detailed look on certain functions of the network module, partly from the SDKs point of view. Please refer to the API documention to get detailed descriptions of functions and their parameters.

General

Before using any network module function, ua_net_init() has to be called. This function allows the backend to do necessary initializations, like WSAStartup() on Windows.

Afterwards, a network base object is initialized by a call to ua_net_base_init(), passing the maximum number of supported sockets together with memory large enough to store them and optional IP filters. The generic function ua_net_base_size() can be used to calculate the necessary number of bytes.

These operations can be reverted by calling ua_net_base_clear() and ua_net_clear().

During normal operation, the function ua_net_do_events() must be called repeatedly from the main execution loop of the program This processes the pending operations and triggers operation completion events. A completed operation is removed from the operation queue of a socket and the next pending operation is started, if available.

Many functions use the structure ua_socket_op for asynchronous operation processing and completion. The helper macro ua_socket_op_init is provided to conveniently set the elements of this structure. Since the members of the ua_socket_op structure may vary between different backends, the use of this macro is strongly advised.

Sockets are initialized and registered at a network base by either calling ua_socket_initset() or ua_socket_init(). The first function initializes the socket with the given system socket handle, whereas the second also creates this handle. Although the second one is more convenient to use, the first one might be necessary if handles created by third party libraries must be added to the network event processing.

Sockets are cleaned up and deregistered from their network base by calling ua_socket_close(). In most cases, it is advised to call ua_socket_shutdown() on TCP/IP sockets to initiate a graceful shutdown by closing the sending direction only and wait for the peer to do the same.

TCP

ua_socket_connect() requires a ua_socket_op structure, but only uses the notification information. In addition to that and the necessary socket and server address, an optional local address can be provided. If done so, the socket is bound to this local address before connecting, instead of letting the system use one of the ephemeral ports.

Before calling ua_socket_accept(), a listen socket must be created. This is done be calling ua_socket_bind() with an already initialized socket and putting it into listen mode with ua_socket_listen(). This socket is passed to ua_socket_accept(), together with another initialized socket, which will the connection socket after the operation completes. The callback in the provided ua_socket_op is used for completion notification.

After a connection is established, data can be send and received. The socket operation passed to ua_socket_write() is initialized with the buffer containing the message to be sent and the length field set to the content length. The socket operation for ua_socket_read() contains an empty buffer with the length field set to the number of bytes to be received. In both cases, the operation completes when either an error happens or the set buffer length is reached. Both functions evaluate the operation flag UA_NET_F_ALLOWSYNC. If this flag is set, the function may try to send or receive the data immediately. If successful, the functions return UA_NET_EGOOD instead of UA_NET_ASYNC, and the notification callback is not called.

Callers should always expect asynchronous functions to complete asynchronous, even if UA_NET_F_ALLOWSYNC is set. Therefore, support of this flag by the backend is optional. However, protocols using prefixed length may profit from this functionality, because receiving is possibly done in two steps: header and (variable length) body. If the header is received, it is very likely for the body to also be available. For this reason, the SDK’s UA TCP protocol implementation uses this flag for reading the message body.

UDP

The network module provides two functions for communication over UDP. Both functions take the socket and the operation structure as parameters and behave like their TCP/IP pendants. Please refer to their description at TCP.

Data is written with ua_socket_sendto(). Besides the socket and operation parameter, this function must be supplied with the destination address.

Receiving data is done by calling ua_socket_recvfrom() which, upon successful completion, stores the senders IP address in the the addr field of the ua_socket_op structure.