試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

libsoupライブラリのHTTPサーバ機能を用いたテスト(例)

libsoupライブラリのHTTPサーバ機能を用いたテスト(メモ)」の続きとして、簡単な例を貼り付ける。

[任意]ファイル名: httpservertest.vala エンコーディング: UTF-8

/*
 * libsoup 2.4を用いたHTTPサーバのテスト
 *
 * 例:
 * http://127.0.0.1:11111/
 * http://127.0.0.1:11111/?key1=value1&key2=value2
 *
 * valac --pkg posix --pkg libsoup-2.4 httpservertest.vala -o httpservertest
 */

using Posix;
using Soup;

namespace HttpServerTest
{
  /**
   * libsoupはGObjectのオブジェクトシステムに基づいており
   * Valaのオブジェクト指向プログラミングでそのまま扱える
   * (GTK+,libnotify,GStreamerなどと同様にクラスの継承なども簡単に行える)
   */
  class Httpd : Soup.Server
  {
    int count = 1;
    public
    Httpd ()
    {
      GLib.Object (port: 11111);  // オブジェクト生成時のみ書き込める

      // FIXME: ポートが使用中のときにはこの先の処理に失敗するが判別ができない?

      // 呼ばれたURLによって対応を変えるために別々のハンドラを用意して関連付ける
      // add_handler()ではパス名からの先頭のマッチでハンドラを振り分けるだけなので
      // 「/reset」を指定しても「/resetfoobar」のURLでそのハンドラが呼ばれてしまう点に注意
      this.add_handler ("/", this.default_handler);
      this.add_handler ("/reset", this.reset_handler);
      this.add_handler ("/quit", this.quit_handler);
      this.add_handler ("/style.css", this.css_handler);
    }
    void
    default_handler (Soup.Server server,
                     Soup.Message msg,
                     string path,
                     GLib.HashTable<string,string>? query,
                     Soup.ClientContext client)
    {
      // add_handler()に「/」を指定すると他のハンドラに一致しないURLは
      // このハンドラで処理されるため、関係ないパスのものはチェックして弾くことにする
      if (path != "/" && ! path.has_prefix ("/?"))
      {
        // ステータスによるNOT_FOUND通知
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        string html;
        string queries = "";
        unowned Soup.Address addr = client.get_address ();
        if (query != null)
        {
          // GETメソッドの「key=val」形式のパラメータ
          query.foreach ((key, val) =>
          {
            queries += @"<tr><td>$((string) key)</td><td>$((string) val)</td></tr>\n";
          });
        }
        html = @"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>テスト</title>
</head>
<body>
<p id=\"body\">これはテストです。起動後$(this.count)回目のアクセスです。</p>
<table>
<caption>情報一覧(一部)</caption>
<thead>
<tr><th>項目/変数名</th><th>値</th>
</thead>
<tbody>
<tr><td>path</td><td>$(path)</td></tr>
<tr><td>msg.method</td><td>$(msg.method)</td></tr>
<tr><td>msg.http_version</td><td>$(msg.http_version)</td></tr>
<tr><td>msg.is_keepalive()</td><td>$(msg.is_keepalive ())</td></tr>
<tr><td>client.get_address().port</td><td>$(addr.port)</td></tr>
$(queries)</tbody>
</table>
<hr>
<p id=\"footer\"><a href=\"/reset\">カウンタをリセットする</a> | <a href=\"/quit\">プログラムを終了</a></p>
</body>
</html>";
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
        this.count++;
      }
    }
    void
    reset_handler (Soup.Server server,
                   Soup.Message msg,
                   string path,
                   GLib.HashTable<string,string>? query,
                   Soup.ClientContext client)
    {
      // そのままでは「/reset[任意の文字列]」のURLも対象にするため
      // 「/reset」でない場合は弾く
      if (path != "/reset")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>カウンタのリセット</title>
</head>
<body>
<p id=\"body\">カウンタをリセットしました。</p>
<hr>
<p id=\"footer\"><a href=\"/?\">トップページ</a> | <a href=\"/quit\">プログラムを終了</a></p>
</body>
</html>";
        this.count = 1;
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
      }
    }
    void
    quit_handler (Soup.Server server,
                  Soup.Message msg,
                  string path,
                  GLib.HashTable<string,string>? query,
                  Soup.ClientContext client)
    {
      // そのままでは「/quit[任意の文字列]」のURLも対象にするため
      // 「/quit」でない場合は弾く
      if (path != "/quit")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>プログラム終了</title>
</head>
<body>
<p id=\"body\">このプログラムを終了します。</p>
</body>
</html>";
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
        // ループを抜ける
        this.quit ();
      }
    }
    void
    css_handler (Soup.Server server,
                 Soup.Message msg,
                 string path,
                 GLib.HashTable<string,string>? query,
                 Soup.ClientContext client)
    {
      if (path != "/style.css")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var css = """
table
{
  border-collapse: collapse;
}
table, th, td
{
  border: 1px solid black;
  padding: 0.2em;
  margin: 0px;
}
th
{
  background-color: aliceblue;
}
p#body
{
  background-color: mistyrose;
  padding: 0.1em;
}
p#footer
{
  font-size: 80%;
}
""";
        msg.set_response ("text/css",
                          Soup.MemoryUse.COPY,
                          css,
                          css.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
      }
    }
    public static int
    main (string[] args)
    {
      var s = new Httpd ();
      s.run ();  // ループを抜けるまで先に進まない
      return EXIT_SUCCESS;
    }
  }
}

実行例

コンパイルした実行ファイルを実行するとループに入るので
http://127.0.0.1:11111/
にアクセスすると

このようなページが表示される。ページを再読み込みするなどして開き直すと「[回数]回目のアクセスです。」の数字が増えていく。

また、
http://127.0.0.1:11111/?key1=value1&key2=value2
にアクセスするとURLに含まれる引数の名前(上のURLではkey1,key2)とその値(上のURLではvalue1,value2)も情報に追加される。

「カウンタをリセットする」のリンクをたどると

「カウンタをリセットしました。」と表示されてトップページに戻ったときにカウンタが1からになり、「プログラムを終了」のリンクをたどると

「このプログラムを終了します。」と表示されつつプログラムが終了する。

サーバの非同期ループとGLibのメインループを用いた例

これは上の例と基本的に処理内容は同じだが、サーバのループは非同期版を用いており、ループはGLibのメインループを用いている。GTK+やGStreamerをlibsoupアプリケーションで用いる場合はこのような形でループをする。
[任意]ファイル名: httpservertest2.vala エンコーディング: UTF-8

/*
 * libsoup 2.4を用いたHTTPサーバのテスト(サーバ非同期ループ/GLibメインループ使用版)
 *
 * 例:
 * http://127.0.0.1:11111/
 * http://127.0.0.1:11111/?key1=value1&key2=value2
 *
 * valac --pkg posix --pkg libsoup-2.4 httpservertest2.vala -o httpservertest2
 */

using Posix;
using Soup;

namespace HttpServerTest
{
  /**
   * libsoupはGObjectのオブジェクトシステムに基づいており
   * Valaのオブジェクト指向プログラミングでそのまま扱える
   * (GTK+,libnotify,GStreamerなどと同様にクラスの継承なども簡単に行える)
   */
  class Httpd : Soup.Server
  {
    int count = 1;
    public static GLib.MainLoop loop;
    public
    Httpd ()
    {
      GLib.Object (port: 11111);  // オブジェクト生成時のみ書き込める

      // FIXME: ポートが使用中のときにはこの先の処理に失敗するが判別ができない?

      // 呼ばれたURLによって対応を変えるために別々のハンドラを用意して関連付ける
      // add_handler()ではパス名からの先頭のマッチでハンドラを振り分けるだけなので
      // 「/reset」を指定しても「/resetfoobar」のURLでそのハンドラが呼ばれてしまう点に注意
      this.add_handler ("/", this.default_handler);
      this.add_handler ("/reset", this.reset_handler);
      this.add_handler ("/quit", this.quit_handler);
      this.add_handler ("/style.css", this.css_handler);
    }
    void
    default_handler (Soup.Server server,
                     Soup.Message msg,
                     string path,
                     GLib.HashTable<string,string>? query,
                     Soup.ClientContext client)
    {
      // add_handler()に「/」を指定すると他のハンドラに一致しないURLは
      // このハンドラで処理されるため、関係ないパスのものはチェックして弾くことにする
      if (path != "/" && ! path.has_prefix ("/?"))
      {
        // ステータスによるNOT_FOUND通知
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        string html;
        string queries = "";
        unowned Soup.Address addr = client.get_address ();
        if (query != null)
        {
          // GETメソッドの「key=val」形式のパラメータ
          query.foreach ((key, val) =>
          {
            queries += @"<tr><td>$((string) key)</td><td>$((string) val)</td></tr>\n";
          });
        }
        html = @"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>テスト</title>
</head>
<body>
<p id=\"body\">これはテストです。起動後$(this.count)回目のアクセスです。</p>
<table>
<caption>情報一覧(一部)</caption>
<thead>
<tr><th>項目/変数名</th><th>値</th>
</thead>
<tbody>
<tr><td>path</td><td>$(path)</td></tr>
<tr><td>msg.method</td><td>$(msg.method)</td></tr>
<tr><td>msg.http_version</td><td>$(msg.http_version)</td></tr>
<tr><td>msg.is_keepalive()</td><td>$(msg.is_keepalive ())</td></tr>
<tr><td>client.get_address().port</td><td>$(addr.port)</td></tr>
$(queries)</tbody>
</table>
<hr>
<p id=\"footer\"><a href=\"/reset\">カウンタをリセットする</a> | <a href=\"/quit\">プログラムを終了</a></p>
</body>
</html>";
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
        this.count++;
      }
    }
    void
    reset_handler (Soup.Server server,
                   Soup.Message msg,
                   string path,
                   GLib.HashTable<string,string>? query,
                   Soup.ClientContext client)
    {
      // そのままでは「/reset[任意の文字列]」のURLも対象にするため
      // 「/reset」でない場合は弾く
      if (path != "/reset")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>カウンタのリセット</title>
</head>
<body>
<p id=\"body\">カウンタをリセットしました。</p>
<hr>
<p id=\"footer\"><a href=\"/?\">トップページ</a> | <a href=\"/quit\">プログラムを終了</a></p>
</body>
</html>";
        this.count = 1;
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
      }
    }
    void
    quit_handler (Soup.Server server,
                  Soup.Message msg,
                  string path,
                  GLib.HashTable<string,string>? query,
                  Soup.ClientContext client)
    {
      // そのままでは「/quit[任意の文字列]」のURLも対象にするため
      // 「/quit」でない場合は弾く
      if (path != "/quit")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
                      \"http://www.w3.org/TR/html4/loose.dtd\">
<html lang=\"ja\">
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=\"content-script-type\" content=\"text/javascript\">
<meta http-equiv=\"content-style-type\" content=\"text/css\">
<link rel=\"stylesheet\" href=\"/style.css\">
<title>プログラム終了</title>
</head>
<body>
<p id=\"body\">このプログラムを終了します。</p>
</body>
</html>";
        msg.set_response ("text/html",
                          Soup.MemoryUse.COPY,
                          html,
                          html.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
        // ループを抜ける
        this.quit ();  // サーバのループ
        Httpd.loop.quit ();  // GLibのメインループ
      }
    }
    void
    css_handler (Soup.Server server,
                 Soup.Message msg,
                 string path,
                 GLib.HashTable<string,string>? query,
                 Soup.ClientContext client)
    {
      if (path != "/style.css")
      {
        msg.set_status (Soup.KnownStatusCode.NOT_FOUND);
        return;
      }
      else
      {
        var css = """
table
{
  border-collapse: collapse;
}
table, th, td
{
  border: 1px solid black;
  padding: 0.2em;
  margin: 0px;
}
th
{
  background-color: aliceblue;
}
p#body
{
  background-color: mistyrose;
  padding: 0.1em;
}
p#footer
{
  font-size: 80%;
}
""";
        msg.set_response ("text/css",
                          Soup.MemoryUse.COPY,
                          css,
                          css.size ());
        msg.set_status (Soup.KnownStatusCode.OK);
      }
    }
    public static int
    main (string[] args)
    {
      var s = new Httpd ();
      s.run_async ();  // 非同期なのですぐ次に進む
      Httpd.loop = new GLib.MainLoop ();
      Httpd.loop.run ();
      return EXIT_SUCCESS;
    }
  }
}

関連記事:

使用したバージョン:

  • libsoup-2.4 2.30.2
  • Vala 0.10.1