色々使ってサーバサイドから情報を取り出す
やりたいこと
複雑なGUIを作らないならHTMLでできそうな感じだったので、自然と通信はJSON形式に決まった。
JavaScriptで通信する場合、ざっと調べたところWebSocketがAPIで用意されているようだったので採用する方向で考えた。
ほかにもXmlHttpRequestがあるけど、サーバ側にサーブレットとかCGI的な仕組みが必要そうだったので却下した。
C/C++側をどうするか、だけど。JSONもWebSocketもライブラリが存在する。
ただ、ライブラリの使い方を調べて試行錯誤するのが大変。
ざざっと調べたらLuaのライブラリがあるみたいで、そっちは簡単に呼べそうだったのでLuaとくっつけてLuaのライブラリで通信を担当する方向とした。
ソフトウェア構成
アーキテクチャ図はこんな感じ。
環境構築
こちらで試したのは
Ubuntu 14.04 LTS
の環境。
パッケージのインストール
CDインストールなのか、Vagrantの特定Boxなのかで入っているパッケージが違うので、環境によってすでに入っているものがある。インストール済みの場合でも実行して問題なさそう。
$ sudo apt-get install libboost-dev
$ sudo apt-get install libboost1.54-all-dev
$ sudo apt-get install liblua5.2-dev
$ sudo apt-get install libluabind0.9.1
$ sudo apt-get install libluabind-dev
$ sudo apt-get install g++
$ sudo apt-get install checkinstall
$ sudo apt-get install liblua5.2-0
$ sudo apt-get install liblua5.2-dev
$ sudo apt-get install lua5.2
luarocksとlua用ライブラリのインストール
luaのパッケージ管理システムluarocksを入れる。
ただ、Ubuntu14.04にパッケージされているluarocksはlua5.1に対応したものになっている。
それなのに、luabindの方はlua5.2に依存している。
luarocksで入れたパッケージがlua5.1用ディレクトリにインストールされてしまい、luabindからみえなくなってしまうので自分でlua5.2用luarocksをインストールする。
luarocksはgitからソースをもらってくる。gitコマンドでもらってきてOK。
以下はWebから手動でzipをもらってきたパターン。
$ unzip luarocks-2.3.0.zip $ cd luarocks-2.3.0/ ~/luarocks-2.3.0$ ./configure --lua-version=5.2 --versioned-rocks-dir ~/luarocks-2.3.0$ make build ~/luarocks-2.3.0$ sudo checkinstall --pkgname=luarocks5.2 --pkgversion=2.3.0 --backup=no --deldoc=yes --fstrans=no --default $ sudo luarocks-5.2 install lua-websockets $ sudo luarocks-5.2 install lua-cjson
今回書いたソースコード
サーバ側
以下サーバ側ソース。C言語でデーモンのような何かがあるとする。
以下サンプル的に。
/* main.c */ #include <unistd.h> #include <math.h> void do_lua_thread(const char *filename); /* * Lua側と共有するグローバル変数 * 何かの現在地が刻々と変化しているのを模擬しているつもり */ int g_x; int g_y; int main(void) { unsigned int t = 0; /* Luaスレッド側でWebSocketサーバを動かす */ do_lua_thread("WebSocketServer.lua"); /* こちら側は適当に平行で動いておく */ while(1) { /* 浮動小数で計算後、切り捨て。本来は排他制御が必要 */ g_x = 100.0f * sin(3.14f * t / 10000.0f); g_y = 200.0f * cos(3.14f * t / 10000.0f); t++; /* 1msec寝る */ usleep(1000); } return 0; }
そしてluaとのI/F部分はC++で記述する。
イメージとしては、デーモンの大部分はC言語で書かれていて、そこにluaとのI/FをC++で付け足すイメージ。
今回luaを別スレッドで動かす形式にした。
// LuaThread.cpp #include <iostream> #include <string> #include <boost/thread.hpp> #include <boost/bind.hpp> #include <boost/format.hpp> #include <boost/shared_ptr.hpp> using namespace boost; #include <lua.hpp> #include <luabind/luabind.hpp> extern "C" { // C言語側に用意されているグローバル変数 extern int g_x; extern int g_y; } namespace { void get_pos(lua_State *ls) { // 変数値をそのまま戻り値へ入れる。本来は排他制御つきの読み書き関数を経由すべき lua_pushnumber(ls, g_x); lua_pushnumber(ls, g_y); } int lua_error_handler(lua_State* ls) { lua_Debug debug; std::string msg; // Luaスタックに積まれているエラーメッセージを取得する std::string err_str = lua_tostring(ls, -1); msg = str(boost::format("lua error: %s\n") % err_str); msg += "backtrace:\n"; for(int stack = 1; lua_getstack(ls, stack, &debug); stack++) { // http://www.fxcodebase.com/documents/IndicoreSDK/lua/lua_getinfo.html // n : name and namewhat // S : source, linedefined, lastlinedefined, what, and short_src // l : currentline lua_getinfo(ls, "Sln", &debug); msg += str( boost::format("#%d %s(@ %s) line:%d in %s line:%d-%d\n") % stack % debug.name % debug.namewhat % debug.currentline % debug.short_src % debug.linedefined % debug.lastlinedefined ); } // もともとLuaスタックに積まれていたエラーメッセージを // 今作った新しいエラー文字列に置き換える lua_pop(ls, 1); lua_pushstring(ls, msg.c_str()); std::cerr << msg << std::endl; return 1; } void thread_main(const std::string &script_filename) { lua_State *ls = luaL_newstate(); luaL_openlibs(ls); luabind::open(ls); luabind::module(ls) [ luabind::def("get_pos", get_pos) ]; luabind::set_pcall_callback(lua_error_handler); if(luaL_dofile(ls, script_filename.c_str())) { std::cerr << str( boost::format("running %s error:\n%s") % script_filename % lua_tostring(ls, -1) ) << std::endl; } lua_close(ls); } boost::shared_ptr<boost::thread> g_lua_thread; } // namespace extern "C" { void do_lua_thread(const char *filename) { // thread_main(filename) 状態でスレッド起動してもらう g_lua_thread = boost::shared_ptr<boost::thread>(new boost::thread(boost::bind(thread_main, filename))); } } // extern "C"
以下はluaのソースコード。
LuaはJavaScriptと違い、呼び出す関数や変数を先に定義しておかないと最初に使う場所でnull?として扱われる模様。言語ごとに細かい違いがあるのが面倒だねぇ。
地味に「!=」と書けないのもはまった。
それで、luaでは通信を受け持っている。WebSocketでの通信とJSONデータの読み取り、書き込みを担当する*1。
local cps = require "copas" local cj = require "cjson" local function onReceive(ws, msg) local x, y x, y = get_pos() -- C++側の関数を呼んでデータをもらう local value = {name = "resp", pos = {x, y}} -- とりあえずサンプル的なJSONデータ ws:send(cj.encode(value)) end local is_end = false local function onAccept(ws) is_end = false while is_end ~= true do local msg = ws:receive() if msg then onReceive(ws, msg) else ws:close() is_end = true end end end -- WebSocketサーバを作ってlisten開始する local server = require'websocket'.server.copas.listen { -- listen port port = 8002, -- サブプロトコル名「MySubProtocol」で新規接続時に onAccept() がコールバック protocols = { MySubProtocol = onAccept } } -- 後は copas にお願いする cps.loop()
クライアント側
以降はクライアント側のソース。
まずはHTMLファイル。
Webサーバが配信してくれる場所においておく*2。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>WebSocketデモ</title> <script type="text/javascript" src="ws-demo.js"></script> </head> <body onLoad="test()"> <!-- ページをロードしたときにテストデータを表示する --> <div id="test_view"></div><br/> <!-- データ出力場所 --> <button onClick="triggerOpen()">start</button> <button onClick="triggerStop()">stop</button> </body> </html>
そしてJavaScriptファイル。
// ws-demo.js function test() { // テスト関数 console.log("test"); var input = {name:"row", args:["col1", "col2"]}; view2table(input); // テストデータを表形式で表示してみる } // 引数inputを表形式で表示する function view2table(input) { var view = document.getElementById("test_view"); view.innerHTML = ""; var tbl = document.createElement("table"); tbl.style.border = "1px solid black"; // tbl.rurles = "all"; for(var r in input){ var row = tbl.insertRow(-1); var cell = row.insertCell(-1); cell.appendChild(document.createTextNode(r)); var obj = input[r]; if(obj instanceof Array){ for(var c = 0;c<obj.length;c++){ var cell = row.insertCell(-1); cell.appendChild(document.createTextNode(obj[c])); } }else{ var cell = row.insertCell(-1); cell.appendChild(document.createTextNode(obj)); } } view.appendChild(tbl); } var ws = null; var uri = "ws://192.168.33.10:8002"; var subpros = ["MySubProtocol"]; var timer = null; function triggerOpen() { if(ws != null) { return; } ws = new WebSocket(uri, subpros); ws.onopen = onOpen; ws.onmessage = onMessageReceive; ws.onclose = onClose; ws.onerror = onError; timer = setInterval(triggerSend, 1000); } function triggerStop() { if(timer == null) { return; } clearInterval(timer); timer = null; ws.close(); } function triggerSend() { if(ws == null) { return; } ws.send("{dummy: 0}"); // ダミーデータを送る } function onOpen(event) { console.log("onopen: " + event); } function onMessageReceive(event) { if (event && event.data) { view2table(JSON.parse(event.data)); // 受信データを表形式で表示する } } function onClose(event) { console.log("onclose"); ws = null; } function onError(event) { console.error(event); console.trace(); }
ソースコードのビルド
サーバ側ソースのコンパイル。Vagrant環境だとメモリ不足になるのでVMのメモリを2GBに設定する。
$ gcc -c main.c $ g++ -I /usr/include/lua5.2 -c LuaThread.cpp $ g++ *.o -llua5.2 -lluabind -lboost_thread -lboost_system -lstdc++
参考URL
この記事を書くにあたって、各サイト様を参考にさせてもらいました。
ありがとうございました。
JavaScript
WebSocket APIの呼び出しコード例を書かれている
http://qiita.com/tnakagawa/items/f7c764d044ba56d9e0fd
lua関連
lua-websockets(GitHub)
サンプルコードがある
https://github.com/lipp/lua-websockets
lua-cjson(GitHub)
https://github.com/efelix/lua-cjson
lua5.2用luarocksのビルド方法を参考にしました
http://stackoverflow.com/questions/20321560/how-do-install-libraries-for-both-lua5-2-and-5-1-using-luarocks
luabindのコード例を参考にしました
http://d.hatena.ne.jp/androcoffee/20110719/1311088086
こちらのエラーハンドラを大変参考にさせてもらいました
http://staffblog.yumemi.jp/%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E8%A8%80%E8%AA%9E-c-%E3%81%A8%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E8%A8%80%E8%AA%9E-lua-%E3%82%92%E3%80%81luabind-%E3%82%92%E4%BD%BF%E3%81%A3-2/