IT Notes

LibQxt: Сигналы и слоты по сети

Введение

Сигналы и слоты в Qt являются весьма удачной реализацией паттерна Наблюдатель. Однако их взаимодействие ограничено тем процессом, в котором работает приложение. И этого не всегда оказывается достаточно. Конечно, существуют различные технологии IPC (в том числе и в Qt), но они, как правило, не столь удобны и просты в использовании. Поэтому нам на помощь приходит библиотека LibQxt. Она позволяет совместить ясность сигналов/слотов и гибкость сокетов, чтобы ваши приложения могли взаимодействовать в сетевой среде без чрезмерных усложнений.

Перед тем как начать

Проект LibQxt не входит в стандартный пакет поставки Qt. Его нужно скачивать отдельно (например, вот тут). Кроме того, на сегодняшний день его поддержка прекращена. Проект не развивается. Возможно, когда-нибудь ситуация изменится, но сейчас могут возникнуть проблемы со сборкой под Qt 5, поэтому с LibQxt рекомендуется использовать Qt 4.

Когда вы скачаете последнюю стабильную версию LibQxt (на момент написании заметки 0.6.2), то сразу подключить ее к вашему проекту не получится. Библиотека распространяется в виде исходного кода, поэтому сначала нужно провести сборку с тем компилятором и той версией Qt, которую вы используете для собственных разработок.

Собрать LibQxt совсем не сложно. Для Linux предусмотрен скрипт configure, а для Windows configure.bat.

Сборка LibQxt под Linux

Для Linux'а команда сборки выглядит следующим образом:

./configure && make

Замечу, что мне под Archlinux'ом для успешного выполнения configure пришлось поменять значение переменной QMAKE_BIN внутри скрипта с qmake на qmake-qt4, но вполне возможно, что вам это не потребуется.

Кроме того, вы можете выполнить установку LibQxt в системные папки с помощью команды make install с root-правами. Это даст некоторые удобства при подключении скомпонованных библиотек к вашим проектам, но преимущества не столь существенны. К тому же я предпочитаю хранить нестандартные внешние библиотеки в репозитории системы контроля версий вместе с исходниками. Но выбор в данном случае остается за вами.

Сборка LibQxt под Windows

Для Windows с использованием компилятора msvc запуск сборки выглядит так:

configure.bat && nmake

Удобнее всего вводить эти строки в Visual Studio Command Prompt. Это один из инструментов Visual Stuido Tools, который представляет собой обычную консоль cmd, но с дополнительными путями в PATH и необходимыми переменными окружения. Конечно, можно было бы указать все это в ручную для системы в целом, но если, например, вы используете разные версии Qt одновременно, то это приведет к появлению загадочных сообщений об ошибках при запуске Qt-приложений, в которых они расскажут все, что о вас думают.

Что получилось?

Предположим, что мы не стали запускать make install, а ограничились командой make. Если все прошло без ошибок, то в результате сборки у нас появился каталог lib/. В этом каталоге нас будут интересовать две библиотеки: libQxtCore и libQxtNetwork. Именно они и обеспечат работу сигналов и слотов по сети. Кроме того, обратите внимание на другой каталог, расположенный рядом с lib/, который называется include/. В нем для каждого модуля имеется свой подкаталог, но h-файлов там вы не найдете. Здесь проявляется самый ощутимый недостаток отказа от установки, поскольку придется скопировать их туда из соответствующих подкаталогов src/ вручную. Необходимо перенести src/core/*.h в include/QxtCore/, а src/network/*.h в include/QxtNetwork/. Все это нам скоро понадобится.

Пишем спецификацию

Пришло время придумать задачу, которую мы будем решать с помощью LibQxt. Сложность здесь заключается в том, что пример должен быть не слишком скучным, но и не слишком сложным. Поэтому создадим клиент-серверное приложение для совместного общения, то есть очень простой чат. Идея следующая:

  1. Есть приложение-сервер, которое представляет собой обычное консольное приложение;
  2. Есть приложение-клиент, работающее в графическом режиме. В нем должно быть поле ввода сообщений и область отображения всех сообщений, отправленных в чат участниками диалога;
  3. Само собой, к серверу может подключиться множество клиентов одновременно и общаться между собой.

Проект назовем QxtChat. Тогда серверная часть будет QxtChatServer, а клиентская - QxtChatClient. Готовая система будет выглядеть примерно так, как показано на рисунке ниже:

QxtChat

На рисунке изображены 3 запущенных экземпляра чата на фоне консоли, в которой работает серверная часть. Я собирал и запускал приложение под Archlinux'ом с графической оболочкой Openbox и отключенными декорациями окон, поэтому вид приложений может показаться несколько необычным, но это самые стандартные виджеты Qt.

По рисунку мы можем понять, что чат пока что не дает возможности представиться и связывает сообщения с уникальными числовыми идентификаторами клиентов, что на практике будет не особо удобно. Кроме того, сейчас адрес сервера жестко прошит в коде клиентов и определен значением localhost. Однако не так сложно будет вынести запрос этого значения в графический интерфейс или в конфигурационный файл. Поэтому при желании вы сами можете добавить эти возможности после того, как мы разберемся с текущей реализацией.

Реализация чата QxtChat

Сразу хочу предупредить, что я проводил сборку под Linux, но если вы работает под Windows, то разница будет минимальной. По сути все отличия сводятся к тому, что вместо суффикса linux будет использован win32. Однако я буду делать некоторые комментарии по поводу Windows-версии, где разница окажется более существенной.

Подготовка проекта

Я уже затрагивал вопрос о структуре Qt-проектов. Поэтому здесь пройдемся лишь кратко по ключевым вопросам. Дерево проекта у меня получилось таким:

.
├── bin
├── import
│   ├── QxtCore
│   └── QxtNetwork
├── lib.linux
│   └── LibQxt
└── src
    ├── include
    ├── QxtChatClient
    └── QxtChatServer

Поскольку проект простой, то каталогов не очень много. В import/ я скопировал include/QxtCore/ и include/QxtNetwork/, которые мы заранее подготовили. А в lib.linux/LibQxt/ поместил файлы библиотек из каталога сборки LibQxt.

В каталоге src для исходных кодов выделены подкаталоги для каждого подпроекта и добавлен вспомогательный каталог include/ для общих заголовочных файлов (у нас он будет всего один).

QxtChat.pro

В корневом каталоге проекта создадим главный pro-файл с шаблоном subdirs и добавим в него 2 наших подпроекта:

TEMPLATE = subdirs

SUBDIRS += src/QxtChatServer \
    src/QxtChatClient

common.pri

Добавим в корневой каталог проекта разделяемый pri-файл. Поскольку библиотеки мы в проект не включаем, то для краткости поместим в него все, что понадобится и в QxtChatClient и в QxtChatServer:

PROJECT_ROOT_PATH = $${PWD}/

win32: OS_SUFFIX = win32
linux-g++: OS_SUFFIX = linux

CONFIG(debug, debug|release) {
    BUILD_FLAG = debug
} else {
    BUILD_FLAG = release
}

LIBS_PATH = $${PROJECT_ROOT_PATH}/lib.$${OS_SUFFIX}/
IMPORT_PATH = $${PROJECT_ROOT_PATH}/import/
BIN_PATH = $${PROJECT_ROOT_PATH}/bin/$${BUILD_FLAG}/

BUILD_PATH = $${PROJECT_ROOT_PATH}/build.$${OS_SUFFIX}/$${BUILD_FLAG}/$${TARGET}/
RCC_DIR = $${BUILD_PATH}/rcc/
UI_DIR = $${BUILD_PATH}/ui/
MOC_DIR = $${BUILD_PATH}/moc/
OBJECTS_DIR = $${BUILD_PATH}/obj/

linux-g++: QMAKE_CXXFLAGS += -std=c++11

INCLUDEPATH += $${IMPORT_PATH}/QxtCore/
INCLUDEPATH += $${IMPORT_PATH}/QxtNetwork/
INCLUDEPATH += $${PROJECT_ROOT_PATH}/src/include/
LIBS += -L$${LIBS_PATH}/LibQxt/ -lQxtCore -lQxtNetwork

DESTDIR = $${BIN_PATH}

linux-g++: QMAKE_LFLAGS += -Wl,--rpath=\\\$\$ORIGIN/../../lib.$${OS_SUFFIX}/LibQxt/

Ничего особо нового здесь мы не видим. Однако обратите внимание на то, что мы прописали в INCLUDEPATH пути к заголовочным файлам LibQxt и к нашему внутреннему src/include/. Кроме того, в QMAKE_LFLAGS для Linux-версии добавлен путь поиска библиотек LibQxt. В Windows просто скопируйте dll-ки в bin/debug/ и bin/release/.

QxtChatServer

Начнем с реализации серверной части. Вот содержимое QxtChatServer.pro, который находится в src/QxtChatServer/:

QT       += core network

QT       -= gui

TARGET = QxtChatServer
CONFIG   += console
CONFIG   -= app_bundle

TEMPLATE = app

HEADERS += qxtchatserver.h \
    ../include/shareddefs.h

SOURCES += main.cpp \
    qxtchatserver.cpp

include( ../../common.pri )

Здесь самым важным является то, что мы подключаем модуль network в первой строке. Без него LibQxt работать не будет. Кроме того, в HEADERS прописывается наш разделяемый заголовочный файл shareddefs.h из src/include/. В остальном все без сюрпризов. Обычное консольное приложение на Qt.

Теперь рассмотрим заголовочный файл qxtchatserver.h:

#ifndef QXTCHATSERVER_H
#define QXTCHATSERVER_H

#include <QObject>
#include <QxtRPCPeer>

class QxtChatServer : public QObject {
    Q_OBJECT
public:
    explicit QxtChatServer( QObject* parent = 0 );

    bool start();

private slots:
    void onClientConnected( quint64 clientID );
    void onClientDisconnected( quint64 clientID );

    void onMessageReceived( quint64 clientID, const QString& message );

private:
    QxtRPCPeer m_peer;
};

#endif // QXTCHATSERVER_H

Для сервера мы определили соответствующий класс, который должен наследовать QObject, чтобы можно было пользоваться сигналами и слотами.

Функция-член start(), очевидно, запускает сервер и возвращает true, если это сделать удалось, иначе false.

Назначение слотов onClientConnected() и onClientDisconnected() тоже понятно из их названий. Один обрабатывает ситуацию, когда клиент устанавливает сетевое соединение, а второй срабатывает в случае, когда соединение с клиентом обрывается. В качестве параметра им передается уникальный числовой идентификатор клиента.

Еще один слот onMessageReceived() будет настроен на прием сигналов от клиентов по сети с помощью LibQxt. Он принимает два параметра: числовой идентификатор клиента и текстовое сообщение, которое отправил этот клиент.

И наконец экземпляр класса QxtRPCPeer в качестве закрытого поля m_peer. В нем и заключена та мощь LibQxt, которой мы вскоре воспользуемся. Заметим, что m_peer может работать как в режиме сервера, так и в режиме клиента.

Далее перейдем к файлу реализации qxtchatserver.cpp:

#include "qxtchatserver.h"

#include "shareddefs.h"

QxtChatServer::QxtChatServer( QObject* parent ) : QObject( parent ) {
    connect( &m_peer, SIGNAL( clientConnected( quint64 ) ), SLOT( onClientConnected( quint64 ) ) );
    connect( &m_peer, SIGNAL( clientDisconnected( quint64 ) ), SLOT( onClientDisconnected( quint64 ) ) );

    m_peer.attachSlot( SEND_MSG_TO_SERVER, this, SLOT( onMessageReceived( quint64, const QString& ) ) );
}

bool QxtChatServer::start() {
    static const QString BIND_IP = "0.0.0.0";
    return m_peer.listen( QHostAddress( BIND_IP ), PORT );
}

void QxtChatServer::onClientConnected( quint64 clientID ) {
    qDebug() << "Client connected:" << clientID;
    m_peer.call( clientID, SEND_MSG_TO_CLIENT, QString( "Hello, %1!" ).arg( clientID ) );
    m_peer.callExcept( clientID, SEND_MSG_TO_CLIENT, QString( "%1 connected" ).arg( clientID ) );
}

void QxtChatServer::onClientDisconnected( quint64 clientID ) {
    qDebug() << "Client disconnected:" << clientID;
    m_peer.call( SEND_MSG_TO_CLIENT, QString( "%1 disconnected" ).arg( clientID ) );
}

void QxtChatServer::onMessageReceived( quint64 clientID , const QString& message ) {
    qDebug() << clientID << ":" << message;
    m_peer.call( SEND_MSG_TO_CLIENT, QString( "%1> %2" ).arg( clientID ).arg( message ) );
}

Сначала мы соединяем сигналы от m_peer с нашими слотами для контроля за подключением и отключением клиентов:

connect( &m_peer, SIGNAL( clientConnected( quint64 ) ), SLOT( onClientConnected( quint64 ) ) );
connect( &m_peer, SIGNAL( clientDisconnected( quint64 ) ), SLOT( onClientDisconnected( quint64 ) ) );

Но это стандартное соединение сигналов-слотов в Qt. А вот следующее соединение уже интереснее:

m_peer.attachSlot( SEND_MSG_TO_SERVER, this, SLOT( onMessageReceived( quint64, const QString& ) ) );

Здесь мы прикрепляем наш слот onMessageReceived() к m_peer, связывая его с помощью символьной константы SEND_MSG_TO_SERVER. Сама эта константа определена в файле src/include/shareddefs.h, о котором мы поговорим немного позже. Выбор значения константы ничем не ограничен, но в данном случае удобно рассматривать ее как имя канала передачи данных от клиента к серверу. Таким образом, вместо традиционного указателя на Qt-объект и сигнала здесь мы используем некоторый именованный канал. У класса QxtRPCPeer еще есть функция-член attachSignal(), которая позволяет аналогичным образом связывать сигналы Qt-объектов с символьными константами. В нашем приложении мы ее не используем, но вы без труда можете задействовать такую возможность при необходимости.

Далее идет реализация функции start():

bool QxtChatServer::start() {
    static const QString BIND_IP = "0.0.0.0";
    return m_peer.listen( QHostAddress( BIND_IP ), PORT );
}

Для простоты мы не ограничиваем адрес прослушивания какой-то одной сетью (например, 127.0.0.1), а разрешаем прием всех соединений. В реальном приложении может потребоваться вынести этот параметр в файл конфигурации. Запустить Qxt-сервер очень легко. Достаточно вызывать listen() нашего объекта m_peer. В качестве параметра мы передаем ему подготовленный IP, обернутый в класс QHostAddress и константу со PORT. Эта константа определена в заголовочном файле shareddefs.h. Как понятно из названия, она определяет числовое значение порта, на котором будет ожидаться прием соединений от клиентов. Ее выбор достаточно произвольный, однако следует убедиться, что этот порт не занят и его уже не использует какое-нибудь приложение в системе.

Теперь разберемся с обработкой ситуации, когда к серверу подключается новый клиент:

void QxtChatServer::onClientConnected( quint64 clientID ) {
    qDebug() << "Client connected:" << clientID;
    m_peer.call( clientID, SEND_MSG_TO_CLIENT, QString( "Hello, %1!" ).arg( clientID ) );
    m_peer.callExcept( clientID, SEND_MSG_TO_CLIENT, QString( "%1 connected" ).arg( clientID ) );
}

Сначала мы просто выводим на консоль отладочную информацию с указанием идентификатора клиента. Далее с помощью функции-члена call() отправляем сообщение новому клиенту по именованному каналу SEND_MSG_TO_CLIENT (также определенному в shareddefs.h) с приветствием. А в последней строке уведомляем подключенных ранее клиентов о том, что к нам на сервер зашел новый пользователь. Это сообщение получат все клиенты, кроме вновь подключившегося.

Аналогичным образом мы обрабатываем и ситуацию разрыва соединения с клиентом:

void QxtChatServer::onClientDisconnected( quint64 clientID ) {
    qDebug() << "Client disconnected:" << clientID;
    m_peer.call( SEND_MSG_TO_CLIENT, QString( "%1 disconnected" ).arg( clientID ) );
}

Выводим отладочное сообщение и отправляем всем клиентам уведомление о том, что кто-то отключился.

Слот для обработки сообщений от пользователей не намного сложнее:

void QxtChatServer::onMessageReceived( quint64 clientID , const QString& message ) {
    qDebug() << clientID << ":" << message;
    m_peer.call( SEND_MSG_TO_CLIENT, QString( "%1> %2" ).arg( clientID ).arg( message ) );
}

В первой строке выводим полученное сообщение для отладки. А затем отправляем это сообщение всем подключенным клиентам. В том числе и тому клиенту, от которого пришло сообщение. Вообще, можно было провести небольшую оптимизацию и не отправлять сообщение тому клиенту, от которого оно пришло. Но для наших целей это не столь существенно и вы можете сами сделать описанное улучшение.

Осталось рассмотреть файл main.cpp для проекта-сервера:

#include <QCoreApplication>

#include "qxtchatserver.h"

int main( int argc, char* argv[] ) {
    QCoreApplication a( argc, argv );

    QxtChatServer server;
    if( !server.start() ) {
        qDebug() << "Server failed to start";
        return -1;
    }

    return a.exec();
}

Но и здесь мы не видим ничего необычного. Мы просто создаем экземпляр нашего сервера и запускаем его. Если запуск окончился неудачей, то особого смысла делать что-то еще нет, поэтому мы выводим уведомление об ошибке на консоль и завершаем работу приложения. Ну а если сервер стартовал, то обо всем остальном позаботится LibQxt.

Перед тем, как перейти к разработке клиентской части, посмотрим на константы, определенные в файле shareddefs.h:

#ifndef SHAREDDEFS_H
#define SHAREDDEFS_H

#include <QString>

static const QString SEND_MSG_TO_CLIENT = "SEND_MSG_TO_CLIENT";
static const QString SEND_MSG_TO_SERVER = "SEND_MSG_TO_SERVER";

static const int PORT = 9900;

#endif // SHAREDDEFS_H

Сейчас имена констант полностью совпадают с их текстовыми значениями, но при желании значения можно сократить в качестве незначительной оптимизации за счет уменьшения передаваемой информации. Значение порта взято совершенно случайным образом и единственное ограничение заключалось в его уникальности. Вы можете придумать свое значение, а еще лучше вынести его в конфигурационный файл.

QxtChatClient

Начнем с содержимого файла QxtChatClient.pro:

QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = QxtChatClient
TEMPLATE = app

HEADERS  += qxtchatwidget.h \
    ../include/shareddefs.h

SOURCES += main.cpp\
        qxtchatwidget.cpp

include( ../../common.pri )

Практически то же самое, что было для сервера. Но теперь у нас графическое приложение, а не консольное. И вновь обратите внимание на подключенный модуль network. Без него LibQxt работать не будет.

Далее посмотрим на заголовочный файл нашего виджета для чата:

#ifndef QXTCHATWIDGET_H
#define QXTCHATWIDGET_H

#include <QWidget>

#include <QxtRPCPeer>

class QTextEdit;
class QLineEdit;
class QPushButton;

class QxtChatWidget : public QWidget {
    Q_OBJECT

public:
    QxtChatWidget( QWidget* parent = 0 );
    ~QxtChatWidget();

protected:
    void showEvent( QShowEvent* e );

private slots:
    void onConnectedToServer();
    void onDisconnectedFromServer();
    void onServerError( const QAbstractSocket::SocketError& error );

    void appendMessage( const QString& message );

    void sendMessage();

    void onConnectionFailed();
    void refreshConnection();

private:
    void initUI();

private:
    QTextEdit* m_chatView;
    QLineEdit* m_messageInput;
    QPushButton* m_sendButton;

    QxtRPCPeer m_peer;

    bool m_connected;
};

#endif // QXTCHATWIDGET_H

Пройдемся по объявлениям в этом классе по порядку. Мы переопределили виртуальную функцию- член showEvent() класса QWidget лишь для того, чтобы активировать фокус поля ввода сообщения, когда окно отобразится на экране.

Затем идут три слота:

void onConnectedToServer();
void onDisconnectedFromServer();
void onServerError( const QAbstractSocket::SocketError& error );

Они предназначены для прямой связи с объектом m_peer для приема сигналов об успешном подключении и отключении от сервера, а также на случай сообщения об ошибке соединения. Код ошибки передается в параметре error и определяется в перечислении enum.

Слот appendMessage() будет использовать для присоединения сообщений к главной текстовой области чата. А слот sendMessage() является обработчиком нажатия кнопки Отправить.

Следующие два слота выполняют вспомогательную роль и предназначены для автоматического установления соединения с сервером:

void onConnectionFailed();
void refreshConnection();

Функция-член initUI() выделена для компоновки UI нашего чата. Можно было воспользоваться приложением Qt Designer, но интерфейс не настолько сложный, чтобы это было необходимо.

Среди полей класса определены элементы графического интерфейса пользователя, экземпляр класса QxtRPCPeer и логическая переменная для хранения текущего статуса соединения m_connected.

Переходим к реализации нашего виджета qxtchatwidget.cpp:

#include "qxtchatwidget.h"

#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QLayout>

#include <QTimer>

#include "shareddefs.h"

static const int CONNECTION_RETRY_TIME_OUT_MSEC = 2 * 1000; // 2 секунды
static const QString SERVER_HOST = "localhost";

QxtChatWidget::QxtChatWidget( QWidget* parent ) : QWidget( parent ), m_connected( false ) {
    initUI();

    connect( &m_peer, SIGNAL( connectedToServer() ), SLOT( onConnectedToServer() ) );
    connect( &m_peer, SIGNAL( disconnectedFromServer() ), SLOT( onDisconnectedFromServer() ) );
    connect(
                   &m_peer,
                   SIGNAL( serverError( const QAbstractSocket::SocketError& ) ),
                   SLOT( onServerError( const QAbstractSocket::SocketError& ) )
            );

    m_peer.attachSlot(
                   SEND_MSG_TO_CLIENT,
                   this,
                   SLOT( appendMessage( const QString& ) ),
                   Qt::QueuedConnection
            );

    connect( m_messageInput, SIGNAL( returnPressed() ), SLOT( sendMessage() ) );
    connect( m_sendButton, SIGNAL( clicked() ), SLOT( sendMessage() ) );

    refreshConnection();
}

QxtChatWidget::~QxtChatWidget() {
}

void QxtChatWidget::showEvent( QShowEvent* e ) {
    QWidget::showEvent( e );
    m_messageInput->setFocus();
}

void QxtChatWidget::onConnectedToServer() {
    appendMessage( "Connected to server" );
    m_connected = true;
}

void QxtChatWidget::onDisconnectedFromServer() {
    appendMessage( "Disconnected from server" );
    m_connected = false;
}

void QxtChatWidget::onServerError( const QAbstractSocket::SocketError& error ) {
    appendMessage( QString( "Server error: %1" ).arg( error ) );
    onConnectionFailed();
}

void QxtChatWidget::appendMessage( const QString& message ) {
    m_chatView->append( message );
    QTextCursor cursor = m_chatView->textCursor();
    cursor.movePosition( QTextCursor::End );
    m_chatView->setTextCursor( cursor );
}

void QxtChatWidget::sendMessage() {
    if( m_connected ) {
        if( m_messageInput->text().isEmpty() ) {
            return;
        }
        m_peer.call( SEND_MSG_TO_SERVER, m_messageInput->text() );
        m_messageInput->clear();
    } else {
        appendMessage( "You are not connected" );
    }
}

void QxtChatWidget::onConnectionFailed() {
    m_connected = false;
    QTimer::singleShot( CONNECTION_RETRY_TIME_OUT_MSEC, this, SLOT( refreshConnection() ) );
}

void QxtChatWidget::refreshConnection() {
    if( !m_connected ) {
        m_peer.connect( SERVER_HOST, PORT );
    }
}

void QxtChatWidget::initUI() {
    QVBoxLayout* mainLayout = new QVBoxLayout;
    m_chatView = new QTextEdit;
    m_chatView->setReadOnly( true );
    mainLayout->addWidget( m_chatView );

    QHBoxLayout* messagePanelLayout = new QHBoxLayout;
    m_messageInput = new QLineEdit;
    messagePanelLayout->addWidget( m_messageInput, 9 );
    m_sendButton = new QPushButton( trUtf8( "Отправить" ) );
    messagePanelLayout->addWidget( m_sendButton, 1 );

    mainLayout->addLayout( messagePanelLayout );
    setLayout( mainLayout );

    resize( 800, 600 );
}

В верхней части файла мы определили две константы:

static const int CONNECTION_RETRY_TIME_OUT_MSEC = 2 * 1000; // 2 секунды
static const QString SERVER_HOST = "localhost";

Первая определяет время в миллисекундах между попытками повторного подключения к серверу, если соединение оборвалось. А во второй мы зашили константу с адресом сервера. Конечно, для реального приложения этот параметр обязательно должен быть вынесен в конфигурационный файл с настройками.

В конструкторе класса мы вызываем функцию компоновки нашего UI. Ее реализация достаточно тривиальна, поэтому на нее мы останавливаться не будем. Далее идет соединение сигналов m_peer с нашими обработчиками подключения, отключения и ошибки сервера. Заметим, что основное отличие между сигналами disconnectedFromServer() и serverError(), которые возвращает QxtRPCPeer, заключается в том, что в первом случае мы сами закрываем соединение (с помощью disconnectServer()), а во втором либо происходит обрыв связи, либо сервер завершает свою работу (причину можно узнать в параметре error). В следующей строке определяется соединение:

m_peer.attachSlot( SEND_MSG_TO_CLIENT, this, SLOT( appendMessage( const QString& ) ), Qt::QueuedConnection );

Здесь практически то же самое, что было для сервера, кроме явного указания типа соединения Qt::QueuedConnection. Это сделано для того, чтобы обезопасить себя от пропуска Qxt-сообщений. Проблемы могут возникнуть, если в обработчике одного из слотов, пришедших от сервера, мы отправим обратно некий запрос и попытаемся его дождаться. Однако в нашем простом чате подобного происходить не будет, поэтому можно особо не беспокоиться.

Чтобы наш интерфейс реагировал на действия пользователя свяжем сигналы со слотами:

connect( m_messageInput, SIGNAL( returnPressed() ), SLOT( sendMessage() ) );
connect( m_sendButton, SIGNAL( clicked() ), SLOT( sendMessage() ) );

Отправка сообщений будет происходить при нажатии клавиши Enter в текстовом поле и в случае срабатывания кнопки Отправить.

В последней строке конструктора запускается слот refreshConnection(), чтобы выполнить попытку подключения к серверу.

В showEvent() происходит ровно то, что мы и планировали:

void QxtChatWidget::showEvent( QShowEvent* e ) {
    QWidget::showEvent( e );
    m_messageInput->setFocus();
}

Логика работы слотов onConnectedToServer(), onDisconnectedFromServer() и onServerError() довольно похожа на то, что происходило на сервере ранее. Отличие здесь лишь в том, что в случае успешного подключения мы переводим состояние m_connected в true, а в случае успешного отключения в false. Если соединение завершилось не по нашему желанию, то мы вызываем слот onConnectionFailed():

void QxtChatWidget::onConnectionFailed() {
    m_connected = false;
    QTimer::singleShot( CONNECTION_RETRY_TIME_OUT_MSEC, this, SLOT( refreshConnection() ) );
}

В нем мы сбрасываем значение флага m_connected и откладываем запуск слота refreshConnection() по таймеру. В самом слоте refreshConnection() все довольно просто:

void QxtChatWidget::refreshConnection() {
    if( !m_connected ) {
        m_peer.connect( SERVER_HOST, PORT );
    }
}

Если подключение еще не установлено, то вызывается метод connect() объекта m_peer по значениям SERVER_HOST, определенному выше в этом же файле, и PORT, который мы задали в shareddefs.h. Если подключение пройдет успешно, то сработает соответствующий сигнал и мы окажемся в слоте onConnectedToServer(), а если что-то пойдет не так, то мы вновь попадем в onServerError(), который приведет к еще одному запуску таймера и т.д.

Теперь посмотрим на слот отправки сообщения на сервер:

void QxtChatWidget::sendMessage() {
    if( m_connected ) {
        if( m_messageInput->text().isEmpty() ) {
            return;
        }
        m_peer.call( SEND_MSG_TO_SERVER, m_messageInput->text() );
        m_messageInput->clear();
    } else {
        appendMessage( "You are not connected" );
    }
}

Если соединение не установлено, то мы просто выводим соответствующее сообщение на экран. Кроме того, если текст для отправки пустой, то обрабатывать тоже нечего. Сама отправка сообщения достигается с помощью функции-члена call() совершенно так же, как это делалось на сервере.

Остался последний слот в QxtChatWidget:

void QxtChatWidget::appendMessage( const QString& message ) {
    m_chatView->append( message );
    QTextCursor cursor = m_chatView->textCursor();
    cursor.movePosition( QTextCursor::End );
    m_chatView->setTextCursor( cursor );
}

Он предназначен, как для вывода сообщений от сервера, так и прямого вызова на клиенте. В целом можно было бы сделать прямую привязку к слоту append() объекта m_chatView, но я решил, что для чата не помешает автопрокрутка вниз, которая по умолчанию не происходит.

Вот и все. Наш виджет чата готов. Осталось лишь вывести его на экран:

#include "qxtchatwidget.h"
#include <QApplication>

int main( int argc, char* argv[] ) {
    QApplication a( argc, argv );
    QxtChatWidget w;
    w.show();

    return a.exec();
}

Заключение

Вот мы и рассмотрели основы работы с LibQxt. Это очень полезная и мощная сетевая библиотека. Мы затронули лишь самые-самые базовые возможности, но уже создали полноценный чат, которым с минимальными доработками можно пользоваться. Возможно, в будущем появятся другие заметки по этой теме, где мы добавим еще пару функций, которые пригодятся в большинстве приложений.

Похожие публикации