Backport the simple chat example

Backport the simple chat example. The application implements a simple
messenger using Qt GRPC. It shows how to utilize the server-side
streaming and use the call-based user/password credentials.

Task-number: QTBUG-109598
Pick-to: 6.5
Change-Id: I4ca3695780a9cc9991c92c4423e3af9d8e0eaf35
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Alexey Edelev 2023-01-20 17:43:29 +01:00
parent 2e3fdd5c05
commit fdd1d0339f
18 changed files with 1179 additions and 0 deletions

View File

@ -6,4 +6,5 @@ if(TARGET Qt6::Quick
AND TARGET Qt6::qtprotobufgen
AND TARGET Qt6::qtgrpcgen)
qt_internal_add_example(magic8ball)
qt_internal_add_example(chat)
endif()

View File

@ -0,0 +1,5 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
add_subdirectory(client)
add_subdirectory(server)

View File

@ -0,0 +1,61 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(GrpcChatClient LANGUAGES CXX)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/grpc/chat")
find_package(Qt6 REQUIRED COMPONENTS Core Protobuf Grpc Quick)
qt_standard_project_setup()
qt_add_executable(grpcchatclient
main.cpp
simplechatengine.cpp simplechatengine.h
chatmessagemodel.cpp chatmessagemodel.h
)
qt_add_qml_module(grpcchatclient
URI qtgrpc.examples.chat
VERSION 1.0
AUTO_RESOURCE_PREFIX
QML_FILES
Main.qml
ChatInputField.qml
ChatView.qml
)
qt_add_resources(grpcchatclient
"assets"
PREFIX "/"
FILES
"qt_logo_green_64x64px.png"
)
qt_add_protobuf(grpcchatclient
PROTO_FILES
../proto/simplechat.proto
)
qt_add_grpc(grpcchatclient CLIENT
PROTO_FILES
../proto/simplechat.proto
)
target_link_libraries(grpcchatclient
PRIVATE
Qt::Gui
Qt::Quick
Qt::Grpc
)
install(TARGETS grpcchatclient
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,23 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import QtQuick
import QtQuick.Controls
TextField {
id: _inputField
width: 200
color: "#cecfd5"
placeholderTextColor: "#9d9faa"
font.pointSize: 14
padding: 10
background: Rectangle {
radius: 5
border {
width: 1
color: _inputField.focus ? "#41cd52" : "#f3f3f4"
}
color: "#222840"
}
}

View File

@ -0,0 +1,153 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import QtQuick
import qtgrpc.examples.chat
Rectangle {
id: root
anchors.fill: parent
color: "#09102b"
onVisibleChanged: {
if (visible) {
_inputField.forceActiveFocus()
}
}
ListView {
id: messageListView
anchors.top: parent.top
anchors.bottom: _inputField.top
anchors.left: parent.left
anchors.right: parent.right
model: SimpleChatEngine.messages
clip: true
delegate: Item {
height: _imageWrapper.height + 10
width: root.width
Item {
id: _imageWrapper
height: _messageColumn.height + 20
width: parent.width/2 - 20
property bool ownMessage: SimpleChatEngine.userName === model.from
anchors{
right: _imageWrapper.ownMessage ? parent.right : undefined
left: _imageWrapper.ownMessage ? undefined : parent.left
rightMargin: _imageWrapper.ownMessage ? 10 : 0
leftMargin: _imageWrapper.ownMessage ? 0 : 10
verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.fill: parent
radius: 5
color: _imageWrapper.ownMessage ? "#9d9faa" : "#53586b"
border.color: "#41cd52"
border.width: 1
}
Column {
id: _messageColumn
anchors {
left: parent.left
right: parent.right
leftMargin: 10
rightMargin: 10
verticalCenter: parent.verticalCenter
}
Text {
id: _userName
property string from: _imageWrapper.ownMessage ? qsTr("You") : model.from
anchors.left: parent.left
anchors.right: parent.right
font.pointSize: 12
font.weight: Font.Bold
color: "#f3f3f4"
text: _userName.from + ": "
}
Loader {
id: delegateLoader
anchors.left: parent.left
anchors.right: parent.right
height: item ? item.height : 0
sourceComponent: model.type === SimpleChatEngine.Image ? imageDelegate : textDelegate
onItemChanged: {
if (item) {
item.content = model.content
}
}
}
}
}
}
onCountChanged: {
Qt.callLater( messageListView.positionViewAtEnd )
}
}
Component {
id: textDelegate
Item {
property alias content: innerText.text
height: childrenRect.height
Text {
id: innerText
anchors.left: parent.left
anchors.right: parent.right
height: implicitHeight
font.pointSize: 12
color: "#f3f3f4"
wrapMode: Text.Wrap
}
}
}
Component {
id: imageDelegate
Item {
clip: true
property alias content: innerImage.source
height: childrenRect.height
Image {
id: innerImage
width: implicitWidth
height: implicitHeight
}
}
}
ChatInputField {
id: _inputField
focus: true
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
margins: 20
}
placeholderText: qsTr("Start typing here or paste an image")
onAccepted: {
SimpleChatEngine.sendMessage(_inputField.text)
_inputField.text = ""
}
Keys.onPressed: (event)=> {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
console.log("Ctrl + V")
switch (SimpleChatEngine.clipBoardContentType) {
case SimpleChatEngine.Text:
paste()
break
case SimpleChatEngine.Image:
SimpleChatEngine.sendImageFromClipboard()
break
}
event.accepted = true
}
}
}
}

View File

@ -0,0 +1,151 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import QtQuick
import QtQuick.Controls
import qtgrpc.examples.chat
ApplicationWindow {
id: mainWindow
visible: true
width: 640
height: 480
minimumWidth: 640
minimumHeight: 480
title: qsTr("Simple Chat")
Rectangle {
id: background
anchors.fill: parent
color: "#09102b"
}
Row {
spacing: 10
anchors {
top: parent.top
topMargin: 10
left: parent.left
leftMargin: 20
}
Image {
source: "qrc:/qt_logo_green_64x64px.png"
width: implicitWidth
height: implicitHeight
}
Text {
anchors.verticalCenter: parent.verticalCenter
color: "#f3f3f4"
font.pointSize: 20
text: qsTr("Chat")
}
}
Column {
id: loginControl
spacing: 5
visible: SimpleChatEngine.state !== SimpleChatEngine.Connected
anchors.centerIn: parent
enabled: SimpleChatEngine.state === SimpleChatEngine.Disconnected
ChatInputField {
id: loginField
width: 200
placeholderText: qsTr("Login")
onAccepted: {
SimpleChatEngine.login(loginField.text, passwordField.text)
}
onVisibleChanged: {
if (visible) {
loginField.forceActiveFocus()
}
}
Component.onCompleted: {
if (visible) {
loginField.forceActiveFocus()
}
}
}
ChatInputField {
id: passwordField
echoMode: TextInput.Password
placeholderText: qsTr("Password")
onAccepted: {
SimpleChatEngine.login(loginField.text, passwordField.text)
}
}
Button {
id: loginButton
anchors.horizontalCenter: parent.horizontalCenter
width: enterText.implicitWidth + 20
height: 40
background: Rectangle {
radius: 5
border {
width: 1
color: loginButton.pressed ? "#41cd52" : "#f3f3f4"
}
color:"#53586b"
Text {
id: enterText
text : qsTr("Enter")
color: "#f3f3f4"
anchors.centerIn: parent
font.pointSize: 14
}
}
onClicked: {
SimpleChatEngine.login(loginField.text, passwordField.text)
}
}
}
ChatView {
id: chatView
visible: SimpleChatEngine.state === SimpleChatEngine.Connected
}
Text {
id: connectingText
visible: SimpleChatEngine.state === SimpleChatEngine.Connecting
anchors.top: loginControl.bottom
anchors.topMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Connecting...")
font.pointSize: 14
color: "#f3f3f4"
}
Text {
id: authFailedText
visible: false
anchors.top: loginControl.bottom
anchors.topMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("This username with this password doesn't exists.")
font.pointSize: 14
color: "#f3f3f4"
onVisibleChanged: {
if (authFailedText.visible) {
fadeOutTimer.restart()
} else {
fadeOutTimer.stop()
}
}
Timer {
id: fadeOutTimer
onTriggered: {
authFailedText.visible = false
}
}
}
Connections {
target: SimpleChatEngine
function onAuthFailed() {
authFailedText.visible = true;
}
}
}

View File

@ -0,0 +1,90 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QImage>
#include <QBuffer>
#include "chatmessagemodel.h"
#include "simplechat.qpb.h"
namespace {
enum Role {
Content = Qt::UserRole + 1,
Type,
From,
};
QString getImage(const QByteArray &data)
{
return QString("data:image/png;base64,") + data.toBase64();
}
QString getImageScaled(const QByteArray &data)
{
QImage img = QImage::fromData(data, "PNG");
img = img.scaled(300, 300, Qt::KeepAspectRatio);
QByteArray scaledData;
QBuffer buffer(&scaledData);
img.save(&buffer, "PNG");
return getImage(scaledData);
}
QString getText(const QByteArray &data)
{
return QString::fromUtf8(data);
}
} // namespace
ChatMessageModel::ChatMessageModel(QObject *parent) : QAbstractListModel(parent) { }
QHash<int, QByteArray> ChatMessageModel::roleNames() const
{
static QHash<int, QByteArray> s_roleNames;
if (s_roleNames.isEmpty()) {
s_roleNames.insert(Content, "content");
s_roleNames.insert(Type, "type");
s_roleNames.insert(From, "from");
}
return s_roleNames;
}
QVariant ChatMessageModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if (row < 0 || row >= m_container.count() || !m_container.at(row))
return QVariant();
auto dataPtr = m_container.at(row).get();
switch (role) {
case Content:
if (dataPtr->type() == qtgrpc::examples::chat::ChatMessage::Image)
return QVariant::fromValue(getImageScaled(dataPtr->content()));
else
return QVariant::fromValue(getText(dataPtr->content()));
case Type:
return QVariant::fromValue(dataPtr->type());
case From:
return QVariant::fromValue(dataPtr->from());
}
return QVariant();
}
int ChatMessageModel::rowCount(const QModelIndex &) const
{
return m_container.count();
}
void ChatMessageModel::append(
const QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> &messages)
{
if (messages.size() > 0) {
beginInsertRows(QModelIndex(), m_container.size(),
m_container.size() + messages.size() - 1);
m_container.append(messages);
endInsertRows();
}
}

View File

@ -0,0 +1,32 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef CHATMESSAGEMODEL_H
#define CHATMESSAGEMODEL_H
#include <QAbstractListModel>
#include <memory>
namespace qtgrpc::examples::chat {
class ChatMessage;
}
class ChatMessageModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit ChatMessageModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent) const override;
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
void append(const QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> &messages);
private:
QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> m_container;
};
#endif // CHATMESSAGEMODEL_H

View File

@ -0,0 +1,26 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QFontDatabase>
#include <QIcon>
#include <QFont>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QGuiApplication::setWindowIcon(QIcon(":/qt_logo_green_64x64px.png"));
QQmlApplicationEngine engine;
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, [](){
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.loadFromModule("qtgrpc.examples.chat", "Main");
return app.exec();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,172 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "simplechatengine.h"
#include <QGrpcHttp2Channel>
#include <QGrpcUserPasswordCredentials>
#include <QGrpcInsecureChannelCredentials>
#include <QDebug>
#include <QFile>
#include <QCryptographicHash>
#include <QDateTime>
#include <QClipboard>
#include <QGuiApplication>
#include <QMimeData>
#include <QImage>
#include <QByteArray>
#include <QBuffer>
SimpleChatEngine::SimpleChatEngine(QObject *parent)
: QObject(parent),
m_state(Disconnected),
m_client(new qtgrpc::examples::chat::SimpleChat::Client),
m_clipBoard(QGuiApplication::clipboard())
{
qRegisterProtobufTypes();
if (m_clipBoard)
QObject::connect(m_clipBoard, &QClipboard::dataChanged, this,
&SimpleChatEngine::clipBoardContentTypeChanged);
}
SimpleChatEngine::~SimpleChatEngine()
{
delete m_client;
}
void SimpleChatEngine::login(const QString &name, const QString &password)
{
if (m_state != Disconnected)
return;
setState(Connecting);
QUrl url("http://localhost:65002");
// ![0]
std::shared_ptr<QAbstractGrpcChannel> channel(
new QGrpcHttp2Channel(url,
QGrpcUserPasswordCredentials(name, password.toUtf8())
| QGrpcInsecureChannelCredentials()));
// ![0]
m_client->attachChannel(channel);
// ![1]
auto stream = m_client->streamMessageList(qtgrpc::examples::chat::None());
QObject::connect(stream.get(), &QGrpcStream::errorOccurred, this,
[this, stream](const QGrpcStatus &status) {
qCritical()
<< "Stream error(" << status.code() << "):" << status.message();
if (status.code() == QGrpcStatus::Unauthenticated)
emit authFailed();
});
QObject::connect(stream.get(), &QGrpcStream::finished, this,
[this, stream]() { setState(Disconnected); });
QObject::connect(stream.get(), &QGrpcStream::messageReceived, this, [this, name, stream]() {
if (m_userName != name) {
m_userName = name;
emit userNameChanged();
}
setState(Connected);
m_messages.append(stream->read<qtgrpc::examples::chat::ChatMessages>().messages());
});
// ![1]
}
void SimpleChatEngine::sendMessage(const QString &content)
{
// ![2]
qtgrpc::examples::chat::ChatMessage msg;
msg.setContent(content.toUtf8());
msg.setType(qtgrpc::examples::chat::ChatMessage::Text);
msg.setTimestamp(QDateTime::currentMSecsSinceEpoch());
msg.setFrom(m_userName);
m_client->sendMessage(msg);
// ![2]
}
SimpleChatEngine::ContentType SimpleChatEngine::clipBoardContentType() const
{
if (m_clipBoard != nullptr) {
const QMimeData *mime = m_clipBoard->mimeData();
if (mime != nullptr) {
if (mime->hasImage() || mime->hasUrls())
return SimpleChatEngine::ContentType::Image;
else if (mime->hasText())
return SimpleChatEngine::ContentType::Text;
}
}
return SimpleChatEngine::ContentType::Unknown;
}
void SimpleChatEngine::sendImageFromClipboard()
{
if (m_clipBoard == nullptr)
return;
QByteArray imgData;
const QMimeData *mime = m_clipBoard->mimeData();
if (mime != nullptr) {
if (mime->hasImage()) {
QImage img = mime->imageData().value<QImage>();
img = img.scaled(300, 300, Qt::KeepAspectRatio);
QBuffer buffer(&imgData);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG");
buffer.close();
} else if (mime->hasUrls()) {
QUrl imgUrl = mime->urls().at(0);
if (!imgUrl.isLocalFile()) {
qWarning() << "Only supports transfer of local images";
return;
}
QImage img(imgUrl.toLocalFile());
if (img.isNull()) {
qWarning() << "Invalid image format";
return;
}
QBuffer buffer(&imgData);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG");
buffer.close();
}
}
if (imgData.isEmpty())
return;
qtgrpc::examples::chat::ChatMessage msg;
msg.setContent(imgData);
msg.setType(qtgrpc::examples::chat::ChatMessage::Image);
msg.setTimestamp(QDateTime::currentMSecsSinceEpoch());
msg.setFrom(m_userName);
m_client->sendMessage(msg);
}
ChatMessageModel *SimpleChatEngine::messages()
{
return &m_messages;
}
QString SimpleChatEngine::userName() const
{
return m_userName;
}
SimpleChatEngine::State SimpleChatEngine::state() const
{
return m_state;
}
void SimpleChatEngine::setState(SimpleChatEngine::State state)
{
if (m_state != state) {
m_state = state;
emit stateChanged();
}
}

View File

@ -0,0 +1,75 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef SIMPLECHATENGINE_H
#define SIMPLECHATENGINE_H
#include <QObject>
#include <QQmlEngine>
#include "simplechat.qpb.h"
#include "simplechat_client.grpc.qpb.h"
#include "chatmessagemodel.h"
QT_BEGIN_NAMESPACE
class QClipboard;
QT_END_NAMESPACE
class SimpleChatEngine : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QString userName READ userName NOTIFY userNameChanged)
Q_PROPERTY(ChatMessageModel *messages READ messages CONSTANT)
Q_PROPERTY(SimpleChatEngine::ContentType clipBoardContentType READ clipBoardContentType NOTIFY
clipBoardContentTypeChanged)
Q_PROPERTY(SimpleChatEngine::State state READ state NOTIFY stateChanged)
public:
enum ContentType {
Unknown = qtgrpc::examples::chat::ChatMessage::ContentType::Unknown,
Text = qtgrpc::examples::chat::ChatMessage::ContentType::Text,
Image = qtgrpc::examples::chat::ChatMessage::ContentType::Image,
};
Q_ENUM(ContentType)
enum State {
Disconnected = 0,
Connecting = 1,
Connected = 2,
};
Q_ENUM(State)
explicit SimpleChatEngine(QObject *parent = nullptr);
~SimpleChatEngine() override;
Q_INVOKABLE void login(const QString &name, const QString &password);
Q_INVOKABLE void sendMessage(const QString &message);
Q_INVOKABLE void sendImageFromClipboard();
QString userName() const;
ChatMessageModel *messages();
SimpleChatEngine::ContentType clipBoardContentType() const;
SimpleChatEngine::State state() const;
Q_SIGNALS:
void authFailed();
void clipBoardContentTypeChanged();
void userNameChanged();
void stateChanged();
private:
void setState(SimpleChatEngine::State state);
SimpleChatEngine::State m_state;
ChatMessageModel m_messages;
qtgrpc::examples::chat::SimpleChat::Client *m_client;
QClipboard *m_clipBoard;
QString m_userName;
};
Q_DECLARE_METATYPE(ChatMessageModel *)
#endif // SIMPLECHATENGINE_H

View File

@ -0,0 +1,41 @@
syntax = "proto3";
//! [0]
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) { }
}
//! [0]

View File

@ -0,0 +1,58 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(GrpcChatServer LANGUAGES CXX)
# Qt6::Grpc module is not used directly in this project. But this allows to find Qt6::Grpc's
# dependencies without setting extra cmake module paths.
find_package(Qt6 COMPONENTS Grpc)
find_package(WrapgRPCPlugin)
find_package(WrapgRPC)
if(NOT TARGET WrapgRPC::WrapgRPCPlugin OR NOT TARGET WrapProtoc::WrapProtoc
OR NOT TARGET WrapgRPC::WrapLibgRPC)
message(WARNING "Dependencies of QtGrpc test server not found. Skipping.")
return()
endif()
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/simplechat.proto")
set(out_dir "${CMAKE_CURRENT_BINARY_DIR}")
set(generated_files
"${out_dir}/simplechat.pb.h" "${out_dir}/simplechat.pb.cc"
"${out_dir}/simplechat.grpc.pb.h" "${out_dir}/simplechat.grpc.pb.cc")
add_custom_command(
OUTPUT ${generated_files}
COMMAND
$<TARGET_FILE:WrapProtoc::WrapProtoc>
ARGS
--grpc_out "${out_dir}"
--cpp_out "${out_dir}"
"-I${CMAKE_CURRENT_LIST_DIR}/../proto/"
--plugin=protoc-gen-grpc=$<TARGET_FILE:WrapgRPC::WrapgRPCPlugin>
"${proto_files}"
WORKING_DIRECTORY ${out_dir}
DEPENDS "${proto_files}"
COMMENT "Generating gRPC ${target} sources..."
COMMAND_EXPAND_LISTS
VERBATIM
)
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
add_executable(grpcchatserver
${generated_files}
main.cpp
)
target_include_directories(grpcchatserver PRIVATE ${out_dir})
target_link_libraries(grpcchatserver PRIVATE WrapgRPC::WrapLibgRPC)
install(TARGETS grpcchatserver
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,213 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <iostream>
#include <string_view>
#include <list>
#include <cassert>
#include <optional>
#include <grpc++/grpc++.h>
#include <simplechat.pb.h>
#include <simplechat.grpc.pb.h>
using namespace qtgrpc::examples::chat;
class MessageListHandler;
class UserListHandler;
constexpr std::string_view nameHeader("user-name");
constexpr std::string_view passwordHeader("user-password");
class SimpleChatService final : public SimpleChat::WithAsyncMethod_messageList<SimpleChat::Service>
{
public:
SimpleChatService();
void run(std::string_view address);
grpc::Status sendMessage(grpc::ServerContext *context, const ChatMessage *request,
None *) override;
private:
std::optional<std::string> checkUserCredentials(grpc::ServerContext *context);
void addMessageHandler(MessageListHandler *userHandler);
void sendMessageToClients(const ChatMessage *message);
Users m_usersDatabase;
ChatMessages m_messages;
std::list<MessageListHandler *> m_activeClients;
};
struct HandlerTag
{
enum Type { Request = 1, Reply, Disconnect, Reject };
HandlerTag(HandlerTag::Type t, MessageListHandler *h) : tag(t), handler(h) { }
HandlerTag::Type tag;
MessageListHandler *handler;
};
class MessageListHandler
{
public:
MessageListHandler(SimpleChatService *service, grpc::ServerCompletionQueue *queue)
: writer(&ctx), cq(queue)
{
ctx.AsyncNotifyWhenDone(new HandlerTag(HandlerTag::Disconnect, this));
service->RequestmessageList(&ctx, &request, &writer, cq, cq,
new HandlerTag(HandlerTag::Request, this));
}
None request;
grpc::ServerAsyncWriter<qtgrpc::examples::chat::ChatMessages> writer;
grpc::ServerContext ctx;
grpc::ServerCompletionQueue *cq;
};
void SimpleChatService::run(std::string_view address)
{
grpc::ServerBuilder builder;
builder.AddListeningPort(std::string(address), grpc::InsecureServerCredentials());
builder.RegisterService(this);
std::unique_ptr<grpc::ServerCompletionQueue> cq = builder.AddCompletionQueue();
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << address << '\n';
MessageListHandler *pending = new MessageListHandler(this, cq.get());
while (true) {
HandlerTag *tag = nullptr;
bool ok = false;
cq->Next(reinterpret_cast<void **>(&tag), &ok);
if (tag == nullptr)
continue;
if (!ok) {
std::cout << "Unable to proccess tag from the completion queue\n";
delete tag;
continue;
}
switch (tag->tag) {
case HandlerTag::Request: {
std::cout << "New connection request received\n";
std::string name;
if (!checkUserCredentials(&(pending->ctx))) {
std::cout << "Authentication failed\n";
pending->writer.Finish(grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
"User or login are invalid"),
new HandlerTag(HandlerTag::Reject, pending));
} else {
std::cout << "User " << name << " connected to chat\n";
addMessageHandler(pending);
pending->writer.Write(m_messages, nullptr);
}
pending = new MessageListHandler(this, cq.get());
} break;
case HandlerTag::Disconnect: {
auto it = std::find(m_activeClients.begin(), m_activeClients.end(), tag->handler);
if (it != m_activeClients.end()) {
std::cout << "Client disconnected\n";
m_activeClients.erase(it);
delete tag->handler;
}
} break;
case HandlerTag::Reject:
std::cout << "Connection rejected\n";
m_activeClients.remove(tag->handler);
delete tag->handler;
break;
case HandlerTag::Reply:
std::cout << "Sending data to users\n";
break;
}
delete tag;
}
}
std::optional<std::string> SimpleChatService::checkUserCredentials(grpc::ServerContext *context)
{
assert(context != nullptr);
std::string name;
std::string password;
for (const auto &[key, value] : std::as_const(context->client_metadata())) {
if (std::string(key.data(), key.size()) == nameHeader) {
name = std::string(value.data(), value.size());
}
if (std::string(key.data(), key.size()) == passwordHeader) {
password = std::string(value.data(), value.size());
}
}
return std::find_if(m_usersDatabase.users().begin(), m_usersDatabase.users().end(),
[&name, &password](const auto &it) {
return it.name() == name && it.password() == password;
})
!= m_usersDatabase.users().end()
? std::optional{ name }
: std::nullopt;
}
SimpleChatService::SimpleChatService()
{
// All passwords are 'qwerty' by default
User *newUser = m_usersDatabase.add_users();
newUser->set_name("user1");
newUser->set_password("qwerty");
newUser = m_usersDatabase.add_users();
newUser->set_name("user2");
newUser->set_password("qwerty");
newUser = m_usersDatabase.add_users();
newUser->set_name("user3");
newUser->set_password("qwerty");
newUser = m_usersDatabase.add_users();
newUser->set_name("user4");
newUser->set_password("qwerty");
newUser = m_usersDatabase.add_users();
newUser->set_name("user5");
newUser->set_password("qwerty");
}
void SimpleChatService::sendMessageToClients(const ChatMessage *message)
{
assert(message != nullptr);
// Send only new messages after users received all the messages for this session after stream
// call.
ChatMessages messages;
ChatMessage *msg = messages.add_messages();
*msg = *message;
for (auto client : m_activeClients)
client->writer.Write(messages, new HandlerTag(HandlerTag::Reply, client));
}
void SimpleChatService::addMessageHandler(MessageListHandler *handler)
{
assert(handler != nullptr);
m_activeClients.push_back(handler);
}
grpc::Status SimpleChatService::sendMessage(grpc::ServerContext *context,
const ChatMessage *request, None *)
{
assert(context != nullptr);
assert(request != nullptr);
auto name = checkUserCredentials(context);
if (!name)
return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Login or password are invalid");
auto msg = m_messages.add_messages();
*msg = *request;
msg->set_from(*name);
sendMessageToClients(msg);
return grpc::Status();
}
int main(int, char *[])
{
SimpleChatService srv;
srv.run("localhost:65002");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,78 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example chat
\ingroup qtgrpc-examples
\meta tag {network,protobuf,grpc,serialization,tcp}
\meta category {Networking}
\title Chat
\brief Using the Qt GRPC client API in the user applications.
\e Chat explains how to authenticate chat users and send and receive short
messages between chat clients. The application supports the following
message formats:
\list
\li Text - use message input field to send the message.
\li Image - copy the image buffer to the clipboard to send the message
using the \c{'Ctrl + V'} shortcut.
\endlist
The chat client uses a simple RPC protocol described in the protobuf scheme:
\snippet chat/proto/simplechat.proto 0
On the login screen, enter user credentials:
\image chatlogin.webp
\note The list of users is predefined on the server side and is constant.
The password for all users is \c qwerty.
The chat client uses an authentication type called \c {call credentials}.
Each gRPC message includes the user credentials in the message header.
Credentials are passed to the \l QGrpcHttp2Channel once and reused
implicitily:
\snippet chat/client/simplechatengine.cpp 0
The chat client starts the communication with the server using a
subscription to gRPC server streaming:
\snippet chat/client/simplechatengine.cpp 1
The \l QGrpcStream handler provides the signals that the client application
should connect to.
The \l QGrpcStream::errorOccurred signal indicates the error that occurred
either on the server side or in the communication channel. Typically, an
error results in the connection to the server being closed and
the \l QGrpcStream::finished signal being emitted.
When the server sends new messages to the stream, \l QGrpcStream emits the
\l QGrpcStream::messageReceived signal. The slot connected to this signal
processes the chat message. Messages that are received from the
\c {SimpleChat/messageList} server stream are collected in the custom
\l QAbstractListModel model and displayed to the user.
When the \l QGrpcStream::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:
\image chatconversation.webp
To send the message, use a unary RPC call \c {SimpleChat/sendMessage}.
The client application first sets fields of the \c ChatMessage protobuf
message and then calls the client method:
\snippet chat/client/simplechatengine.cpp 2
Then, the gRPC server processes the client messages and broadcasts them to
all the connected clients through the \c {SimpleChat/messageList} stream.
\note This example uses the reference gRPC C++ API in the server
implementation.
*/