Qt
QtDay 18

websocketで待ち受けるサーバなのにJSONベースのコマンドを自発的に投げるツールを作ろう

この投稿は、Qt Advent Calendar 2017の18日目の投稿です。

昨日は@argama147さんの「今更聞けないシグナル・スロット総整理」でした。日本語のドキュメントは本当に不足していると思うのでこうして情報が増えていくのは素晴らしいですよね。

tl;dr

仕事で欲しいテスト用のアプリをQtを使って作ってみました。

  • QtでWebsocketのサーバをたてた(QWebSocketServer
  • GUIから任意のタイミングでJSONフォーマットのコマンド送信できるようにした(QJsonObject
  • 仕事用に直したコードは社内のgitにおいて情報展開しておいた
  • きっと同僚の誰かがQtはじめてくれると信じます

はじめに

最近の仕事でWebsocketのサーバ/クライアント開発をやっております。主にクライアント担当なのですが動作自体は接続先のサーバが投げてくるコマンドに沿って行う感じのため割とデバッグが面倒な感じです。今はnode.jsでWebsocketサーバを立てて適当に手打ちしたコマンドを投げさせているのですが、この仕組みでやっていると

  • 毎回jsを書き直す必要がある
  • 連続するコマンド処理を制御するのにロジックを作りこむ必要がある(jsでやりたくない...)
  • 結果の確認がずさんになってる

辺りに問題があります。細かいのを上げればきりがありませんが、、、そして、別にこのjsをいじくって直すかクライアント側の実装言語になってるのはgolangなので対向側のサーバも簡易で書いてしまえば良いような気も大変するのですが面倒でやっていませんでした。

そんな折、Qtアドベントカレンダー担当の”はりきら神さま”からの勅命を頂いてしまいました。



というわけで、折角なのでQtで書いてみようかなと思った次第です。

取り合えず、今回の方針は

  • 基本的に動けばOK(細かい汎用性とか応用とかも後回し、あくまでテスト用と割り切る)
  • 他のメンバにも保守する気にはなって欲しいのでQtとC++だけ使う(QMLは諦めました)

として、動くもの優先で行っています。仕事に関わらない部分はgithubに公開していますので、reviewやPRも歓迎です。1 尚、とっかかりの記事は別にblogで書いているので良ければそちらと、その作業は月例のQt勉強会でやっていたので、その辺りもぜひ御贔屓に。。。

QtでWebsocketのサーバをたてる(QWebSocketServer

サンプルはいくつもあるのですがシンプルで分かりやすいのは公式のこの辺りでした。

testserver.cpp
TestServer::TestServer(quint16 port, QObject *parent) :
    QObject(parent),
    m_pWebSocketServer(Q_NULLPTR)
{
    m_pWebSocketServer = new QWebSocketServer(QStringLiteral("Test Server"),
                                              QWebSocketServer::NonSecureMode,
                                              this);
    if (m_pWebSocketServer->listen(QHostAddress::Any, port))
    {
        qDebug() << "Echo Server listening on port " << port;
        connect(m_pWebSocketServer, &QWebSocketServer::newConnection,
                this, &TestServer::onNewConnection);
    }
}

コンストラクタでQWebSocketServerのインスタンスをnewして

testserver.cpp
void TestServer::onNewConnection()
{
    QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection();
    pSocket->setParent(this);

    connect(pSocket, &QWebSocket::textMessageReceived, this, &TestServer::processTextMessage);
    connect(pSocket, &QWebSocket::binaryMessageReceived, this, &TestServer::processBinaryMessage);
    connect(pSocket, &QWebSocket::disconnected, this, &TestServer::socketDisconnected);

    m_clients << pSocket;
}

接続イベントを受け取るコールバックでメッセージ受信イベントのコールバックを登録。

testserver.cpp
void TestServer::processTextMessage(QString message)
{
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient)
    {
        pClient->sendTextMessage(message);
    }
}

Echoサーバなので受信した内容をそのまま返すだけというシンプルさです。個人的にコンストラクタでいろいろやってmain.cppがあんまり何もしないのが好きになれませんが、、、

接続直後にサーバからクライアントにデータを投げる

Echoサーバベースなので当然ですが、このままだとクライアントがデータを投げてくるまでサーバが何もしてくれません。というわけで、クライアントが接続してきたタイミングでサーバからメッセージを投げるように直します。というと簡単ですが、まるでこの辺りを把握していないので軽くはまりました。どうも公式のsimple chat serverを参考にするとonNewConnection時のpSocketがそもそもQWebSocketなのでそれを使えばいいだけのようでした。

testserver.cpp
    m_clients << pSocket;

    for (QWebSocket *pClient : qAsConst(m_clients)) {

というわけで

testserver.cpp
void TestServer::onNewConnection()
{
    QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection();
    pSocket->setParent(this);

    connect(pSocket, &QWebSocket::textMessageReceived, this, &TestServer::processTextMessage);
    connect(pSocket, &QWebSocket::binaryMessageReceived, this, &TestServer::processBinaryMessage);
    connect(pSocket, &QWebSocket::disconnected, this, &TestServer::socketDisconnected);

    m_clients << pSocket;

    for (QWebSocket *pClient : qAsConst(m_clients)) {
        pClient->sendTextMessage("abcdefghijkllm");
    }
}

という感じにしたら接続時にクライアントにメッセージを投げられるようになりました。

GUIウィンドウを追加する

ここまではCUIで作成していました。よく考えるとこれだと送りたいタイミングでコマンドを送れないので、GUIを用意してボタンを押すとコマンドを送るように修正しました。

修正版コードはこちら。冒頭で作ったTestServerクラスに以下のメソッドを追加しました。

testserver.cpp
bool TestServer::SendText(QString str)
{
    bool r_inf = false;

    for (QWebSocket *pClient : qAsConst(m_clients)) {
        pClient->sendTextMessage(str);
        r_inf = true;
    }

    return r_inf;
}

送信電文をJSONでやり取りする(QJsonObject

仕事で作ってるのがデータのやり取りをJSONでやっているからというのもありますが、昨今は割とJSONでやり取りするのが一般的なんだろうと勝手に思っていたりします。C++でこれを何とかしようと思うと外部のjsonライブラリ(picojsonとかjson11?)を使う方法しか考えない程度にC/C++力は衰えておりますが、そもそもQtにはQJsonObjectというクラスがあって、下手するとgolangより楽に操作できる感がありました。

testcommand.cpp
QString TestCommand::getCommand() {
    QJsonObject jHead, jPayload, jObj;
    jHead["test1"] = "0";
    jHead["test2"] = 1;
    jPayload["test3"] = "1";
    jPayload["test4"] = 2;

    jObj["head"] = jHead;
    jObj["payload"] = jPayload;

    qDebug() << jObj ;

    QJsonDocument doc(jObj);
    QString strJson(doc.toJson(QJsonDocument::Compact));

    return strJson;
}

入れ子や配列もこんなC++っぽくない入れ方で簡単にJSONにしてくれます。素晴らしい。

まとめ

GUIの完成形は↓な感じです。
z.png

本日さっそくtestcommand.cppに必要な電文を追加して利用しました。node.jsだと任意のタイミングでデータを投げたり、投げたデータのログをとったり、はたまた組み合わせコマンドのデバッグなど上げればいくらも面倒があったのですが概ね解決しました。
何より調査と実装を含めて合計4~5時間程度でできたことを考えるとQtを使ったユーティリティツールの生産性は結構高いのではないでしょうか。(実際には開発環境のインストールに結構な時間を使ってますが、それは無視して、、、)(C++で書くのが心の底から苦痛でしたが、、、)

そして、社内のgitにも置いて情報展開しておいたのでそのうちに誰かがQtを使い始めてくれると信じます。
Qt勉強会にも来てくれるといいなぁ。。。

明日は@hermit4さんの”Qt Remote Object入門”です。おやつ部長渾身のPOST(?)に期待しましょう。

蛇足

頼まれてからしばらく、エントリもしていなかった所、はりきら神さま催促を頂いてしまいました。



”はりきらない”じゃないのですか、滅茶はりきってるじゃ無いですか!!??

  1. ないと思うけど・・・