2017年6月7日 Theolizer®最新版v1.1.0へ対応するために修正しました。
最新版のソース一式をGistに置いてます。

こんにちは。田原です。

TCP/IP通信と言えばsocketですが、これは何にでも対応できる低レベルなインタフェースなので その分使うのがたいへんです。Theolizer®の通信サンプルを開発するにはちょっと大掛かりになりすぎるので躊躇していました。しかし、Boost.Asioにstd::iostream形式でTCP/IP通信を行える機能が含まれていることが判りました。
そこで、Boost.Asioを使って家計簿データの送受信をやってみました。予想通り実に簡単に通信できましたので、ご紹介します。

今回作ったサンプルは、下記処理を行います。

  1. 家計簿データをサーバへ送信
  2. サーバにて集計処理
  3. 集計結果だけをクライアントへ返却
  4. クライアントで表示

TCP/IP通信ですので、サーバとクライアントは別PC上にあっても動作します。
Windows PCとubuntu間での通信もあっさり成功しました。

1.Boost.Asioについて

Boost.Asioは非同期I/O処理ライブラリで、IP通信のサポートが充実しています。
std::iostreamを派生したTCP/IPストリーム機能を提供しています。今回はこの機能を使いました。

クライアント側は、fstreamとほぼ同じイメージで使えます。
fstreamではファイル名を指定してファイルをオープンしますが、Boost.Asioのクライアント側のストリームはサーバのドメイン名(IPアドレスも可)とポート番号を指定します。
例えば、下記にてhttpのGETリクエストを発行して、応答を受信しています。同期処理とは言え、たったこれだけでできます。

// TCP/IPコネクション接続
boost::asio::ip::tcp::iostream aTcpStream("theolizer.com", "80" );

// タイムアウト設定
aTcpStream.expires_from_now(boost::posix_time::seconds(3));

// 要求送信
aTcpStream << "GET / HTTP/1.0\nHost: theolizer.com\n" << std::endl;

// 応答受信
std::string line;
while(std::getline(aTcpStream, line))
    std::cout << line << "\n";

// エラー・チェック
if ((!aTcpStream) && (!aTcpStream.eof()))
    std::cout << "error: " << aTcpStream.error().message() << std::endl;

注)std::getline()はC言語標準ライブラリのgetline()とほぼ同じ機能を提供します。

サーバ側は接続待ちするのですが、何故かBoost.Asioのストリーム・サボートは接続待ちに対応していません。この部分はBoost.Asioの通常の同期処理か非同期処理を使って接続を待ちます。
基本はポート番号を指定して接続待ちするだけですので、本質的には1行に纏めることができますが、定形処理が若干あるので下記のように数行かかります。

//ポート番号12345で接続待ち
boost::asio::io_service         aIoService;
boost::asio::ip::tcp::endpoint  aEndPoint(boost::asio::ip::tcp::v4(), 12345);
boost::asio::ip::tcp::acceptor  aAcceptoracc(aIoService, aEndPoint);
boost::asio::ip::tcp::iostream  aTcpStream;
aAcceptoracc.accept(*aTcpStream.rdbuf());

今回のサンプル・ソースでは上記を少しまとめて使い易くしたTcpStreamクラスを用意しました。

boost1.59.0の問題について
Theolizer®開発中にも遭遇したのですが、boost 1.59.0にはいくつかのトラブルがあります。
1つはgcc(MinGW含む)5.4.0等の新しいバージョンでビルドする時、幾つか警告が生成されます。boostソース内で出ます。Theilizerのビルド時のみこれらの警告をコンパイラ・オプションでディセーブルして対処してます。
今回のサンプルもboostを使うためその警告が出てしまいます。(boostのソースで「std::auto_ptrは非推奨」警告が出ます。)asio_helper.hにてgccならば#pragma GCC diagnostic ignored “-Wdeprecated-declarations”を定義することでこの警告をディセーブルしています。

また、Visual Studio 2015でビルドする際にBoost.Asioに不具合が出ます。
これはboost 1.62.0にて対処されてます。
他のツールのバージョンも絡むので今はboostを1.62.0へ上げる時間が取れないため、暫定的にこのリンク先と同じ対処をboostboost1.59.0へ当てて使っています。

2.通信処理

今回は前々回の技術解説でご紹介した家計簿ソフトをファイル保存/回復ではなく通信するように変更しました。
クライアントは、家計簿データの初期設定と表示、サーバへの家計簿データ(集計要求)送信と集計結果(応答)受信、集計結果の表示を行います。
サーバは、家計簿データ(集計要求)受信、集計処理と表示、集計結果(応答)送信を行います。

2-1.クライアント側

今回はBinaryI/OSerializerを使ってみました。使い方はJsonI/OSerializerとほぼ同じです。PrettyPrint(人が見やすいように整形する)指定がないだけです。Json形式ではなく独自のバイナリー形式でシリアラズ/デシリアライズします。

エンディアン・フリーですので、データ・サイズに注意すれば異なる処理系間でのデータ交換にも使うことができます。
また、整数型変数は変数内の値を記録するのに必要十分なバイト数で保存します。例えばint型変数に100が入っていたらタグの1バイトとデータ1バイトの計2バイトで保存します。(タグ+int型データの5バイトではないです。)

2-1-1.クライアントの通信処理
client.cpp

2-2.サーバ側

サーパ側は上述の通りです。送信しているデータの内容をできるように、最後にJson書式で出力しています。

2-2-1.サーバの通信処理
server.cpp

2-3.データ構造定義

家計簿データ構造を定義しています。内容は前々回のcommon.hとほぼ同じですが、クライアント→サーバへの要求(Request)とサーバ→クライアントへの応答(Response)を保存先指定として追加し、必要なデータのみを通信でやり取りするようにしています。

2-3-1.サーバとクライアントの共通ヘッダ
common.h

2-4.エラー処理について

前々回はファイル保存/回復なのでエラーが発生しにくいため、エラー処理は手を抜いてました。今回は通信なのでクライアント→サーバの順で起動した場合など、簡単にエラーが発生するのでエラー処理を入れてます。

表示しているメッセージはboostから返却されたものですが、いまいち適切ではないメッセージになっています。
クライアントもサーバも一定時間接続できなければタイムアウトしているのですが、「対象のコンピューターによって拒否されたため、接続できませんでした。」と言うエラーメッセージが表示されます。

2-5.Boost.Asio用ヘルパー・クラスTcpStream

Boost.AsioのTCP/IPストリーム機能を使いやすくするため、TcpStreamクラスを用意しました。
これはBoost.AsioのTCP/IPストリーム(ip::tcp::iostream)クラスに下記機能を追加したものです。

  1. クライアント側が指定するポート番号を文字列ではなくunsigned shortで指定
  2. サーバ側はポート番号とタイムアウトを指定して接続待ち
  3. 通信タイムアウトをunsigned int型の秒で指定
  4. エラー・メッセージをUTF-8へ変換(boostからはMultiByte文字列で返ってきます)

C++の比較的高度な機能を多用しています。できるだけC++の深い機能を使わない当技術解説としてはよろしくないのですが、将来的にTcpStreamをTheolizer®内部に取り込む方向で考えていますのでご勘弁頂ければ幸いです。

2-5-1.Boost.Asioを使うためのヘルパー・クラス
asio_helper.h

3.実行結果

3-1.クライアント(Windows 10)の実行画面

クライアントは家計簿データをサーバへ送り、サーバが送り返してきた集計結果を表示しています。
下記画面はサーバから送り返されてきた集計結果です。

 

3-2.サーバ(ubuntu 16.04 LTS)の実行画面

サーバはクライアントから受け取った家計簿データを集計して表示し、クライアントへ送り返しています。

3-3.サーバが送り返しているデータ

実際にはバイナリー・データですが、人が読み取ることはちょっと無理があるので、Json形式でダンプしてみました。(JsonOSerializerを使ってお手軽に)
ざっと見て頂ければ分かりますが、Itemクラス内のmParent, mName, mIsAssets, mManageType, mUnit変数は送信されていません。集計結果が保存されているmAssetsIncrease, mAmountとItemツリーとしてアクセスするためのmChildrenのみを含みます。

3-3-1.サーバがクライアントへ返却した集計結果のJson形式ダンプ
response.json

 

4.従来のソース(分割)

クライアントとサーバに分けたので従来sub.cppに入れていたソースを3つに分割しました。

4-1.共通部

集計結果の表示とItemのフルネーム生成処理です。

4-1-1.サーバとクライアントの共通処理
common.cpp

4-2.クライアント側

家計簿データの初期化と表示処理です。

4-2-1.クライアントのサブ・ルーチン
client_sub.cpp

4-3.サーバ側

家計簿データの集計処理です。

4-3-1.サーバのサブ・ルーチン
server_sub.cpp

5.自動生成ソース

大半の部分はサーバとクライアントで共通なcommon.hから生成されるため、client.cpp.theolizer.hppとほぼ同じです。しかし、それぞれclient.cppとserver.cppも用いて生成されるため、微妙に異なります。

5-1.クライアントの自動生成ソース

client.cpp.theolizer.hpp

5-2.サーバの自動生成ソース

server.cpp.theolizer.hpp

5-3.CMakeLists.txt

CMakeLists.txt

 

6.最後に

TCP/IPで通信して異なる処理系間でもデータ交換できることを確認しました。
意外に簡単なことに驚かれたのではないでしょうか。
私はもう少し苦労すると思っていたのですが、Boost.Asioのお陰で1日も掛かりませんでした。
Boost、素晴らしいです。