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