WIP-How to create a simple chat application: Difference between revisions
No edit summary |
No edit summary |
||
Line 97: | Line 97: | ||
, m_loggedIn(false) | , m_loggedIn(false) | ||
{ | { | ||
// Forward the connected and disconnected signals | |||
connect(m_clientSocket, &QTcpSocket::connected, this, &ChatClient::connected); | |||
connect(m_clientSocket, &QTcpSocket::disconnected, this, &ChatClient::disconnected); | |||
// connect readyRead() to the slot that will take care of reading the data in | |||
connect(m_clientSocket, &QTcpSocket::readyRead, this, &ChatClient::onReadyRead); | |||
// Forward the error signal, QOverload is necessary as error() is overloaded, see the Qt docs | |||
connect(m_clientSocket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this, &ChatClient::error); | |||
// Reset the m_loggedIn variable when we disconnec. Since the operation is trivial we use a lambda instead of creating another slot | |||
connect(m_clientSocket, &QTcpSocket::disconnected, this, [this]()->void{m_loggedIn = false;}); | |||
} | |||
</code> | </code> | ||
In the constructor we initialize the parent class and the members, then we take care of connecting to the signals coming from the socket. | In the constructor we initialize the parent class and the members, then we take care of connecting to the signals coming from the socket. | ||
<code> | <code> | ||
void ChatClient::login(const QString &userName) | |||
{ | |||
if (m_clientSocket->state() == QAbstractSocket::ConnectedState) { // if the client is connected | |||
// | // create a QDataStream operating on the socket | ||
QDataStream clientStream(m_clientSocket); | |||
// | // set the version so that programs compiled with different versions of Qt can agree on how to serialise | ||
clientStream.setVersion(QDataStream::Qt_5_7); | |||
// | // Create the JSON we want to send | ||
QJsonObject message; | |||
message["type"] = QStringLiteral("login"); | |||
message["username"] = userName; | |||
// send the JSON using QDataStream | |||
clientStream << QJsonDocument(message).toJson(); | |||
} | |||
} | |||
void ChatClient::sendMessage(const QString &text) | |||
{ | |||
if (text.isEmpty()) | |||
return; // We don't send empty messages | |||
// create a QDataStream operating on the socket | |||
QDataStream clientStream(m_clientSocket); | |||
// set the version so that programs compiled with different versions of Qt can agree on how to serialise | |||
clientStream.setVersion(QDataStream::Qt_5_7); | |||
// Create the JSON we want to send | |||
QJsonObject message; | |||
message["type"] = QStringLiteral("message"); | |||
message["text"] = text; | |||
// send the JSON using QDataStream | |||
clientStream << QJsonDocument(message).toJson(); | |||
} | |||
</code> | </code> | ||
The <tt>login</tt> and <tt>sendMessage</tt> are very similar as both of them send a JSON over the TCP socket. | |||
<code> | |||
void ChatClient::disconnectFromHost() | |||
{ | |||
m_clientSocket->disconnectFromHost(); | |||
} | |||
void ChatClient::connectToServer(const QHostAddress &address, quint16 port) | |||
{ | |||
m_clientSocket->connectToHost(address, port); | |||
} | |||
</code> | |||
The <tt>login</tt> and <tt>sendMessage</tt> slots just call the corresponding method on the socket | |||
<code> | |||
void ChatClient::jsonReceived(const QJsonObject &docObj) | |||
{ | |||
// actions depend on the type of message | |||
const QJsonValue typeVal = docObj.value(QLatin1String("type")); | |||
if (typeVal.isNull() || !typeVal.isString()) | |||
return; // a message with no type was received so we just ignore it | |||
if (typeVal.toString().compare(QLatin1String("login"), Qt::CaseInsensitive) == 0) { //It's a login message | |||
if (m_loggedIn) | |||
return; // if we are already logged in we ignore | |||
// the success field will contain the result of our attempt to login | |||
const QJsonValue resultVal = docObj.value(QLatin1String("success")); | |||
if (resultVal.isNull() || !resultVal.isBool()) | |||
return; // the message had no success field so we ignore | |||
const bool loginSuccess = resultVal.toBool(); | |||
if (loginSuccess) { | |||
// we logged in succesfully and we notify it via the loggedIn signal | |||
emit loggedIn(); | |||
return; | |||
} | |||
// the login attempt failed, we extract the reason of the failure from the JSON | |||
// and notify it via the loginError signal | |||
const QJsonValue reasonVal = docObj.value(QLatin1String("reason")); | |||
emit loginError(reasonVal.toString()); | |||
} else if (typeVal.toString().compare(QLatin1String("message"), Qt::CaseInsensitive) == 0) { //It's a chat message | |||
// we extract the text field containing the chat text | |||
const QJsonValue textVal = docObj.value(QLatin1String("text")); | |||
// we extract the sender field containing the username of the sender | |||
const QJsonValue senderVal = docObj.value(QLatin1String("sender")); | |||
if (textVal.isNull() || !textVal.isString()) | |||
return; // the text field was invalid so we ignore | |||
if (senderVal.isNull() || !senderVal.isString()) | |||
return; // the sender field was invalid so we ignore | |||
// we notify a new message was received via the messageReceived signal | |||
emit messageReceived(senderVal.toString(), textVal.toString()); | |||
} else if (typeVal.toString().compare(QLatin1String("newuser"), Qt::CaseInsensitive) == 0) { // A user joined the chat | |||
// we extract the username of the new user | |||
const QJsonValue usernameVal = docObj.value(QLatin1String("username")); | |||
if (usernameVal.isNull() || !usernameVal.isString()) | |||
return; // the username was invalid so we ignore | |||
// we notify of the new user via the userJoined signal | |||
emit userJoined(usernameVal.toString()); | |||
} else if (typeVal.toString().compare(QLatin1String("userdisconnected"), Qt::CaseInsensitive) == 0) { // A user left the chat | |||
// we extract the username of the new user | |||
const QJsonValue usernameVal = docObj.value(QLatin1String("username")); | |||
if (usernameVal.isNull() || !usernameVal.isString()) | |||
return; // the username was invalid so we ignore | |||
// we notify of the user disconnection the userLeft signal | |||
emit userLeft(usernameVal.toString()); | |||
} | |||
} | |||
</code> | |||
The <tt>jsonReceived</tt> method is all about JSON parsing, since it's outside the scope of this example we won't spend any-more words on it as the code itself is well commented | |||
<code> | |||
void ChatClient::onReadyRead() | |||
{ | |||
// prepare a container to hold the UTF-8 encoded JSON we receive from the socket | |||
QByteArray jsonData; | |||
// create a QDataStream operating on the socket | |||
QDataStream socketStream(m_clientSocket); | |||
// set the version so that programs compiled with different versions of Qt can agree on how to serialise | |||
socketStream.setVersion(QDataStream::Qt_5_7); | |||
// start an infinite loop | |||
for (;;) { | |||
// we start a transaction so we can revert to the previous state in case we try to read more data than is available on the socket | |||
socketStream.startTransaction(); | |||
// we try to read the JSON data | |||
socketStream >> jsonData; | |||
if (socketStream.commitTransaction()) { | |||
// we successfully read some data | |||
// we now need to make sure it's in fact a valid JSON | |||
QJsonParseError parseError; | |||
// we try to create a json document with the data we received | |||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); | |||
if (parseError.error == QJsonParseError::NoError) { | |||
// if the data was indeed valid JSON | |||
if (jsonDoc.isObject()) // and is a JSON object | |||
jsonReceived(jsonDoc.object()); // parse the JSON | |||
} | |||
// loop and try to read more JSONs if they are available | |||
} else { | |||
// the read failed, the socket goes automatically back to the state it was in before the transaction started | |||
// we just exit the loop and wait for more data to become available | |||
break; | |||
} | |||
} | |||
} | |||
</code> | |||
The <tt>onReadyRead</tt> slot is really the most interesting one. |
Revision as of 14:22, 1 June 2018
Introduction
This article will illustrate a simple chat client and server communicating over TCP. The aim is to clarify aspects of QTcpSocket/QTcpServer that are not developed in the official Qt Fortune example. This has no intention to be a fully featured chat application.
You can find the source code relating to this example on GitHub
Pre-Requisites
- Qt 5.7 or later
- An intermediate level of knowledge of Qt5
- Familiarity with C++11 concepts, especially lambdas and std::bind
The Logic
This application will use a central server that will manage the communication among clients via JSON messages. We'll implement 2 versions of the server, one that runs in a single server and one that distributes the sockets among multiple threads.
The Client
The client in this example is a simple QtWidgets application, the ui is minimal, just a button to connect to the server, a list view to display the messages received, a line edit to type your messages and a button to send them.
The core of the functionality is in the ChatClient class.
class ChatClient : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(ChatClient)
public:
explicit ChatClient(QObject *parent = nullptr);
public slots:
void connectToServer(const QHostAddress &address, quint16 port);
void login(const QString &userName);
void sendMessage(const QString &text);
void disconnectFromHost();
private slots:
void onReadyRead();
signals:
void connected();
void loggedIn();
void loginError(const QString &reason);
void disconnected();
void messageReceived(const QString &sender, const QString &text);
void error(QAbstractSocket::SocketError socketError);
void userJoined(const QString &username);
void userLeft(const QString &username);
private:
QTcpSocket *m_clientSocket;
bool m_loggedIn;
void jsonReceived(const QJsonObject &doc);
};
Let's break down what each of these methods and member does:
Private Members | |
---|---|
m_clientSocket | The socket that will interact with the server |
m_loggedIn | Used to record whether the client already agreed a username with the server or not |
Private Methods and Slots | |
onReadyRead() | Slot that reacts to data being available on the socket. It will download the data and pass it to jsonReceived() |
jsonReceived() | Method to process the messages received from the server |
Signals | |
connected() | The client connected to the server successfully |
disconnected() | The client is not connected any more |
loggedIn() | The client successfully agreed a username with the server |
loginError() | The server rejected the username provided |
messageReceived() | The client received a chat message |
userJoined() | A new user joined the chat |
userLeft() | A user left the chat |
error() | Just a forward of the QTcpSocket::error signal |
Public Slots | |
connectToServer() | Attempts the connection to a server |
login() | Attempts to agree the given username with the server |
disconnectFromHost() | Closes the connection with the server |
sendMessage() | Sends a chat message to the server |
Let's now look at the implementation:
ChatClient::ChatClient(QObject *parent)
: QObject(parent)
, m_clientSocket(new QTcpSocket(this))
, m_loggedIn(false)
{
// Forward the connected and disconnected signals
connect(m_clientSocket, &QTcpSocket::connected, this, &ChatClient::connected);
connect(m_clientSocket, &QTcpSocket::disconnected, this, &ChatClient::disconnected);
// connect readyRead() to the slot that will take care of reading the data in
connect(m_clientSocket, &QTcpSocket::readyRead, this, &ChatClient::onReadyRead);
// Forward the error signal, QOverload is necessary as error() is overloaded, see the Qt docs
connect(m_clientSocket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this, &ChatClient::error);
// Reset the m_loggedIn variable when we disconnec. Since the operation is trivial we use a lambda instead of creating another slot
connect(m_clientSocket, &QTcpSocket::disconnected, this, [this]()->void{m_loggedIn = false;});
}
In the constructor we initialize the parent class and the members, then we take care of connecting to the signals coming from the socket.
void ChatClient::login(const QString &userName)
{
if (m_clientSocket->state() == QAbstractSocket::ConnectedState) { // if the client is connected
// create a QDataStream operating on the socket
QDataStream clientStream(m_clientSocket);
// set the version so that programs compiled with different versions of Qt can agree on how to serialise
clientStream.setVersion(QDataStream::Qt_5_7);
// Create the JSON we want to send
QJsonObject message;
message["type"] = QStringLiteral("login");
message["username"] = userName;
// send the JSON using QDataStream
clientStream << QJsonDocument(message).toJson();
}
}
void ChatClient::sendMessage(const QString &text)
{
if (text.isEmpty())
return; // We don't send empty messages
// create a QDataStream operating on the socket
QDataStream clientStream(m_clientSocket);
// set the version so that programs compiled with different versions of Qt can agree on how to serialise
clientStream.setVersion(QDataStream::Qt_5_7);
// Create the JSON we want to send
QJsonObject message;
message["type"] = QStringLiteral("message");
message["text"] = text;
// send the JSON using QDataStream
clientStream << QJsonDocument(message).toJson();
}
The login and sendMessage are very similar as both of them send a JSON over the TCP socket.
void ChatClient::disconnectFromHost()
{
m_clientSocket->disconnectFromHost();
}
void ChatClient::connectToServer(const QHostAddress &address, quint16 port)
{
m_clientSocket->connectToHost(address, port);
}
The login and sendMessage slots just call the corresponding method on the socket
void ChatClient::jsonReceived(const QJsonObject &docObj)
{
// actions depend on the type of message
const QJsonValue typeVal = docObj.value(QLatin1String("type"));
if (typeVal.isNull() || !typeVal.isString())
return; // a message with no type was received so we just ignore it
if (typeVal.toString().compare(QLatin1String("login"), Qt::CaseInsensitive) == 0) { //It's a login message
if (m_loggedIn)
return; // if we are already logged in we ignore
// the success field will contain the result of our attempt to login
const QJsonValue resultVal = docObj.value(QLatin1String("success"));
if (resultVal.isNull() || !resultVal.isBool())
return; // the message had no success field so we ignore
const bool loginSuccess = resultVal.toBool();
if (loginSuccess) {
// we logged in succesfully and we notify it via the loggedIn signal
emit loggedIn();
return;
}
// the login attempt failed, we extract the reason of the failure from the JSON
// and notify it via the loginError signal
const QJsonValue reasonVal = docObj.value(QLatin1String("reason"));
emit loginError(reasonVal.toString());
} else if (typeVal.toString().compare(QLatin1String("message"), Qt::CaseInsensitive) == 0) { //It's a chat message
// we extract the text field containing the chat text
const QJsonValue textVal = docObj.value(QLatin1String("text"));
// we extract the sender field containing the username of the sender
const QJsonValue senderVal = docObj.value(QLatin1String("sender"));
if (textVal.isNull() || !textVal.isString())
return; // the text field was invalid so we ignore
if (senderVal.isNull() || !senderVal.isString())
return; // the sender field was invalid so we ignore
// we notify a new message was received via the messageReceived signal
emit messageReceived(senderVal.toString(), textVal.toString());
} else if (typeVal.toString().compare(QLatin1String("newuser"), Qt::CaseInsensitive) == 0) { // A user joined the chat
// we extract the username of the new user
const QJsonValue usernameVal = docObj.value(QLatin1String("username"));
if (usernameVal.isNull() || !usernameVal.isString())
return; // the username was invalid so we ignore
// we notify of the new user via the userJoined signal
emit userJoined(usernameVal.toString());
} else if (typeVal.toString().compare(QLatin1String("userdisconnected"), Qt::CaseInsensitive) == 0) { // A user left the chat
// we extract the username of the new user
const QJsonValue usernameVal = docObj.value(QLatin1String("username"));
if (usernameVal.isNull() || !usernameVal.isString())
return; // the username was invalid so we ignore
// we notify of the user disconnection the userLeft signal
emit userLeft(usernameVal.toString());
}
}
The jsonReceived method is all about JSON parsing, since it's outside the scope of this example we won't spend any-more words on it as the code itself is well commented
void ChatClient::onReadyRead()
{
// prepare a container to hold the UTF-8 encoded JSON we receive from the socket
QByteArray jsonData;
// create a QDataStream operating on the socket
QDataStream socketStream(m_clientSocket);
// set the version so that programs compiled with different versions of Qt can agree on how to serialise
socketStream.setVersion(QDataStream::Qt_5_7);
// start an infinite loop
for (;;) {
// we start a transaction so we can revert to the previous state in case we try to read more data than is available on the socket
socketStream.startTransaction();
// we try to read the JSON data
socketStream >> jsonData;
if (socketStream.commitTransaction()) {
// we successfully read some data
// we now need to make sure it's in fact a valid JSON
QJsonParseError parseError;
// we try to create a json document with the data we received
const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error == QJsonParseError::NoError) {
// if the data was indeed valid JSON
if (jsonDoc.isObject()) // and is a JSON object
jsonReceived(jsonDoc.object()); // parse the JSON
}
// loop and try to read more JSONs if they are available
} else {
// the read failed, the socket goes automatically back to the state it was in before the transaction started
// we just exit the loop and wait for more data to become available
break;
}
}
}
The onReadyRead slot is really the most interesting one.