Help us understand the problem. What is going on with this article?

Qt HTTP Server を試してみよう

More than 1 year has passed since last update.

快挙!

今年も Qt Champion が決まり、なんと日本から Kazuo Asano さんが選ばれました!!!

This year’s *Ambassador* title goes to Kazuo Asano who’s a very active IRL Qt promoter in Japan. His various activities including study-sessions, seminar and now a book are doing wonder making Qt known in the empire of the rising sun.

これで日本からはこの制度が始まった2014年以降、4年で3人もの Qt Champion が誕生しているわけで、 オープンソース を中心とした ユーザーコミュニティ の重要性を再度認識している次第です。Qt 自体のユーザーも、Qt Creator ユーザーも、Qt 関連の書籍も結構増えたなというのを実感した2018年でした。来年も楽しみですね!

はじめに

昨日は @moguriso さんによる「Qt SensorsでIoTしたつもりになる」でした。
最初の一歩は無事踏み出せたようなので、今後の展開に期待しましょう。カレンダーを埋めるためだけにちょっと試してみたとかではないはず。

今日は、Qt Project の元で開発されているモジュールのうち、qt-labs/qthttpserver を取り上げてみましょう。

Qt HTTP Server #とは

2017年10月に、Qt の開発者のメーリングリストに以下のメールが流れてきました。

[Development] Repository request: HTTP server

Hi all,

We have recently been working on a research project looking into the possibilities for creating a lightweight server component that can easily enable Qt applications to serve over HTTP. We would like to make this work public and therefore request a repository.

This work is intended to continue as a research project, to explore alternatives and reveal areas that need work, so it should be under qt-labs.

Name of the project: Qt HTTP Server
Responsible person: ******* ** ****
Gerrit user/email: ****.***@qt.io
Desired repository name: qthttpserver

2012年に、自分用の Qt で HTTP のサーバー を作り始めてはや5年、やっと時代 Qt が俺に追いついたな?という感じですね。

このリポジトリの 一番最初のコミットのメッセージ に、このモジュールのゴールが定義されていました。

Introduce Qt HttpServer framework

Small, Qt integrated framework for creating specialized http server.

Goals of the project:
- Create a framework allowing creation of a specialized web server
running in non public networks (home and company networks, stealth
or hidden services)
- Create an easy tool for developers to embed http servers in their
apps.
- Playground to narrow down problems in Qt, related to network
stack, but also to explore general usability.
- Potentially reduce code duplication in Qt.

Not goals:
- Standalone server, in particular not Apache or nginx replacement

とのことでした。

ソースコードの入手とビルド

$ git clone http://code.qt.io/qt-labs/qthttpserver.git
$ cd qthttpserver
$ ls -a
.           .gitmodules     src
..          .qmake.conf     sync.profile
.git            qthttpserver.pro    tests

ということで、ごくごく普通の Qt のモジュールの形式になっています。git の submodule があるようなので、それも合わせて取得しましょう。

$ git submodule
-edeedb1b4d2f34e4c7d8045ac8b92adbc35e7ed7 src/3rdparty/http-parser
$ git submodule update --init
Submodule 'src/3rdparty/http-parser' (https://github.com/nodejs/http-parser.git) registered for path 'src/3rdparty/http-parser'
Cloning into '/Users/tasuku/io/qt/code/qt-labs/qthttpserver/qthttpserver/src/3rdparty/http-parser'...
Submodule path 'src/3rdparty/http-parser': checked out 'edeedb1b4d2f34e4c7d8045ac8b92adbc35e7ed7'

nodejs 由来の http-parser が使われているようです。

ではビルドしてみましょう。

$ cd ..
$ mkdir build
$ qmake -r ../qthttpserver
$ make -j4
$ make install # 必要な場合のみ、root 権限が必要な場合は sudo で実行

使い方

まだ、examples などという親切なものも、オンラインで参照可能なドキュメントも無いようなので、ソースコードをみてみましょう。

$ cd ../qthttpserver/src/httpserver
$ ls
httpserver.pro          qhttpserverrequest.cpp      qhttpserverresponder.h
qabstracthttpserver.cpp     qhttpserverrequest.h        qhttpserverresponder_p.h
qabstracthttpserver.h       qhttpserverrequest_p.h      qthttpserverglobal.h
qabstracthttpserver_p.h     qhttpserverresponder.cpp

_p.h はプライベートな API (=普通は使わない)なので、

  • qabstracthttpserver.h
  • qhttpserverrequest.h
  • qhttpserverresponder.h

を見ればいいようだ。
QAbstractHttpServer を継承したクラスを作って、QHttpServerRequest の内容に応じて QHttpServerResponder の API で実際のコンテンツを返してやれば良さそうだ。ということがなんとなくわかる。

QAbstractHttpServer

qabstracthttpserver.h
class Q_HTTPSERVER_EXPORT QAbstractHttpServer : public QObject
{
    Q_OBJECT
public:
    QAbstractHttpServer(QObject *parent = nullptr);

    int listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);

    void bind(QTcpServer *server = nullptr);
    QVector<QTcpServer *> servers() const;

Q_SIGNALS:
    void missingHandler(const QHttpServerRequest &request, QTcpSocket *socket);

#if defined(QT_WEBSOCKETS_LIB)
    void newWebSocketConnection();

public:
    bool hasPendingWebSocketConnections() const;
    QWebSocket *nextPendingWebSocketConnection();
#endif // defined(QT_WEBSOCKETS_LIB)

protected:
    QAbstractHttpServer(QAbstractHttpServerPrivate &dd, QObject *parent = nullptr);

    virtual bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) = 0;
    static QHttpServerResponder makeResponder(const QHttpServerRequest &request,
                                              QTcpSocket *socket);
    ...
};

QTcpSocket と同様に、QHostAddressportlisten() する API と、ユーザー側で用意した QTcpSocketbind() する API があるようだ。

QtWebSockets が利用可能な場合には WebSockets 対応もできるようだ。

継承したクラスでは、virtual になっている handleRequest() を実装すれば良さそうで、makeResponder の API で、なんかコンテンツを返すクラスを使うことで色々簡単になるっぽい。

QHttpServerRequest

qhttpserverrequest.h
class Q_HTTPSERVER_EXPORT QHttpServerRequest : public QObjectUserData
{
    ...
public:
    ~QHttpServerRequest() override;

    enum class Method
    {
        Unknown = -1,

        Get,
        Put,
        Delete,
        Post,
        Head,
        Options,
        Patch
    };

    QString value(const QString &key) const;
    QUrl url() const;
    Method method() const;
    QVariantMap headers() const;
    QByteArray body() const;
    ...
};

特筆すべきは QObjectUserData などという滅多に見かけないクラスを継承しているということ。

そして、value ってなんだよ。と思って実装を見ると、

qhttpserverrequest.cpp
QString QHttpServerRequest::value(const QString &key) const
{
    return d->headers.value(d->headerHash(key)).second;
}

なるほどヘッダか。命名バグですね。

QHttpServerResponder

qhttpserverresponder.h
class QHttpServerResponderPrivate;
class Q_HTTPSERVER_EXPORT QHttpServerResponder final
{
    Q_DECLARE_PRIVATE(QHttpServerResponder)

    friend class QAbstractHttpServer;

public:
    enum class StatusCode {
        // 1xx: Informational
        Continue = 100,
        SwitchingProtocols,
        Processing,

        // 2xx: Success
        Ok = 200,
        Created,
        ...
    };

    QHttpServerResponder(QHttpServerResponder &&other);
    ~QHttpServerResponder();

    void write(QIODevice *data, const QByteArray &mimeType, StatusCode status = StatusCode::Ok);
    void write(const QByteArray &data,
               const QByteArray &mimeType,
               StatusCode status = StatusCode::Ok);
    void write(const QJsonDocument &document, StatusCode status = StatusCode::Ok);
    void write(StatusCode status = StatusCode::Ok);

    QTcpSocket *socket() const;

    bool addHeader(const QByteArray &key, const QByteArray &value);
    ...
};

コンテンツを書き出す用の write が数パターン用意されていますね。

簡単なサーバーを書いてみる

httpserver.pro
TEMPLATE = app
TARGET = httpserver
QT = httpserver

SOURCES = main.cpp
main.cpp
#include <QtCore/QCoreApplication>
#include <QtHttpServer>

class HttpServer : public QAbstractHttpServer
{
public:
    bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) override {
        makeResponder(request, socket).write("Hello World!", "text/plain");
        return true;
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    HttpServer server;
    server.listen(QHostAddress::LocalHost, 8080);

    return app.exec();
}

これで、ビルドし実行し、http://localhost:8080/ にアクセスすると以下のようになります。

image.png

QtHttpServer に貢献してみました

QAbstracdtHttpServerQVector<QTcpServer *> servers() const; という API があるのですが、その実装がビミョーだったので、パッチを送りました。

https://codereview.qt-project.org/#/c/243297/2/src/httpserver/qabstracthttpserver.cpp,unified

QVector<QTcpServer *> QAbstractHttpServer::servers() const
{
-    auto c = children();
-    QVector<QTcpServer *> result;
-    result.reserve(c.size());
-    for (auto i = c.constBegin(); i != c.constEnd(); ++i) {
-        if ((*i)->inherits("QTcpServer"))
-            result.append(static_cast<QTcpServer*>(*i));
-    }
-    result.squeeze();
-    return result;
+    return findChildren<QTcpServer *>().toVector();
}

一行でできることをですね、(ry

ついでに、その API のテストコードがなかったので 追加しておきました

さらに貢献してみました。

Add qml module というパッチを書いて、QML でもサーバーが作れるようにしてみましたw

main.qml
import QtHttpServer 1.0

HttpServer {
    id: root
    listen: true
    port: 8080
    onPortChanged: console.debug(port)
    onRequest: {
        var headers = []
        for (var i in request.headers) {
            headers.push('<dt>%1</dt><dd>%2</dd>'.arg(i).arg(request.headers[i]))
        }

        responder.write('<dl><dt>URL:</dt><dd>%1</dd>%2</dl>'.arg(request.url).arg(headers.join('')), 'text/html')
    }
}

雑に実装したので色々ツッコミが入りそうですが、これは どうしてもサポートしたい機能 なので頑張ります!

おわりに

Qt Project では、様々なプロジェクトが開発されています。qtbase などのメジャーなものから、誰もメンテナンスしてないようなものもありますが、暇なときになんか面白そうなものが無いか、眺めてみるのはいかがでしょう?

明日は @shin1_okada が何かを書きます。お楽しみに!

task_jp
Qt が好きで遊んでいたら2014年、世界で史上2番目の Qt Champion というのになっちゃいました。 2019年にも自身2度目の Qt Champion を獲得。 現在求職中です。今までの経験を活かして活躍できそうなお仕事があれば是非紹介してください。 それ以外のお仕事の問い合わせは https://qt5.jp/qwork/ からお願いします。
https://qt5.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away