Chat

Using the Qt GRPC client API in the user applications.

Chat explains how to authenticate chat users and send and receive short messages between chat clients. The application supports the following message formats:

  • Text - use message input field to send the message.
  • Image - copy the image buffer to the clipboard to send the message using the 'Ctrl + V' shortcut.

The chat client uses a simple RPC protocol described in the protobuf scheme:

package qtgrpc.examples.chat;

message ChatMessage {
    enum ContentType {
        Unknown = 0;
        Text = 1;
        Image = 2;
    };
    uint64 timestamp = 1;
    bytes content = 2;
    ContentType type = 3;
    string from = 4;
}

message ChatMessages {
    repeated ChatMessage messages = 1;
}

message User {
    string name = 1;
    string password = 2;
}

message Users {
    repeated User users = 1;
}

message None {}

service SimpleChat {
    rpc messageList(None) returns (stream ChatMessages) {}
    rpc sendMessage(ChatMessage) returns (None) {}
}

On the login screen, enter user credentials:

Note: The list of users is predefined on the server side and is constant. The password for all users is qwerty.

The chat client authenticates to the server using user-name and user-password HTTP headers. These fields are set within the metadata. Each gRPC message includes the user credentials in the message header. The metadata is passed to the QGrpcHttp2Channel inside QGrpcChannelOptions once and reused implicitily:

QGrpcChannelOptions channelOptions;
QHash<QByteArray, QByteArray> metadata = {
    { "user-name", { name.toUtf8() } },
    { "user-password", { password.toUtf8() } },
};
channelOptions.setMetadata(metadata);
std::shared_ptr<QAbstractGrpcChannel>
    channel = std::make_shared<QGrpcHttp2Channel>(url, channelOptions);

The chat client starts the communication with the server using a subscription to gRPC server streaming:

auto stream = m_client->messageList(qtgrpc::examples::chat::None());
auto streamPtr = stream.get();
auto finishedConnection = std::make_shared<QMetaObject::Connection>();
*finishedConnection = QObject::connect(streamPtr, &QGrpcServerStream::finished, this,
                                       [this, finishedConnection,
                                        stream = std::move(stream)](const QGrpcStatus &status) {
                                           if (!status.isOk()) {
                                               qCritical() << "Stream error(" << status.code()
                                                           << "):" << status.message();
                                           }
                                           if (status.code()
                                               == QtGrpc::StatusCode::Unauthenticated) {
                                               emit authFailed();
                                           } else if (status.code() != QtGrpc::StatusCode::Ok) {
                                               emit networkError(status.message());
                                               setState(Disconnected);
                                           } else {
                                               setState(Disconnected);
                                           }
                                           disconnect(*finishedConnection);
                                       });

QObject::connect(streamPtr, &QGrpcServerStream::messageReceived, this,
                 [this, name, password, stream = streamPtr]() {
                     if (m_userName != name) {
                         m_userName = name;
                         m_password = password;
                         emit userNameChanged();
                     }
                     setState(Connected);
                     if (const auto msg = stream->read<qtgrpc::examples::chat::ChatMessages>())
                         m_messages.append(msg->messages());
                 });

The QGrpcServerStream handler provides the signals that the client application should connect to.

Use the QGrpcServerStream::finished signal to handle both errors and the successful stream finishing. The status code indicates the error that happened during the stream communication.

When the server sends new messages to the stream, QGrpcServerStream emits the QGrpcServerStream::messageReceived signal. The slot connected to this signal processes the chat message. Messages that are received from the SimpleChat/messageList server stream are collected in the custom QAbstractListModel model and displayed to the user.

When the QGrpcServerStream::finished signal is emitted, there is nothing more you can do with this stream instance, so you need to initiate a new subscription.

After a successful subscription, the chat client switches to the conversation screen and allows you to see and send short messages:

To send the message, use a unary RPC call SimpleChat/sendMessage. The client application first sets fields of the ChatMessage protobuf message and then calls the client method:

qtgrpc::examples::chat::ChatMessage msg;
msg.setContent(content.toUtf8());
msg.setType(qtgrpc::examples::chat::ChatMessage::ContentType::Text);
msg.setTimestamp(QDateTime::currentMSecsSinceEpoch());
msg.setFrom(m_userName);

std::unique_ptr<QGrpcCallReply> reply = m_client->sendMessage(msg);
// We explicitly take a copy of the reply pointer, since moving it into
// the lambda would make the get() function invalid.
// Reply's lifetime will be extended until finished() is emitted.
// Qt::SingleShotConnection is needed to destroy the lambda (and its capture).
auto *replyPtr = reply.get();
connect(
    replyPtr, &QGrpcCallReply::finished, this,
    [reply = std::move(reply)](const QGrpcStatus &status) {
        if (!status.isOk())
            qDebug() << "Failed to send message: " << status;
    },
    Qt::SingleShotConnection);

Then, the gRPC server processes the client messages and broadcasts them to all the connected clients through the SimpleChat/messageList stream.

Note: This example uses the reference gRPC C++ API in the server implementation.

Example project @ code.qt.io

© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.