Организация сетевого взаимодействия на Qt

в 6:28, , рубрики: c++, qt, Qt Software, RPC, Анализ и проектирование систем, метки: ,

В данной статье я бы хотел рассказать об одном из вариантов организации сетевого взаимодействия в программах, написанных на Qt, используя библиотеку QexRemint. Эта библиотека позволяет сериализовать/десериализовать сигналы и слоты. Дописав к ней сетевую часть, можно получить отличный и удобный механизм для удаленного вызова процедур (Remote Procedure Call).
Организация сетевого взаимодействия на Qt

Основные классы

QexMethodTranslator – класс для вызова методов.
QexPropertyTranslator – класс для передачи свойств.
QexSignalSlotTranslator – класс для передачи сигналов.
QexMetaTranslator – агрегатор основных классов.
QexRemintReader – вспомогательный класс для ожидания прихода пакета целиком.

Передаваемые по сети данные

Во время вызова, на вызываемую сторону предается:
— идентификатор ответа;
— имя вызываемого объекта;
— сигнатура вызываемого метода;
— тип возвращаемого значения;
— список типов вызываемых аргументов;
— список значений вызываемых аргументов.
Аргументы сохраняются и восстанавливаются в QByteArray, используя QMetaType::save/ QMetaType::load.

При возвращении ответа передается:
— идентификатор ответа;
— имя объекта, который был вызван;
— имя метода, который был вызван;
— тип ответа;
— значение ответа.

Методы у объектов вызываются через QMetaObject::invokeMethod, поэтому они выполняются в потоке, в котором он обрабатывает слоты.

Пример

Для примера был реализован простенький чат. Что бы выделить важный код, были удалены почти все проверки и очистки. Этот код является очень урезанной версией того, что используется в боевом режиме. Как уже было сказано выше, сетевое взаимодействие не входит в саму библиотеку, поэтому, в начале, устанавливается связь.

Взаимодействие на стороне сервера

Сервер начинает слушать входящие соединения:

m_tcp_server.listen( QHostAddress::Any, 43567 );

При подключении клиента настраиваем его:

// получаем ожидающего клиента
QTcpSocket *client = m_tcp_server.nextPendingConnection( );
// мониторим отключение клиента
connect( client, SIGNAL( disconnected( ) ), SLOT( disconnect_client( ) ) );
// мониторим когда поступят данные от клиента
connect( client, SIGNAL( readyRead( ) ), SLOT( ready_read( ) ) );
// пара отвечает за обработку сигналов/слотов от одного клиента
typedef QPair< QexRemintReader*, QexMetaTranslator* > reader_translator;
// создаем классы, для обработки данных именного этого клиента
reader_translator rt = qMakePair( new QexRemintReader( client ), new QexMetaTranslator( client ) );
// подключаем обработчик данных, которые приходят из сети (у нас это принятые вызовы методов)
connect( rt.first, SIGNAL( dataFormed( const QByteArray& ) ), rt.second, SLOT( inputData( const QByteArray& ) ) );
// подключаем обработчик данных, который надо послать в сеть (у нас это сгенеренные сигналы)
connect( rt.second, SIGNAL( dataOutputted ( const QByteArray& ) ), SLOT( send_data( const QByteArray& ) ) );
// подключаем список методов, которые можно вызвать
rt.second->methodTranslator().connectMethod( "chat_server", this, SLOT( send_message( const QString&, const QString& ) ) );
// подключаем список сигналов, которые будут передаваться клиентам
rt.second->signalSlotTranslator().connectSignal( "chat_server", this, SIGNAL( message_received( const QString&, const QString& ) ) );

Когда мы делаем какой-то вызов или генерируем сигнал, нам необходимо отправить данные об этом клиенту. Обработчик всех подобных сигналов реализован один в одном объекте, поэтому необходимо найти какому клиенту предназначены данные. Целевого клиента ищем по отправителю сигнала, т.к. данные отправляет QexMetaTranslator, который отвечает за данного клиента:

QexMetaTranslator * mt = qobject_cast< QexMetaTranslator* >( sender() );
for ( QMap< QTcpSocket*, reader_translator >::iterator it = m_clients.begin(); it != m_clients.end(); ++it )
{
	if ( it.value().second == mt )
	{
		QDataStream stream( it.key() );
		stream << data;
	}
}

Необходимо обернуть отправляемые данные в QDataStream, т.к. при чтении данных, в начале вычитывается размер данных, который записывает QDataStream. При приеме нового сообщения нужно только разослать его всем остальным пользователям:

Q_EMIT message_received( nickname, text );

Взаимодействие на стороне клиента

При создание клиента необходимо настроить прием/передачу сигналов и слотов по аналогии с серверной частью:

connect( m_reader, SIGNAL( dataFormed( const QByteArray& ) ), m_translator, SLOT( inputData( const QByteArray& ) ) );
connect( m_translator, SIGNAL( dataOutputted( const QByteArray& ) ), SLOT( send_data( const QByteArray& ) ) );

Далее настраиваем принимаемые сигналы:

m_translator->signalSlotTranslator().connectSlot( "chat_server", SIGNAL( message_received( const QString&, const QString& ) ), this, SLOT( message_received( const QString&, const QString& ) ) );

При получении сигнала, просто добавляем полученный текст сообщения в окно с сообщениями. После создания объекта клиента, необходимо установить связь по сети. Далее, при получении данных из сокета, отправляем их на обработку в парсер:

m_reader->addData( m_socket->readAll() );

Отправка данных серверу осуществляется просто записью в сокет через QDataStream:

QDataStream stream( m_socket );
stream << data;

Отправка сообщение (вызов метода):

m_translator->methodTranslator().callMethod( "chat_server"
	, METHOD( send_message( QString, QString ) )
	, Q_ARG( QString, m_ui.nick->text() )
	, Q_ARG( QString, m_ui.text->toPlainText() )
	);

При отправке сообщения задается название вызываемого объекта, сигнатура вызываемой функции и список параметров. Если вызываемый метод что-то возвращает, то можно задать функцию, которая будет вызываться для получения результата.

Исходники чата

Автор: sploid

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js