Socket.IO C++ Clientを利用する

Socket.IOはwebブラウザ上のアプリケーション間でリアルタイム性の高い通信を実現するためのNode.jsのライブラリです。 Socket.IO C++ ClientはそのSocket.IOのサーバへネイティブアプリから接続するためのライブラリです。 コレを利用するとwebとネイティブアプリで同じサーバを介して通信ができます。 UnixのSocket通信とは異なりストリームではなく、1つ1つのメッセージ単位で授受します。

github.com

  • 5/23 インストール方法の一部に不足があったため編集しました。
  • 5/24 Socket.IO C++ Clientがhttpsに対応しました。以下に関連blogを書きました。

Socket.IO C++ Client over HTTPS - ~llamerada/memo/

前提

  • OS X Yosemite 10.10.3
  • Node.js 0.12.2
  • Socket.IO 1.3.5(サーバ側) Socket.IO C++ Clientを利用するにはver1.0以降が必須です。
  • Socket.IO C++ Client(2015/5/19) 以下sio(C++のnamespaceより)と省略

Node.jsやNode.jsでのSocket.IOの利用方法についてはこの記事では触れません(他にわかりやすい資料がいっぱいあるため)。

インストール(C++版だけ)

sioはBoostを使うので、Boostを用意します。バージョン指定は特になかったので最新版を入れました。

$ brew install boost

パッケージ等はまだ無いので、gitからclone、コンパイルします。 インストールしたboostの格納先に応じてcmakeのオプションは変更してください。

$ cd <設置先ディレクトリ>
$ git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git
$ cd socket.io-client-cpp/
$ cmake -D BOOST_ROOT=/usr/local/Cellar/boost/1.58.0/ . # ← '.'が抜けていたので5/23に追記
$ make -j8
$ make install      # ← 抜けていたので5/23に追記
$ ls
CMakeCache.txt
CMakeFiles
CMakeLists.txt
LICENSE
Makefile
README.md
cmake_install.cmake
examples
lib
libsioclient.a        # ←リンクに使う
src
build                    # ←ヘッダファイル、ライブラリが格納されている
test

サンプルプログラム

webブラウザで入力したコマンドをサーバ経由でネイティブアプリで実行して結果を表示するサンプルプログラムを作りました。 認証しないユーザでコマンドを実行できるので、このままサービスを公開するなどということは絶対しないでください。

github.com

動作

全体では以下の図のような動作をします。 サンプルプログラムなため、sioのエラー処理以外でのパケットのフォーマットチェックなどは省いています。

f:id:llamerad-jp:20150522151021j:plain

サンプルプログラムのコンパイル、起動方法

gitからのclone、コンパイルは以下のようになります。

$ cd <ダウンロード先ディレクトリ>
$ git clone https://github.com/llamerada-jp/socket.io-cpp-client-sample.git
$ cd socket.io-cpp-client-sample
$ cd src
$ mkdir build
$ cd build
$ cmake -D SIO_DIR=<sioを設置したディレクトリ>/socket.io-client-cpp/build -D BOOST_ROOT=/usr/local/Cellar/boost/1.58.0/ ..    # ← 5/23 make install後のパスに合わせて変更
$ make

サーバの起動は以下のとおりです。

$ cd <ダウンロード先ディレクトリ>
$ cd web
$ npm install   # ←依存パッケージのインストール、1度だけで良い
$ node index.js
待機開始

C++プログラムの起動は以下のとおりです。

$ cd <ダウンロード先ディレクトリ>
$ cd src/build
$ ./client <サーバ起動先、http://localhost:8000/とか> <マシン名>
待機開始

webブラウザからhttp://localhost:8000/などにアクセスします。 テキストボックスにlsなどのコマンドを入力し、実行ボタンを押すと、以下のように実行結果が表示されます。

f:id:llamerad-jp:20150522151040p:plain

C++での処理の流れ

SampleClient::runがメインの処理になっています。 on_close、on_fail、on_openはrunの最初でsioにリスナとして登録しておきます。

connectでの接続処理

connectに接続先URLを渡すと、別スレッドで接続処理の待ち合わせが行われます。 socketの取得は接続処理が終わった後でなければなりません。 接続処理が終わると別スレッドでon_openが呼ばれます。 そのため、mutexとcondition_variable_anyを使ってon_openでの処理が終わるまでメインスレッドを待たせています。

// メインスレッドでの処理(runより抜粋)
client.connect(url);
{ // 別スレッドで動く接続処理が終わるまで待つ
  std::unique_lock<std::mutex> lock(sio_mutex);
  if (!is_connected) {
    sio_cond.wait(sio_mutex);
  }
}
socket = client.socket();
 // 接続スレッドでの処理(on_openより抜粋)
{
  std::unique_lock<std::mutex> lock(sio_mutex);
  is_connected = true;
  // 接続処理が終わったのち、待っているメインスレッドを起こす
  sio_cond.notify_all();
}

onでのリスナの登録

web版同様、onメソッドでリスナを登録します。

socket = client.socket();
socket->on("run", std::bind(&SampleClient::on_run, this, std::placeholders::_1));

登録される側は以下のようになっています。処理内容は受信処理の欄で詳しく説明します。

void on_run(sio::event& e) {
  std::unique_lock<std::mutex> lock(sio_mutex);
  sio_queue.push(e.get_message());
  // イベントをキューに登録し、待っているメインスレッドを起こす
  sio_cond.notify_all();
}

emitでの送信処理

メッセージの送信は以下のとおりです。 C++ではJavaScritptでのJSONのような柔らかい(?)構造は取れないため、std::mapを使ってobject相当の構造を作ります。 詳しくはJSONに対応するデータ構造の欄にまとめました。 sio::messageさえ作れれば送信のしかたはweb版と同じです。

sio::message::ptr send_data(sio::object_message::create());
std::map<std::string, sio::message::ptr>& map = send_data->get_map();

// objectのメンバ、typeとnameを設定する
map.insert(std::make_pair("type", sio::string_message::create("native")));
map.insert(std::make_pair("name", sio::string_message::create(name)));

// joinコマンドをサーバに送る
socket->emit("join", send_data);

受信処理

sioではpooling(受信ソケットを監視するなど)の処理を入れる必要はありません。ライブラリ内で別スレッドが起動してサーバとの通信を監視しています。

onメソッドで登録した関数も接続処理と同様、別スレッドで動きます。 サンプルでは受信時はメインスレッドと共有しているキューにデータを登録して、メインスレッドでキューにデータが入っていた場合に解析することで、マルチスレッドでの排他制御を回避しています(イベント受信時と解析時のみ考慮している)。

// sioのメッセージを貯めるためのキュー
std::queue<sio::message::ptr> sio_queue;
void on_run(sio::event& e) {
  std::unique_lock<std::mutex> lock(sio_mutex);
  sio_queue.push(e.get_message());
  // イベントをキューに登録し、待っているメインスレッドを起こす
  sio_cond.notify_all();
}

メインスレッドではキューが空の場合はon_runでsio_cond.notify_all()が呼び出されるまでまち、再度キューの状態を確認します。 キューにデータが入っていることが確認できたら処理に移ります。 処理終了のタイミングでキューからデータを削除します。

while(true) {
  // イベントキューが空の場合、キューが補充されるまで待つ
  std::unique_lock<std::mutex> lock(sio_mutex);
  while (sio_queue.empty()) {
    sio_cond.wait(lock);
  }

  // イベントキューから登録されたデータを取り出す
  sio::message::ptr recv_data(sio_queue.front());

  <取り出したデータに対応して何らかの処理をする>

  // 処理が終わったイベントをキューから取り除く
  sio_queue.pop();
}

この方式ではサーバからの受信とキューの消化処理を平行して実行しません。 並行した処理が無いため、マルチスレッド由来のデッドロックは不安定な状態は発生しませんが、on_runで処理を行う場合に比べて性能が低いです。 作りたいアプリケーションの構造によって適宜構造を考えたほうが良いです。 今回はsioメインのサンプルなので一番安全そうな処理にしたつもりです。

JSONに対応するデータ構造

sioではJSONの値をsio::messageクラスを継承したクラス(sio:: string_messageなど)に格納しています。

C++で取り出すときは、

data->get_string();

値を作るときは

string_message::create("文字列"); // sio::message::ptrを戻す

のようにします。 src/sio_message.hで定義されており、まとめると以下のとおりです。

*JavaScriptの型 C++の型 sioでの対応クラス getter get_flagの値
Null nullptr, NULL - - -
Number(整数) int64_t int_message get_int() flag_integer
Number(小数、NaNなども?) double double_message get_double() flag_double
String std::string string_message get_string() flag_string
Object map<string,message::ptr> object_message get_map() flag_object
Array vector array_message get_vector() flag_array
Blob, ArrayBuffer, (File?) shared_ptr binary_message get_binary() flag_binary

nullの取り扱い

JavaScriptでオブジェクトにnullを設定した場合、C++側ではsio::message::ptrがnullptrを指しています。 そのため、型の判定は以下のようになります。

void print_type(sio::message::ptr data) {
    if (data.get() == nullptr) {
        std::cout << "null";
    } else {
        switch(data->get_field()) {
        case sio::message::flag_integer: std::cout << "integer"; break;
        case sio::message::flag_double: std::cout << "double"; break;
        case sio::message::flag_string: std::cout << "string"; break;
        case sio::message::flag_binary: std::cout << "binary(string)"; break;
        case sio::message::flag_array: std::cout << "array(vector)"; break;
        case sio::message::flag_object: std::cout << "object(map)"; break;
        }
    }
    std::cout << std::endl;
}

boolの取り扱い

bool値の取り扱いには対応していないようです。JavaScript側でbool値を設定した場合、C++側ではnullとなってしまいました。 既存プログラムでbool値がある場合は、プログラムを書き換えるか、Node.jsで判定して整数に変換してからクライアントに渡すなどの対応が必要です。

その他

  • httpsにはまだ対応していないです。現在実装中だそうです。

github.com

  • クライアントがネイティブだけでサーバに大したロジックが不要であればFlatBuffersを梱包して渡すのもありな気がします。ルーティング情報とバイナリ(FlatBuffers)だけを持ったパケットをNode.js+Redisで転送するだけとか。