One of the key features of the QT framework is providing communication between objects via signals and slots. For QT developers, this is a very convenient and organic way to do things, but the nature of the feature does not allow to communicate between objects located in different address spaces. Therefore, the need arose to create a library that would allow to establish communication between objects located in different processes in a way when user just needs to connect signals and slots of these objects.
The results of this efforts I called the QRpc library, and here is how it is applied and built.
Contents
Applying QRpc library
In order to connect the signal of the client to the server slot we need to call the regular connect:
connect(this, SIGNAL(MySignal()), m_server, SLOT(ServerSlot());
If in response to calling ServerSlot() server sends back data, letโs agree that this is a signal called ServerSlot_response(). In order to connect this server signal to the client slot we also need to call the regular connect:
connect(m_server, SIGNAL(ServerSlot_ response ()), this, SLOT(ClientSlot());
Now, letโs see how the client and the server should look like. First of all, letโs introduce some terms that will be used later.
Real client โ an instance of any of the client classes that inherits ClientBase (provided by QRpc). Instances of this class can connect their signals to slots of the real server. Real client calls ClientBase for pointers to a remote server (pseudo-server) in order to connect its own signals.
Real server โ an instance of any server class that inherits ServerBase (provided by QRpc). It must configure an instance of a remote client (pseudo-client) stored in ServerBase by calling the corresponding base class method.
Pseudo-server (same as remote server) โ an object that acts as a receiver of the signal from the client. From the client-server model standpoint, pseudo-server is the one that is acting as a client, as strange as it sounds. QRpc provides the RemoteServer class as a pseudo-server.
Pseudo-client (same as remote client) โ an instance of the RemoteClient class (provided by QRpc). It is not visible for the library user, and it is used in ServerBase.
Terms above describe entities that together with base client and base server comprise a full set of entities providing communication between objects, located in different processes.
Requirements for client and server implementation
Real client must satisfy the following conditions:
- It must inherit the ClientBase class
- Signals need to be connected to the remote server slots via the string form:
connect(QObject*, const char*, QObject*, const char*)
- Pseudo-server should act as a receiver of the signal that needs to be connected to the server slot. The pseudo-server pointer needs to be received from the base class:
ConnectToServer(port, host, parent)
class MyClient : private ClientBase
{
public:
MyClient();
signals:
void MySignal();
private:
qRpc::ServerEmulated* m_server
}
MyClient:: MyClient()
{
QObject* remoteServer = ConnectToServer (serverIp, port, this);
connect(this, SIGNAL(MySignal()), remoteServer, SLOT(ServerSlot());
}
On the server side, it is enough to inherit ClientBase and call the Listen(port, realServer) method in order to configure pseudo-client and start listening to a socket.
This will be enough to call the ServerSlot() method on the server with โserverIpโ that listens to โportโ after the client emits MySignal(). If such server does not exist, or the server doesnโt have such method, the signal will be ignored.
class MyServer
{
โฆ
public slots:
void ServerSlot();
}
MyServer::MyServer()
{
SetClientEmulated(port, this);
}
It is worth noting, that there are no restrictions on connecting other recipients in the connect method of the client.
QRpc also allows to connect server signals to slots of the client. And by server signal we mean the response of the server to the client query. For example, the client sends the registration data to the server, and server returns an id to the client. In order to send a signal, server needs to use the method SendToRealClient. Any number of parameters can be included in this method.
Read also:
Objective-C Quick Tutorial for Beginners: Learn Objective-C Basics and Specifics from Scratch
Example of using the library
Letโs take a closer look on how the library is used by using an example of registering chat client. Registering procedure constitutes sending a string to the server and getting an id in response.
//ChatClient.h
class ChatClient : public ClientBase
{
signals:
void Registrate(const QString&);
private slots:
void OnIdRecived (int id, const QString& name);
}
//ChatServer.h
class ChatServer : public QObject, private qRpc::Server
{
Q_OBJECT
public slots:
void OnRegistration(const QString& name);
}
In order to send the string to the server we need to connect the ChatClient::Registrate signal in the client code to the ChatServer::OnNewMsg slot.
<pre>//ChatClient.cpp
connect(server, SIGNAL(OnRegistration_response(const QString&),
this, SLOT(OnIdRecived(int, const QString& name))
</pre>
In response, client expects to receive an id.
//ChatClient.cpp
QObject* server = ConnectToServer (port, host, this);
connect(this, SIGNAL(Registrate(const QString&),
server, SLOT(OnRegistration(const QString&))
After the Registrate signal is emitted on the server with host, configured to listen to the โportโ, the OnRegistration slot will be called. In the OnRegistration slot, server will send the data to the client by calling the SendToRealClient method:
//ChatServer.cpp
ChatServer::ChatServer(int port, QObject* parent)
: QObject(parent)
{
Listen(port, this);
}
void ChatServer::OnRegistration(const QString& name)
{
const int id = InternalFunctionToRegistrateAndGenerateId(name);
SendToRealClient(id, name);
QRpc library source code and demo application that demonstrates how to use QRpc can be found here.
Demo app is a simple chat with the following features implemented:
- Connecting client to the server
- Registering members of the chat
- Sending messages from the client to the server
- Requesting message history from the server
Demo application in action
Internal design of the library
Entities, taking part in organizing RPC
In the previous section we already mentioned entities that constitute the foundation of the library. Now we will take a closer look at them.
ClientBase serves as the basic class for the client classes. Main purpose of this class is to overload the connect method and provide the ConnectToServer method.
The connect method is overloaded in such a way that if the pointer to RemoteServer acts as a receiver, then the signal connects via QMetaObject::connect. After this, emitting any signal connected to the pseudo-server slot leads to calling the RemoteServer::qt_metacall(QMetaObject::Call call, int id, void** argv) method, then the couple โslot name โ signal nameโ is saved in the container that is the member of RemoteServer class. If the pointer to the RemoteServer class instance is the sender of the signal, then instead of an actual connection only the data about the sender, the signal signature and the slot signature will be saved in the RemoteServe instance, pointer to which will be given as a receiver.
ClientBase:: ConnectToServer (int port, const QString& host, QObject* parent) creates pointer to a pseudo-server and returns it to the real client, allowing it to connect its signals to pseudo-server slots (in actuality, pseudo-slots).
RemoteServer class defines the qt_metacall method. This method can be called after any signal connected to any pseudo-server slot has been emitted. In other words, the qt_metacall method is used in this situation as a universal slot. Qt_metacall receives pointers to the array of parameters given to the slot as the argument. Metamethod of the signal, which emit has caused the call of the qt_metacall method, is received by using the Qt metaobject system: sender()->metaObject()->method(senderSignalIndex()). Next, we receive the signal signature from the received metamethod and use it to get the list of slots to which this signal was connected (slot list is filled in the ClientBase::connect method). After this, pseudo-server will have all the necessary information to send the query to a real server, i.e. list of slots that need to be called and an array of parameters necessary to call these slots.
Overloaded connect method:
QMetaObject::Connection qRpc::Client::connect(QObject* sender, const char* signal, QObject* receiver, const char* method, Qt::ConnectionType type)
{
RemoteServer* serverReceiver = dynamic_cast< RemoteServer *&qt;(receiver);
RemoteServer * serverSender = dynamic_cast< RemoteServer *&qt;(sender);
if(serverReceiver != nullptr && serverSender == nullptr)
{
QMetaObject::Connection connection;
const QString normalazedSignal(utils::GetNormalizedSignature(signal));
if (!serverReceiver-&qt;IsSignalConnected(normalazedSignal))
{
const int signIndx =
sender-&qt;metaObject()-&qt;indexOfSignal(normalazedSignal.toStdString().data());
connection = QMetaObject::connect(this, signIndx, serverReceiver, 0);
}
serverReceiver-&qt;AddConnection(normalazedSignal, method);
return connection;
}
else if (serverSender != nullptr && serverReceiver == nullptr)
{
serverSender-&qt;AddConnection(signal, method, receiver);
return QMetaObject::Connection();
}
else
{
return QObject::connect(sender, signal, receiver, method, type);
}
}
ServerBase is the basic class for server classes. This class provides two methods โ Listen and SendToRealClient. Instance of the RemoteClient class (stored in ServerBase) in the listen method is configured to listen to the port, the number of which is provided via the parameter. The EmitUniversalSignal method allows to send a response to the client giving any number of parameters.
The RemoteClient class stores a pointer to QtcpServer and starts listening to the port, the number of which was provided via a parameter, in constructor. When data is received through this port, the list of slots that need to be called is read, and according to the slot signature, all the parameters necessary to call slots are read. Next, parameters are changed to the array of pointers to void and slots are gradually called.
Using metaobject system to imitate connecting signals and slots, contained in remote processes
If we leave out the details regarding the serialization of parameters, network interactions and implementation of pseudo-client and pseudo-server, then we can say that imitation of signals connecting client to server slots is achieved by overloading the connect method and defining the qt_metacall method.
Due to the fact that in the overloaded connect method of the client connection of the signal to the slot is done in the following way:
connection = QMetaObject::connect(this, signIndx, serverReceiver, 0);
emitting any client signal leads to calling the qt_metacall method from the signal receiver. In this case, the receiver is a pseudo-server. Declaration of the qt_metacall method is a part of the Q_OBJECT macro, while the definition is in moc_ file, which is why RemoteServer doesnโt use Q_OBJECT macro, instead using its own implementation of the qt_metacall method that sends the query to the server.
Pseudo-client (server-side) also uses metaobject system, but in its standard form. When receiving a request from the real client, pseudo-client reads the signature of the slot that needs to be called. Index of the slot is received from the signature and then the qt_metacall method of the real server is called.
qt_metacall(QMetaObject::InvokeMetaMethod, slotIndx, argv);
Naming convention of server signals expected in response to the server slot call
We assume that real server doesnโt have any real signals that it connects to client slots, however, as a response to call of any slot, server can send reply to the client. Client, on the other hand, can connect its slot to the response on calling the server slot. To do this, the pointer to the pseudo-server (received in the ClientBase:: ConnectToServer method) needs to be given as a sender at connection. As the signal, the <slot _name>_response(<types_used_by_a_slot>) construction needs to be transferred.
connect(remoteServer, SIGNAL(ServerSlot_response ()), this, SLOT(ClientSlot());
ServerSlot โ is the name of the slot of the server, from which the response to the client was send.
_response โ is the suffix that needs to be added to the clientโs name
There is one difference from the standard connect method here, because the slot server signature often will differ from the signal server signature. For example, the following code is valid, considering the nature of the signal from the server.
connect(m_server, SIGNAL(ServerSlot_response(QString)), this, SLOT(ClientSlot(int));
Transferring parameters to slots of the object located in another process
To serialize data, the QRpc library uses the QVarian โ QDataStream couple (look below for more details), this is why when emitting signal, real client can transfer types as parameters, for which the following conditions are met:
- QVariant has a constructor that accepts this type
- Type has overloaded stream operators >><< for QDataStream
For example:
emit ClientSignal(10, QString(), QTime());
Other types can be given via QByteArray, In order to convert to QByteArray the function ConvertToByteArray<T>(ะข param) is provided. Types, introduced into this method, need to have overloaded stream operators for QDataStream.
The ConvertFromByteArray function is provided for deserialization. Here is an example of how it can be used:
//client
QByteArray serialized;
ConvertToByteArray<MyClass&qt;(myClass, serialized)
emit ClientSignal(serialized);
//server
void ServerSlot(QByteArray& serialized)
{
ConvertFromByteArray<MyCustomClass&qt;(m_customObject, serialized)
}
To transfer QList and std::vector containers the SerializeContainer ะธ DesrializeContainer functions are used. Elements of the container need to have overloaded stream operators for QDataStream.
For example, real client needs to emit the signal that should have serialized vector, while a slot that uses this vector needs to be executed on the server.
Real client needs to implement the following:
connect(this, SIGNAL(SendVector(QByteArray)), remoteServer, SLOT(ReceiveVector(QByteArray)));
โฆ
QByteArray serialized;
SerializeContainer (m_container, serialized)
emit Signal(serialized)
Real server slot can look like this:
void ReceiveVector(QByteArray& serialized)
{
DeserializeContainer (n_container, serialized);
Data serialization
As already mentioned above, the QVariant โ QdataStream couple is used for serialization. Before sending data to the real server (via the qt_metacall method), pseudo-client knows the signature of the signal, that lead to calling the qt_metacall method, and uses the qt_metacall argument to receive the pointer to the array of parameters. Knowing signature of the signal, pseudo-server can learn the list of slots that needs to be run on the server and saves it into QStringList, then this list is saved into QDataStream.
quint16(0) is saved as the first parameter, in order to record the size of the data in this field after saving all parameters.
After saving the list of slot signatures in QDataStream, pseudo-server starts to parse parameters given in the pointer on the array of parameters. Parameters are transformed to QVariant and saved in QDataStream. In the end, we have the following data structure:
Each QVariant stores data about the basic type that will be used to deserialize the data.
When deserializing, signature of the slot is read from QDataStream. With the signature, we get the index of the slot, that needs to be called.
int slotIndx = receiver->metaObject()->indexOfSlot(signature.toStdString().data());
Knowing the number of parameters (based on the signature), we form the QVariant vector.
const int numberOfParams = args.size();
for (int i = 0; i < numberOfParams; ++i)
{
QVariant type;
inStream &qt;&qt; type;
argv.push_back(type);
}
After this, each element of the array is casted to pointer on void.
std::vector<void*&qt; castToVoid;
for (auto& arg : argv)
{
castToVoid.push_back(const_cast<void*&qt;(reinterpret_cast<const void*&qt;(arg.data())));
}
After getting the array of vector pointers to parameters and slot index, the standard qt_metacall method of real server is called.
m_realServer->qt_metacall(QMetaObject::InvokeMetaMethod, slotIndx, &castToVoid[0]);
Conclusion
The main goal of RPC is to organize the interaction of objects, located in different processes, as well as different objects, located in a single process. Using QRpc library allows to fully achieve this goal.
Also, check out our another article to learn how to create a QML project in the Qt creator.
Downloads
The latest library source code verion: https://github.com/Krestol/QRpcChat
The general scheme of the solution: UML in PDF