Staring Qt network programming
-
Hi all,
finally I could work out the issues dealing with my projects to do their jobs properly, and now they work as expected! :)
The job/purpose: The server app shows IP address plus a port number and a label on its UI. On the other side, the client app uses those IP and port number to connect to the server and then sends it messages using a Send button on its UI.
I may have a couple of questions to be sure I've figured out all the things related to the projects appropriately, but for this post there's merely a question: How good the projects are written to you? Which part would you add/remove/change if you were to program the projects for the purpose mentioned above, please?
client.h
:#include <QDataStream> #include <QObject> class QTcpSocket; class Client : public QObject { Q_OBJECT public: explicit Client(QObject *parent = nullptr); public slots: void sendAddress(QString, QString); void sendMessage(const QString&); private: QTcpSocket* tcpSocket { nullptr }; QDataStream out; };
client.cpp
:#include "client.h" #include <QtNetwork> Client::Client(QObject *parent) : QObject{parent} , tcpSocket(new QTcpSocket(this)) { } void Client::sendAddress(QString ip, QString port) { tcpSocket->abort(); tcpSocket->connectToHost(ip, port.toInt()); } void Client::sendMessage(const QString& message) { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setDevice(tcpSocket); out.setVersion(QDataStream::Qt_5_10); out << message; tcpSocket->write(block); }
client's qml file
:ColumnLayout { anchors.fill: parent TextField { id: ipAddrs; placeholderText: qsTr("IP Address ...") } TextField { id: portNum; placeholderText: qsTr("Port Number ...") } RowLayout { TextField { id: txtField Layout.fillWidth: true } Button { text: qsTr("Send") onClicked: { myObj.sendAddress(ipAddrs.text.toString(), portNum.text.toString()) myObj.sendMessage(txtField.text) } } } } MyClass { id: myObj }
server.h
:#include <QObject> #include <QAbstractSocket> class QTcpServer; class QTcpSocket; class Server : public QObject { Q_OBJECT Q_PROPERTY(QString message READ getMessage WRITE setMessage NOTIFY messageChanged) public: explicit Server(QObject *parent = nullptr); public slots: QString initServer(); void onNewConnection(); void writeMessage(); void setMessage(const QString&); QString getMessage() const; void displayError(QAbstractSocket::SocketError); signals: void messageChanged(QString); private: QTcpServer* tcpServer { nullptr }; QTcpSocket* tcpSocket { nullptr }; QDataStream in; QString message; };
server.cpp
:#include "server.h" #include <QtNetwork> #include <QtCore> Server::Server(QObject *parent) : QObject{parent} , tcpServer(new QTcpServer(this)) , tcpSocket(new QTcpSocket(this)) { } QString Server::initServer() { tcpServer = new QTcpServer(this); if(!tcpServer->listen()) return "Server Unable to start the server: " + tcpServer->errorString(); connect(tcpServer, &QTcpServer::newConnection, this, &Server::onNewConnection); connect(tcpSocket, &QAbstractSocket::errorOccurred, this, &Server::displayError); QString ipAddress; QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses(); // use the first non-local IPv4 address for(int i=0; i<ipAddressesList.size(); ++i) if(ipAddressesList.at(i) != QHostAddress::LocalHost && ipAddressesList.at(i).toIPv4Address()) { ipAddress = ipAddressesList.at(i).toString(); break; } // if we did not find one, use IPv4 localhost if(ipAddress.isEmpty()) ipAddress = QHostAddress(QHostAddress::LocalHost).toString(); return " The server is running on\n\n IP: " + ipAddress + "\n port: " + QString::number(tcpServer->serverPort()) + "\n\n Run the Client example now."; } void Server::onNewConnection() { QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); connect(clientConnection, &QAbstractSocket::disconnected, clientConnection, &QObject::deleteLater); in.setDevice(clientConnection); in.setVersion(QDataStream::Qt_4_0); connect(clientConnection, &QAbstractSocket::readyRead, this, &Server::writeMessage); } void Server::displayError(QAbstractSocket::SocketError socketError) { // for this stage, printing the error messages using qDenug will be fine switch (socketError) { case QAbstractSocket::RemoteHostClosedError: break; case QAbstractSocket::HostNotFoundError: qDebug() <<"The host was not found. Please check the " "host name and port settings."; break; case QAbstractSocket::ConnectionRefusedError: qDebug() << "The connection was refused by the peer. " "Make sure the server is running, " "and check that the host name and port " "settings are correct."; break; default: qDebug() << "The following error occurred: " + tcpSocket->errorString(); } } void Server::writeMessage() { in.startTransaction(); QString msg; in >> msg; if (!in.commitTransaction()) setMessage("commitTransaction error"); else setMessage(msg); } QString Server::getMessage() const { return message; } void Server::setMessage(const QString& newMessage) { message = newMessage; emit messageChanged(message); }
server's qml file
:ColumnLayout { anchors.fill: parent Label { text: myObj.initServer() } Label { text: myObj.message } } MyClass { id: myObj }
Thanks so much in advance.
-
@qcoderpro said in Staring Qt network programming:
How good the projects are written to you
well thats obviously very subjective, but I find nothing to critic, except naming/registering both your client and server instance MyClass. I assume they are different projects ? But it is bound to cause confusion anyway :D
-
@qcoderpro said in Staring Qt network programming:
I thought some part of code is probably redundant
like the
tcpSocket
of your Server class ? yes, true -
@qcoderpro But where are you doing anything with this socket?!
The fact that you connect one signal from that socket to a slot does not mean that this socket is doing manythig useful...
You get the socket from tcpServer->nextPendingConnection(), so why do you need tcpSocket? -
@qcoderpro said in Staring Qt network programming:
Disagree
in principle, no I do not disagree. Listing to the error Signal of your socket is a good thing :D
but, you're neither sending nor receiving data with that socket. That is all done, like @jsulm said,
here:void Server::onNewConnection() { QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); connect(clientConnection, &QAbstractSocket::disconnected, clientConnection, &QObject::deleteLater); in.setDevice(clientConnection); in.setVersion(QDataStream::Qt_4_0); connect(clientConnection, &QAbstractSocket::readyRead, this, &Server::writeMessage); }
That socket is correctly cleaned up thumbs up but you're not listening to the error signal :D
-
@jsulm @J-Hilk
I didn't know that since I'm very new to network programming. So you mean to remove thetcpSocket
and that connect() from the server app and call the displayError slot this way?void Server::onNewConnection() { QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); connect(clientConnection , &QAbstractSocket::errorOccurred, this, &Server::displayError); connect(clientConnection, &QAbstractSocket::disconnected, clientConnection, &QObject::deleteLater); in.setDevice(clientConnection); in.setVersion(QDataStream::Qt_4_0); connect(clientConnection, &QAbstractSocket::readyRead, this, &Server::writeMessage); }
-
@qcoderpro said in Staring Qt network programming:
I didn't know that since I'm very new to network programming. So you mean to remove the tcpSocket and that connect() from the server app and call the displayError slot this way?
yes, or, if you want to keep a pointer to the socket as a class member variable, you could do:
void Server::onNewConnection() { if(tcpSocket) //Some warning or other error handling, because old connection is still in use! return; tcpSocket = tcpServer->nextPendingConnection(); connect(tcpSocket , &QAbstractSocket::errorOccurred, this, &Server::displayError); connect(tcpSocket, &QAbstractSocket::disconnected, tcpSocket, [=] () ->void{ //A Lambda, so we can also reset the pointer to nullptr tcpSocket->deleteLater(); tcpSocket = nullptr; }); in.setDevice(tcpSocket); in.setVersion(QDataStream::Qt_4_0); connect(tcpSocket, &QAbstractSocket::readyRead, this, &Server::writeMessage); }
you would still have to remove the new QTcpSocket from your constructor and initialise tcpSocket as nullptr for this to work.
-
Since, as stated earlier, I'm very new to network programming, let me please start from beginning to figure out the things properly and be sure about that.
Let's start from the server app. Inserver.cpp
starting from top, we havetcpServer(new QTcpServer(this)) { }
.
Here tcpSever is an object whose job is to listen to incoming connections (tcpSockets) and accept one or a number of them. We initialize it and pass the program/project's pointer (this) to it as the parent.
Next in:// tcpServer = new QTcpServer(this); if(!tcpServer->listen()) return "Server Unable to start the server: " + tcpServer->errorString(); connect(tcpServer, &QTcpServer::newConnection, this, &Server::onNewConnection);
we probably can remove the first line which is repetitive! Then the tcpServer will listen on all network interfaces for incoming connections/tcpSockets (IP + port number) and returns an error if it fails listening.
If a connection arrives, connect() invokes the onNewConnection slot.
Completely right up to this point, please? -
@qcoderpro said in Staring Qt network programming:
Completely right up to this point, please?
yes,
see I didn't even notice the 2nd QTcpServer instance, the problem if you just "scroll" through it :D
-
see I didn't even notice the 2nd QTcpServer instance, the problem if you just "scroll" through it :D
Yeah, no problem, thanks for your kindness. :)
Moving on, we reach this slot:void Server::onNewConnection() { QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); connect(clientConnection, &QAbstractSocket::errorOccurred, this, &Server::displayError); connect(clientConnection, &QAbstractSocket::disconnected, clientConnection, &QObject::deleteLater); in.setDevice(clientConnection); in.setVersion(QDataStream::Qt_4_0); connect(clientConnection, &QAbstractSocket::readyRead, this, &Server::writeMessage); }
in which we have clientConnection that is a QTcpSocket representing the server side of the connection!
- But I assume that clientConnection is the one we have already sent from the client app to this server's
connect(tcpServer, &QTcpServer::newConnection, this, &Server::onNewConnection);
and caused the above slot to be invoked! I believe we don't need two tcpSockets, one already sent to the server and another created inside the server the way above! Agree? - In the next connect(), we call the displayError in case of any connection error. Connection errors may happen due to the Internet, operating system or the hardware failures. Using QAbstractSocket::errorOccurred we try to catch and then show them to the user. Right?
- Using the second connect() in the slot above, we aim to delete the clientConnection when we later on exit the slot! But this is ambiguous! If the clientConnection was the one we already sent from the client app, why should we delete it when we exit the slot (onNewConnection)? So something in my assumptions should not be correct!
- But I assume that clientConnection is the one we have already sent from the client app to this server's
-
@qcoderpro said in Staring Qt network programming:
one already sent to the server and another created inside the server
- Sockets are not send to anywhere. You have one socket on the client side and one on the server side and both communicate with each other. The socket on the server side is the one you get using tcpServer->nextPendingConnection(). These two sockets are two different objects living in two different processes (and usually on different machines)!
- Yes
- As said in 1.: sockets are not send to anywhere!
-
Yeah, no problem, thanks for your kindness. :)
no problem I think we have a very pleasant conversation/exchange, I like it :D
1But I assume that clientConnection is the one we have already sent from the client app to this server's connect(tcpServer, &QTcpServer::newConnection, this, &Server::onNewConnection); and caused the above slot to be invoked! I believe we don't need two tcpSockets, one already sent to the server and another created inside the server the way above! Agree?
I'm not sure what you mean, you're not literally sending a socket via the network ? You're creating one on the server side and one on the client side and those communicate with each other.
The QTCPServer is creating the socket and you get pointer to it via the nextPendingConnection call, so you're actually not (explicitly) creating a QTcpSocket instance on your own.In the next connect(), we call the displayError in case of any connection error. Connection errors may happen due to the Internet, operating system or the hardware failures. Using QAbstractSocket::errorOccurred we try to catch and then show them to the user. Right?
well, off all those
https://doc.qt.io/qt-5/qabstractsocket.html#SocketError-enum
and yes unplugging the hardware cable etc should generate an error you can catch here
Using the second connect() in the slot above, we aim to delete the clientConnection when we later on exit the slot! But this is ambiguous! If the clientConnection was the one we already sent from the client app, why should we delete it when we exit the slot (onNewConnection)? So something in my assumptions should not be correct!
sockets aren't sent anywhere, they are parented to the QTcpServer instance, but when you don't need them anymore, you should delete them to free memory, thats what the delete later is for.
https://doc.qt.io/qt-5/qtcpserver.html#nextPendingConnectionThe socket is created as a child of the server, which means that it is automatically deleted when the QTcpServer object is destroyed. It is still a good idea to delete the object explicitly when you are done with it, to avoid wasting memory.
-
@J-Hilk @jsulm
OK, thank you both. To clarify the dark things more, I run both projects and type the IP address, port number (provided by the server) and a "Hi" message on the client app and click on the Send button on it. The message will be printed on the server's UI.Let's explore what happens one by one.
-
A socket is an endpoint instance in a communication through a computer network and is defined by an IP address and a port number. So the first socket is (192.168.56.1 and 53386) and either of the apps is an endpoint. We send that socket (I mean that IP and port) to the server app which will be caught by the
connect(tcpServer, &QTcpServer::newConnection, this, &Server::onNewConnection);
and this, in turn, invokes the onNewConnection slot. There, a new socket (the server's side one) will be created to form the connection with the first socket from the client, completely. Therefore, communications through networks are made via sockets and through them data will be exchanged too. Agree with these all, please? -
For the clientConnection socket, the displayError will aim at catching errors (if any) and that socket is then set for deletion when we exit the onNewConnection slot.
-
Then we assign clientConnection to be the device the DataStream
in
will use for data exchange as the output and the first socket will act like input for that data.
Agree with 2 & 3 too?
-
-
@qcoderpro said in Staring Qt network programming:
Agree with these all, please
no, but close enough
For the clientConnection socket, the displayError will aim at catching errors (if any)
yes
and that socket is then set for deletion when we exit the onNewConnection slot.
no, its set for deletion when the socket connection is lost, not for the scope end of onNewConnection, as the QTcpSocket is heap allocated.
Then we assign clientConnection to be the device the DataStream in will use for data exchange as the output and the first socket will act like input for that data.
yes
-
no, but close enough
- Is the statement "We send that socket (I mean ..." what you disagree with, please, or other things in 1) too?
its set for deletion when the socket connection is lost
-
When is that connection lost, please? Is it when we close both projects (or one of them)? I say that because when we once send the IP and port number to the server and the connection is that way established, we can send as many messages as we wish to, without sending the IP and port number another time.
-
For the line
in.setVersion(QDataStream::Qt_4_0);
we can use QDataStream::Qt_6_3 rather than that version 4 but the important matter is that the other app ought to use the same version too. Right? -
And the
connect(clientConnection, &QAbstractSocket::readyRead, this, &Server::writeMessage);
will invoke the the slotwriteMessage
when the QDataStreamin
notifies its device's socket (clientConnection) that a message has completely arrived from the sender and is now ready to read. Right?