Kikuchy's Second Memory

つくる楽しさをもっと伝えたい。プログラムを書いていて、わからなかったこと・気付いた事を書き留めています。

Qt の QTcpSocket の ReadyReadのタイミングでハマる

異なる OS 間で Socket 通信をしようと思うと、クライアントの実装がなかなか面倒です。
(U|L)nix 系なら socket を、 WIndows なら Winsock2 を使う必要があります。
OS ごとに #ifdef などを使ってコードを分けても良いのでしょうけれど、それも大変。
UI を付けようとすると、また OS ごとに分けなければならず、さらに大変なことになります。
そこで Qt などのマルチプラットフォームなライブラリがあると、その辺りの差異を吸収してくれるので便利です。



そんな訳で、研究室の古いプログラムを (U|L)nix と Windows で動かせるようにしようと Qt を使って通信部分を書き直していたのですが問題発生。


今回書き直しているのは、サーバーとクライアントがペアになって動くプログラムの内の、クライアントの方のみ。
サーバーのプログラムは Unix での動作が想定されていて、当然 socket() を使っています。

クライアントでデータを受信するためには QTcpSocket::waitForReadyRead() を使ってデータの到着を待ち受ける必要があるのですが、こいつが必ずタイムアウトを起こすのです。
(通常、 Qt では readyRead シグナルなどを使ってイベントドリブンにするのですが、今回書き直しているプログラムは CUI プログラムだったため、 QTcpSocket::waitForReadyRead() を使っています)


でも、 QTcpSocket::read() を使ってデータを読み出すと、所望のデータが来ている…
じゃあ「 QTcpSocket::waitForReadyRead() は使わなくて良いかー」と思ってコイツをコメントアウトすると、


今度は所望のデータの前に 0 が混じる……



どうやらデータが全部到着していない模様。かと言って通信の度に QTcpSocket::waitForReadyRead() のタイムアウトを待っていると処理がとんでもなく遅くなってしまいます。
どうしたものやら。



やり取りするデータは、要素数120個の符号無し32ビット整数型配列です。配列一つで 480 バイト。

再現するためのコードは以下。


サーバーは、クライアントから適当なデータを受信した後に、配列を次々に送信します。先述の通り、 Qt を使用しない Unix で動く事を想定したサーバーです。
[C言語]Socket間通信 echoサーバを作る さんのコードを流用させていただきました。

// サーバー側


#include <stdio.h>
#include <stdlib.h>     /* exit() */
#include <string.h>     /* bzero() */
#include <sys/types.h>
#include <sys/socket.h> /* socket(), bind(), listen(), accept(), recv() */
#include <netinet/in.h> /* htons() */
#include <unistd.h>
#include <stdint.h>

#define PORT    8823    /* Listenするポート */
#define MAXDATA 1024    /* 受信バッファサイズ */

int main(void)
{
    struct sockaddr_in saddr; /* サーバ用アドレス格納構造体 */
    struct sockaddr_in caddr; /* クライアント用アドレス格納構造体 */

    int listen_fd;
    int conn_fd;

    int len = sizeof(struct sockaddr_in);

    int rsize;
    char buf[MAXDATA]; /* 受信バッファ */

    /* ソケットの生成 */
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /*
      * saddrの中身を0にしておかないと、bind()でエラーが起こることがある
     */
    bzero((char *)&saddr, sizeof(saddr));

    /* ソケットにアドレスとポートを結びつける */
    saddr.sin_family        = PF_INET;
    saddr.sin_addr.s_addr   = INADDR_ANY;
    saddr.sin_port          = htons(PORT);
    if (bind(listen_fd, (struct sockaddr *)&saddr, len) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    /* ポートをListenする */
    if (listen(listen_fd, SOMAXCONN) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Start Listening Port : %d...\n", PORT);

 /* 接続要求を受け付ける */
    if ((conn_fd = accept(listen_fd, (struct sockaddr *)&caddr, &len)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    /* Listeningソケットを閉じる */
    close(listen_fd);

    /* 送信されたデータの読み出し */
    do {
        rsize = recv(conn_fd, buf, MAXDATA, 0);
        /* クライアントに送るデータの用意 */
        uint32_t table[8*15];
        int i;
        for(i = 0; i < 8*15; i++)
			table[i] = htonl(i);    // ビッグエンディアンにしておく
        if (rsize == 0) { /* クラアイントが接続を切ったとき */
                break;
        } else if (rsize == -1) {
                perror("recv");
                exit(EXIT_FAILURE);
        } else {
        	/* データ第一波 送信 */
        	write(conn_fd, table, sizeof(uint32_t)*8*15);

        	/* 何かの処理の代わり */
                usleep(10);

        	/* データ第二波 送信 */
                write(conn_fd, table, sizeof(uint32_t)*8*15);
        }
    } while (1);

    if ( close(conn_fd) < 0) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("Connection closed.\n");
    return 0;
}

クライアント側は Qt コンソールアプリケーションです。

#include <QTcpSocket>
#include <QDebug>
#include <QByteArray>
#include <QDataStream>

class SockTest
{
public:
    SockTest();
    void Connect();
};

SockTest::SockTest()
{
}

void SockTest::Connect()
{
    /* 接続 */
    QTcpSocket socket;
    socket.connectToHost("localhost", 8823);
    socket.waitForConnected();

    /* 適当なものを送信 */
    socket.write("hello");
    socket.waitForBytesWritten();

    /* レスポンスを読み出す用意 */
    socket.waitForReadyRead();
    qDebug() << "before 1st response: " << socket.bytesAvailable();
    QByteArray data = socket.read(sizeof(quint32)*8*15);
    QDataStream stream(data);
    stream.setByteOrder(QDataStream::BigEndian);

    /* 第一波 読み出し */
    for(int i = 0; i < 8*15; i++){
        quint32 tmp;
        stream >> tmp;
    }

    /* 第一波を読み出した後のバッファの様子 */
    qDebug() << "after 1st response:" << socket.bytesAvailable();

    /* 第二波読み出しの用意。この waitForReadyRead() がタイムアウトを起こすのが問題 */
    socket.waitForReadyRead();
    qDebug() << "before 2nd response: " << socket.bytesAvailable();

    /* 第二波 読み出し */
    data = socket.read(sizeof(quint32)*8*15);
    QDataStream stream2(data);
    stream2.setByteOrder(QDataStream::BigEndian);
    for(int i = 0; i < 8*15; i++){
        quint32 tmp;
        stream2 >> tmp;
    }
    qDebug() << "after 2nd response:" << socket.bytesAvailable();
    socket.close();
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    SockTest sTest;
    sTest.Connect();
    return a.exec();
}

サーバ側の表示はどうでも良いのですが、クライアントの表示をご覧下さい。

before 1st response:  960 
after 1st response: 480 
before 2nd response:  480         ←これが表示されるまで30秒(waitForReadyRead() のデフォルトのタイムアウトまでの時間)かかる
after 2nd response: 0 

サーバー側から第二波のデータが届くタイミングが第一波のデータの直後なので、第一波と一続きになっているように見えているのでしょうか。
単にそうならば、 QTcpSocket::waitForReadyRead() で待たずに第二波を read() で読めば良いだけの話なのですが、そうも行かないのです。
サーバー側で処理にかかる時間はまちまちである(1ミリ秒かからないこともあれば、1秒近くかかることもある)ため、データの到着を待たない訳にはいかないのです。



うぬぬ、 QTcpSocket で recv() みたいに「指定されたサイズ分だけデータが届くまでブロックする」というようなメソッドは無いものでしょうか…




追記(2013年6月10日)

上記クライアント側のコードで、 QTcpSocket::waitForReadyRead() の代わりに以下の関数を挟むようにしました。

// socket はクライアント、 bytes は所望のデータサイズ(Byte)
void SockTest::waitDuringBytesArrival(QtcpSocket *socket, quint64 bytes)
{
    for(;;)
    {
        if(socket->bytesAvailable() >= bytes)
            break;
        socket->waitForReadyRead(-1);
    }
}



// 上記クライアントの SockTest::Connect() の一部をこのように変更しました。


    /* レスポンスを読み出す用意 */
    socket.waitForReadyRead();    // ここはちゃんと動くので別に良い
    qDebug() << "before 1st response: " << socket.bytesAvailable();
    QByteArray data = socket.read(sizeof(quint32)*8*15);
    QDataStream stream(data);
    stream.setByteOrder(QDataStream::BigEndian);

    /* 第一波 読み出し */
    for(int i = 0; i < 8*15; i++){
        quint32 tmp;
        stream >> tmp;
    }

    /* 第一波を読み出した後のバッファの様子 */
    qDebug() << "after 1st response:" << socket.bytesAvailable();

    /* 第二波読み出しの用意。この waitForReadyRead() がタイムアウトを起こすのが問題 */
    waitDuringBytesArrival(&socket, sizeof(quint32)*8*15);    // これでタイムアウトを待たなくても受信ができる。
    qDebug() << "before 2nd response: " << socket.bytesAvailable();

既に必要なサイズのデータが受信されていればループを抜けます。まだ受信されていなければ、 waitForReadyRead() がブロックし、またif文で受信が済んだかどうかをチェックします。
waitForReadyRead() の引数(タイムアウトまでの時間)に負の値を渡すと、タイムアウト無しでデータ到着まで待ってくれるみたいなので、所望のサイズの受信が終わるまで待つ、ということです。

ただ、お察しの通り、タイムアウトによる処理の分岐ができません。
無線の電波が届かなくなった、とか、タイムアウトが必要なアプリケーションには使用できませんね……