色々使ってサーバサイドから情報を取り出す

やりたいこと

  • サーバ(Linuxならサーバじゃなくても良いけど)でデーモン的な何かが稼動している
  • サーバ側はC/C++で作っている
  • サーバの状態を外部からGUIで確認したい
  • できるだけ手間をかけずに実現したい

複雑な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ソースコード
LuaJavaScriptと違い、呼び出す関数や変数を先に定義しておかないと最初に使う場所で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

Future work?(やるとは言っていない)

今回は本当に「動くだけ」のモックアップ状態なので、例えば以下のような発展系が考えられる。

  • C言語とluaの間でもっといろいろな情報を共有する(排他制御などを駆使して)
  • クライアント側にJQueryとか何かライブラリを入れてもっと見た目をリッチにする

例えばsDashboardを使えば時系列データをいい感じに表示することもできると思う。


実際に作ってみて一番うれしかったことは、luaのライブラリが全部MIT系ライセンスでやさしかったことだったり。luarocksがライブラリのインストール時に対象ライブラリのライセンスを表示してくれるのもうれしかった。

*1:ともにライブラリを使わせてもらう

*2:/var/www/html配下とか