QLocalSocket+QEventLoopを組み合わせた時のreadyReadシグナルの仕様
QLocalSocketとQEventLoopを組み合わせると、非同期なネットワーク呼び出しを同期的に記述できます。(もちろんUIスレッドはブロックしません)。
詳細はここの”Forcing event dispatching”という箇所を参考にして下さい。
http://qt-project.org/wiki/Threads_Events_QObjects
基本的な流れとしてはQLocalSocketにデータを何かwriteした後ローカルのQEventLoopを呼び出し(exec()というメソッド)、readyReadシグナルが届いたらイベントループを終了(quit()メソッド)させるという流れです。
しかしここで大きな落とし穴があり、readyReadシグナルが呼ばれた中で再びイベントループを呼び出すと、readyReadシグナルはそのイベントループでは呼び出されません。Qtのドキュメントにもそのように書かれています。(僕はこの仕様でかなりはまりました^^;)
http://doc.qt.io/qt-5/qiodevice.html#readyRead
readyRead() is not emitted recursively; if you reenter the event loop or call waitForReadyRead() inside a slot connected to the readyRead() signal, the signal will not be reemitted (although waitForReadyRead() may still return true).
実際のソースコードの該当箇所を見てみると以下のようになっています。
qabstractsocket.cpp
// only emit readyRead() when not recursing, and only if there is data available
bool hasData = newBytes > 0
#ifndef QT_NO_UDPSOCKET
|| (!isBuffered && socketType != QAbstractSocket::TcpSocket && socketEngine && socketEngine->hasPendingDatagrams())
#endif
|| (!isBuffered && socketType == QAbstractSocket::TcpSocket && socketEngine)
;
if (!emittedReadyRead && hasData) {
QScopedValueRollback r(emittedReadyRead);
emittedReadyRead = true;
emit q->readyRead();
}
readyReadシグナルにDirectConnectionを使ってconnectされたスロットが呼び出され(この時emittedReadyRead == trueとなる)、その中でイベントループが作られ再度上記の箇所に到達した場合、emittedReadyReadはすでにtrueなので再度emitされないという仕組みです。イベントループが終了するとQScopedValueRollbackによりemittedReadyReadの値がfalseに戻され、再びemitされるようになります。
この仕様を実験するため以下のコードを書きました。
QLocalServer、QLocalSocketを使ってサーバ、クライアント間でUnix Domain Socketを介してデータを相互に送受信しています。
動作確認した環境はMac (Mavericks) , Qt 5.4です。
readyReadシグナルにconnectされたonReadyReadスロットの中でQEvnetLoopを作成、実行しています。
サーバ側のコード
Server.h
#pragma once
#include
#include
#include
#include
#include
class Result : public QObject {
Q_OBJECT
int m_result;
public:
int result() { return m_result; }
void setResult(int result) {
m_result = result;
emit ready();
}
signals:
void ready();
};
class Server : public QObject {
Q_OBJECT
Result m_result1;
Result m_result3;
QLocalSocket* m_sock;
public:
void start() {
QString sockPath = "/tmp/qeventloop_test";
QLocalServer* server = new QLocalServer();
QFile sockFile(sockPath);
if (sockFile.exists()) {
sockFile.remove();
}
server->listen(sockPath);
connect(server, &QLocalServer::newConnection, [this, server]() {
m_sock = server->nextPendingConnection();
connect(m_sock, &QLocalSocket::disconnected, m_sock, &QLocalSocket::deleteLater);
QObject::connect(
m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead, Qt::QueuedConnection);
sendData(m_sock, 1);
QEventLoop loop;
connect(&m_result1, &Result::ready, &loop, &QEventLoop::quit);
qDebug("start event loop to wait for 1");
loop.exec();
qDebug("end event loop to wait for 1");
});
}
void onReadyRead() {
qDebug("bytesAvailable: %lld", m_sock->bytesAvailable());
qint64 bytesAvailable = m_sock->bytesAvailable();
QByteArray buffer = m_sock->readAll();
QDataStream ds(buffer);
while (bytesAvailable > 0) {
int num;
ds >> num;
qDebug("received %d", num);
bytesAvailable -= 4;
if (num == 2) {
sendData(m_sock, 3);
QEventLoop loop;
QObject::connect(&m_result3, &Result::ready, &loop, &QEventLoop::quit);
qDebug("start event loop to wait for 3");
loop.exec();
qDebug("end event loop to wait for 3");
} else if (num == -1) {
m_result1.setResult(num);
} else if (num == -3) {
m_result3.setResult(num);
}
}
}
void sendData(QLocalSocket* sock, int num) {
qDebug("send %d", num);
QByteArray block;
QDataStream ds(&block, QIODevice::WriteOnly);
ds << num;
sock->write(block);
}
};
main.cpp
#include
#include
#include
#include "Server.h"
int main(int argv, char** args) {
QApplication app(argv, args);
Server server;
server.start();
return app.exec();
}
クライアント側のコード
main.cpp
#include
#include
#include
void sendData(QLocalSocket& sock, int num) {
qDebug("send %d", num);
QByteArray block;
QDataStream ds(&block, QIODevice::WriteOnly);
ds << num;
sock.write(block);
}
int main(int argv, char** args) {
QApplication app(argv, args);
QLocalSocket sock;
QObject::connect(&sock, &QLocalSocket::readyRead, [&sock]() {
qint64 bytesAvailable = sock.bytesAvailable();
QByteArray buffer = sock.readAll();
QDataStream ds(buffer);
while (bytesAvailable > 0) {
int num;
ds >> num;
qDebug("received %d", num);
bytesAvailable -= 4;
if (num == 1) {
sendData(sock, 2);
sendData(sock, -1);
} else if (num == 3) {
sendData(sock, -3);
}
}
});
sock.connectToServer("/tmp/qeventloop_test");
return app.exec();
}
実行結果は以下のようになります。
send 1
start event loop to wait for 1
bytesAvailable: 8
received 2
send 3
start event loop to wait for 3
bytesAvailable: 4
received -3
end event loop to wait for 3
received -1
end event loop to wait for 1
次にサーバ側プログラムの以下のQueuedConnectionとなっている箇所を
QObject::connect(m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead, Qt::QueuedConnection);
以下のようにDirectConnection(デフォルトではsenderとreceiverが同一スレッドであればDirectConnectionとなります)を使うように変更してみます。
QObject::connect(
m_sock, &QLocalSocket::readyRead, this, &Server::onReadyRead);
実行結果は以下のようになります。クライアントが-3を送信してもサーバのQLocalSocketはreadyReadシグナルが呼び出されないので、以下のように永遠にイベントループが終了しません。
send 1
start event loop to wait for 1
bytesAvailable: 8
received 2
send 3
start event loop to wait for 3
QLocalSocket, QEventLoopを使う時はreadyReadシグナルの仕様に気をつけましょう!