This post is the first in a series of posts illustrating how the Cute server empowers developers to create modern connected systems by enabling them to use the Qt's signals and slots mechanism over a network.
We introduced the Cute server in a previous post.
This post uses the Cute server and SDKs to extend the Qt's Fortune client/server example. In the original example, the client creates a TCP connection to the server that responds by sending a QByteArray containing a randomly chosen fortune text to the client.
With the Cute server, developers map classes containing specially-tagged signals and slots to a server endpoint.
On the server side, we register the Fortunes
class and map it to the fortunes
endpoint. The Fortunes
class emits the remote newFortune
signal with a random fortune every 5 seconds.
We declare the Fortunes
class as follows:
#include <CuteServer.h>
#include <QTimer>
#include <QStringList>
#include <QRandomGenerator>
class Fortunes : public QObject
{
Q_OBJECT
public:
// Constructor takes a single argument
Fortunes(QSharedPointer<Cute::IConnectionInformation> connInfo)
{
Q_UNUSED(connInfo);
m_fortunes << tr("You will learn a magnificent new way to develop network apps.")
<< tr("Affordable. Reliable. Fast. You will have all three at once.")
<< tr("You will never develop network apps like before.")
<< tr("Don't give up. Network development will finally become fun.")
<< tr("You will become a much better network apps developer.");
m_timer.setInterval(5*1000);
QObject::connect(&m_timer, &QTimer::timeout, this, &Fortunes::emitNewFortune);
m_timer.start();
}
~Fortunes() override = default;
signals:
// Remote signals are tagged with the REMOTE_SIGNAL macro
REMOTE_SIGNAL void newFortune(QString fortune);
private slots:
void emitNewFortune()
{
emit newFortune(m_fortunes[QRandomGenerator::global()->bounded(m_fortunes.size())]);
}
private:
QTimer m_timer;
QStringList m_fortunes;
};
The Fortunes
class is registered in a source file as shown below:
#include "Fortunes.h"
REGISTER_REMOTE_OBJECT("/fortunes", Fortunes)
And that's it. Now clients can interact with instances of the Fortunes
class as if they were local.
Clients use the RemoteObject
class to interact with remote objects.
The RemoteObject
class creates the remote object on the server and acts as a proxy to the created object, hiding everything related to network-based interaction from clients. Clients interact with remote signals and slots as if they belonged to the RemoteObject
class.
Below we show the FortunesClient
class that clients use to transparently interact with remote instances of the Fortunes
class to receive fortunes from the server:
#if defined(__APPLE__)
#include <CuteClientSdk/CuteClient.h>
#else
#include <CuteClient.h>
#endif
using namespace Cute::Client;
class FortunesClient : public QObject
{
Q_OBJECT
public:
explicit FortunesClient(const QUrl &remoteObjectUrl)
: m_fortunes("Fortunes", remoteObjectUrl)
{
// Establish remote signal-slot connection
RemoteObject::connect(&m_fortunes,
SIGNAL(newFortune(QString)),
this,
SLOT(onNewFortune(QString)));
}
public slots:
// Remote signal-slot connections require public slots.
void onNewFortune(QString fortune)
{
qWarning("Received new fortune: %s", fortune.toUtf8().constData());
}
private:
RemoteObject m_fortunes;
};
The macro-based signal-slot connections enable clients to interact with remote signals and slots as if they were local. From the client's point of view, the newFortune
signal belongs to the RemoteObject
class.
The RemoteObject
class is the Cute client's SDK main class, and clients use instances of this class to establish signal-slot connections to remote signals/slots defined in remote classes registered on the server-side. Clients interact with remote signals/slots as if they were declared in the RemoteObject
class instead. Clients can also call remote slots directly.
The RemoteObject
class name may make users wrongly think that Cute provides something similar to what the Qt Company offers as Qt Remote Objects (QtRO). These are two completely different solutions for two different problems.
The QtRO employs the concept of source and replicas to establish a peer-to-peer network. A source exposes an object instance to clients. All clients connecting to the same endpoint interact with the same source object. Also, note that using external QIODevices with QtRO is cumbersome. Cute uses a client/server model.
With the Cute server, clients interact with remote objects as if they were local. Everything related to the network is abstracted away. The Cute server maps exposed classes instead of instances to endpoints. The Cute server creates a new remote object whenever a client connects to an endpoint. In addition, the Cute server supports many clients through epoll-based event dispatchers.
Cute aims at streamlining client-server communication by enabling developers to communicate over a network using the intuitive signals and slots mechanism.
Regarding security, the Cute server uses custom data streamers to handle malicious clients sending rogue data to the server. Through error handlers, developers can take action whenever suspicious activities occur.
Enabling object-based network interaction transparently through signals/slots makes Cute unique. Cute hides everything related to network communication by using the Qt's signal and slot mechanism as a communication protocol.
With Cute, developers do not have to learn anything new to implement systems communicating over networks.
Object-based network interaction is not a new thing. The Common Object Request Broker Architecture (CORBA) was the first attempt to standardize network-based object interaction. Unfortunately, complexity and poor performance doomed CORBA-based communication protocols to failure.
Cute shines by simplifying network-based interaction through the intuitive and well-known Qt signals and slots mechanism while providing a highly efficient messaging system.
Cute facilitates the implementation of connected systems communicating over a network by enabling developers to write server code as a set of classes containing specially-tagged signals and slots.
Also, with Cute, developers implement connected clients with local objects interacting through signals and slots.
In addition, the Cute server uses epoll-based event dispatchers that enable the Cute server to handle 1 million connections while staying responsive under heavy load.
The client application uses the FortunesClient
class as shown below:
#include "FortunesClient.h"
#include <QCoreApplication>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
const auto args = QCoreApplication::arguments();
QUrl remoteObjectUrl;
for (const auto &arg : args)
{
if (!arg.startsWith(QStringLiteral("--remoteObjectUrl=")))
continue;
remoteObjectUrl = QUrl(arg.mid(18));
}
if (remoteObjectUrl.isEmpty())
qFatal("Given remoteObjectUrl is empty. Specify the remote object URL as follows: --remoteObjectUrl=remote_object_url");
FortunesClient fortunesClient(remoteObjectUrl);
return QCoreApplication::exec();
}
In the above app, the fortunesClient
object keeps writing fortunes to the console.
Although we showed above all the example's C++ source code, it is also available on GitHub.
We use the CMake file shown below to build the code:
cmake_minimum_required (VERSION 3.7 FATAL_ERROR)
project(Fortunes)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Concurrent Network WebSockets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Concurrent Network WebSockets)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_library(RemoteObjectsLib SHARED
Fortunes.cpp
Fortunes.h)
target_include_directories(RemoteObjectsLib PRIVATE ${CUTE_SERVER_SDK_DIR}/include)
target_link_libraries(RemoteObjectsLib
${CUTE_SERVER_SDK_DIR}/lib/libCuteServerSdk.so
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::WebSockets)
endif()
add_executable(FortunesApp
FortunesClient.h
Main.cpp)
if (APPLE)
target_link_libraries(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/CuteClientSdk.framework)
elseif (WIN32)
target_include_directories(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/include)
target_link_directories(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/lib)
target_link_libraries(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/lib/CuteClientSdk$<$<CONFIG:Debug>:d>.lib)
else ()
target_include_directories(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/include)
target_link_directories(FortunesApp PRIVATE ${CUTE_CLIENT_SDK_DIR}/lib ${QT_SDK_DIR}/lib)
target_link_libraries(FortunesApp PRIVATE -lCuteClientSdk)
endif ()
target_link_libraries(FortunesApp PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::WebSockets)
The CMake file builds the remote objects library that the Cute server loads on startup if building on Linux, and the FortunesApp
, which uses the RemoteObject
class to create and interact with remote objects on the server.
We use the command below to build the project on Linux:
# Configure project
cmake -DCMAKE_BUILD_TYPE=Release \
-DCUTE_CLIENT_SDK_DIR=/home/glauco/Programming/Cute/Qt6/cute-client-sdk \
-DCUTE_SERVER_SDK_DIR=/home/glauco/Programming/Cute/Qt6/cute-server-sdk \
-DCMAKE_PREFIX_PATH=/home/glauco/Programming/Qt/SDK/6.2.3/gcc_64 \
/home/glauco/Programming/MyProjects/CuteExamples/Fortunes
# Build project
make -j12
Now we will use the Cute server Startup edition to load the remote objects library built above.
On cuteserver.io, users can download all Cute server editions as self-extractable, compressed tar archives built with makeself. We install the server editions by executing the downloaded files. The --target command-line option allows users to set the installation directory.
The commands below install the Cute server Startup edition (users can download all Cute server editions and SDKs on cuteserver.io).
# Installing Cute Server Startup edition to /opt/cute-server-startup
chmod a+x ./cute-server-startup.run
./cute-server-startup.run --target /opt/cute-server-startup
The Cute server uses a text file with the INI format for configuration. We use a file with the contents shown below as the configuration file for the Cute server Startup edition:
[Common]
logTo = syslog
logLevel = Info
remoteObjectsLib = /tmp/fortunes-build/libRemoteObjectsLib.so
workerCount = 1
[Listener.AllIPV4]
address = 0.0.0.0
port = 8440
All Cute server editions allow evaluation on AWS EC2/Lightsail for 30 days. Otherwise, a license is required, and users must specify them in the configuration file. For example, if the file license.txt
contains a purchased subscription license, then the configuration file specifies the purchased license as follows:
[Common]
licenseFilePath = /location/of/license.txt
logTo = syslog
logLevel = Info
remoteObjectsLib = /tmp/fortunes-build/libRemoteObjectsLib.so
workerCount = 1
[Listener.AllIPV4]
address = 0.0.0.0
port = 8440
We run the Cute server Startup edition as follows:
/opt/cute-server-startup/bin/cute-server-startup \
-f /tmp/fortunes/cute-server.config
The cute-server.config
file in the above command is the configuration file with the INI format we showed previously.
Now we run the FortunesApp
to interact with the Cute server Lite instance that we started with the above command as follows:
/tmp/fortunes-build/FortunesApp \
--remoteObjectUrl=cute://127.0.0.1:8440/fortunes
If we run the Cute server on a remote machine, we have to change the IP shown above to the remote machine's public IP.
For example, on Windows, we built the Qt6 debug version of the Fortune example as follows:
# Configure project
cmake -DCUTE_CLIENT_SDK_DIR=C:/Programming/CuteClientSDK/Qt6 \
-DCMAKE_PREFIX_PATH=C:/Programming/Qt/SDK/6.2.3/msvc2019_64 \
C:/Programming/MyProjects/CuteExamples/Fortunes
# Build project
cmake --build . --config Debug
# Add directory with CuteClientSdkd.dll to PATH
set PATH=%PATH%;C:\Programming\CuteClientSDK\Qt6\bin
# Run FortunesApp.exe
cd Debug
FortunesApp.exe --remoteObjectUrl=cute://3.135.189.201:8440/fortunes
And on macOS, we built the Qt5 version of the Fortune example as follows:
# Configure project
cmake -DCMAKE_BUILD_TYPE=Release \
-DCUTE_CLIENT_SDK_DIR=/Users/glauco/Programming/Cute/CuteClientSDK/Qt5 \
-DCMAKE_PREFIX_PATH=/Users/glauco/Programming/Qt/SDK/5.15.2/clang_64 \
/Users/glauco/Programming/MyProjects/CuteExamples/Fortunes
# Build project
make -j4
# Run FortunesApp
./FortunesApp --remoteObjectUrl=cute://18.223.213.130:8440/fortunes
The IPs 3.135.189.201
and 18.223.213.130
were the public IPs that AWS gave us in the Ohio Region.
In the Fortunes example, we see that the Cute server abstracts everything related to network interaction from developers.
Although ignored in this example, the only part of network-based interaction that cannot be abstracted away refers to authentication and client identification to enable developers to identify and block malicious clients. The argument that the Cute server passes when constructing instances of the remote objects provides this capability.
Clients can provide authentication data through the sessionData
argument of the RemoteObject
class constructor. Remote objects access this data using the IConnectionInformation
instance passed in the constructor. The IConnectionInformation
instance enables remote objects to fetch network-related information so that they can take action to block malicious clients.
This example showcases how developers can implement network-based interaction using only the Qt's signals and slots mechanism. There are minimal differences between code that interacts over a network to code that interacts through signals and slots.
With Cute, developers focus on interactions through the intuitive signals and slots mechanism without dealing with sockets, servers, or having to write or parse messages. Cute does all the hard work for them.
Cute enables developers to write expressive code that concisely interacts over a network through the signals and slots mechanism.
In the Cute Learning Hub, users can learn how to install, configure and run servers. With Cute, writing connected code is only a matter of exposing remote classes on the server and interacting with remote objects on the client-side.