[C/C++]libwebsocketsを試してみる。その2。

libwebsocketsその2。

C実装WebSocketsであるlibwebsockets続き。

libwebsocketsを解凍してできるtest-serverフォルダのtest-sever.cを参考に、チャットサーバーを作成する。
尚、C++でコーディングする。

・実行環境
Ubuntu 12.04 LTS
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
libwebsockets 1.2

サンプルサーバーの解析
test-server.cは3つのサーバー機能を有している。WebSocketsはサブプロトコルという機構を用意していて、これを指定することで同じ待ち受けポートで共有して使用することができる。

test-server.cのプロトコル定義

static struct libwebsocket_protocols protocols[] = {
	/* first protocol must always be HTTP handler */

	{
		"http-only",		/* name */
		callback_http,		/* callback */
		0,			/* per_session_data_size */
		0,			/* max frame size / rx buffer */
	},
	{
		"dumb-increment-protocol",
		callback_dumb_increment,
		sizeof(struct per_session_data__dumb_increment),
		10,
	},
	{
		"lws-mirror-protocol",
		callback_lws_mirror,
		sizeof(struct per_session_data__lws_mirror),
		128,
	},
	{ NULL, NULL, 0, 0 } /* terminator */
};

仕組みとしては、libwebsocket_protocolsの1つ目のパラメータにプロトコルとなる文字列を指定し、それを指定してアクセスしてきたクライアントに2つ目のパラメータのコールバック関数が実行される仕組み。

test-server(libwebsockets-test-server)はhttpでアクセスし、まず、htmlファイルを返し、その中に記述されているWebSocketsが動き出す仕組み。
html内には”dumb-increment-protocol”と”lws-mirror-protocol”の2つのプロトコルあり、”dumb-increment-protocol”は接続されたクライアントに対し、サーバーが一方的にインクリメントされた数値を返すというもの。”dumb-increment-protocol”はサーバーとクライアントが1対1に動作する。”lws-mirror-protocol”はhtml内のキャンバスエリアにユーザーが描写した線を接続された全てのクライアントに送信するというもの。”lws-mirror-protocol”は1対nの動作になる。

チャットサーバーは”lws-mirror-protocol”を参考に、ユーザーから受け取った文字列を接続中の全てのユーザーに送信する処理を行う。

mainの処理
test-server.cから不必要なものを取り除く。

main()

int main()
{
	int n = 0;
	int use_ssl = 0;
	struct libwebsocket_context *context;
	int opts = 0;
	char interface_name[128] = "";
	const char *iface = NULL;
	int syslog_options = LOG_PID | LOG_PERROR;
	unsigned int oldus = 0;
	struct lws_context_creation_info info;
	int debug_level = 7;
	
	memset(&info, 0, sizeof info);
	info.port = 7681;
	
	lws_set_log_level(debug_level, lwsl_emit_syslog);
	lwsl_notice("libwebsockets chat server -\n");

	info.iface = iface;
	info.protocols = protocols;
	info.ssl_cert_filepath = NULL;
	info.ssl_private_key_filepath = NULL;
	
	info.gid = -1;
	info.uid = -1;
	info.options = opts;
	
	signal(SIGINT, sighandler);
	
	setlogmask(LOG_UPTO (LOG_DEBUG));
	openlog("lwsts", syslog_options, LOG_DAEMON);
	
	context = libwebsocket_create_context(&info);
	if (context == NULL) {
		lwsl_err("libwebsocket init failed\n");
		return -1;
	}
	
	n = 0;
	while (n >= 0 && !force_exit) {
		struct timeval tv;

		gettimeofday(&tv, NULL);

 		n = libwebsocket_service(context, 50);
	}

	libwebsocket_context_destroy(context);

	lwsl_notice("libwebsockets-test-server exited cleanly\n");

	return 0;
}

プロトコル定義

static struct libwebsocket_protocols protocols[] = {
	{
		"chat",		/* name */
		callback_chat,		/* callback */
		0,			/* per_session_data_size */
		128			/* max frame size / rx buffer */
	},
	{ NULL, NULL, 0, 0 } /* terminator */
};

グローバル変数とコールバック関数(callback_chat)

static string _sendStr = "";

static int callback_chat(struct libwebsocket_context *context,
		struct libwebsocket *wsi,
		enum libwebsocket_callback_reasons reason, void *user,
							   void *in, size_t len)
{
	lwsl_notice("%s\n", reason_strings[reason]);
	
	switch (reason) 
	{
		// 新規接続
		case LWS_CALLBACK_ESTABLISHED:
			{
			}
			break;
		// クローズ
		case LWS_CALLBACK_PROTOCOL_DESTROY:
			{
			}
			break;
		// 送信処理
		case LWS_CALLBACK_SERVER_WRITEABLE:
			{
				libwebsocket_write(wsi, (unsigned char*)_sendStr.c_str(), _sendStr.length(), LWS_WRITE_TEXT);
			}
			break;
		// 受信処理
		case LWS_CALLBACK_RECEIVE:
			{
				lwsl_notice("ReceiveMessage=[%s]\n",(const char*)in);
				_sendStr = (const char*)in;
				libwebsocket_callback_on_writable_all_protocol(libwebsockets_get_protocol(wsi));
			}
			break;
		// えーと、えーと、
		case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
			{
				dump_handshake_info(wsi);
			}
			break;
		
		default:
			{
			}
			break;
	}

	return 0;
}

main()ではlibwebsocket_context型のインスタンスを作成し、libwebsocket_create_context関数でContextを作成し、libwebsocket_service関数を実行するというものである。今回定周期処理はないのでループ内で処理する動作はないのだが、よくわからんので、test-serverと同じようにした。

コールバック関数はライブラリ側が勝手にイベントを発行してくれるので、それに応じた処理を書けば良い。パラメータuserは1セッションでユーザーが管理できるデータで、パラメータinがクライアントからのデータ。
チャットサーバーではLWS_CALLBACK_RECEIVEイベント発生時、libwebsocket_callback_on_writable_all_protocol関数で接続中の全クライアントに送信処理を行う。送信処理はLWS_CALLBACK_SERVER_WRITEABLEイベントになるのだが、LWS_CALLBACK_SERVER_WRITEABLEイベント発生時、パラメータinはnullになってしまう。そのため、受信イベント(LWS_CALLBACK_RECEIVE)時にグローバル変数(_sendStr)に保持し、送信時のlibwebsocket_write関数にグローバル変数を指定している。サンプルソース(test-server.c)ではリングバッファによって管理していたので、C++ではSTLのqueueあたりで管理するのが良いのだろう。

新規接続のセッションを保持して管理する方法も可。
std::vectorを使用してLWS_CALLBACK_ESTABLISHEDイベントでセッションを保持すれば受信イベント時に送信してしまうことも可能。セッション管理を以前作成したサーバーと同じようにユニークに管理出来れば、特定のユーザーにのみ送信する処理が書けそう。

vectorで管理

static vector<libwebsocket*> _ws;
static int callback_chat(struct libwebsocket_context *context,
		struct libwebsocket *wsi,
		enum libwebsocket_callback_reasons reason, void *user,
							   void *in, size_t len)
{
	lwsl_notice("%s\n", reason_strings[reason]);
	
	switch (reason) 
	{
		case LWS_CALLBACK_ESTABLISHED:
			{
				_ws.push_back(wsi);
			}
			break;
		case LWS_CALLBACK_RECEIVE:
			{
				lwsl_notice("ReceiveMessage=[%s]\n",(const char*)in);
				_sendStr = (const char*)in;
				//libwebsocket_callback_on_writable_all_protocol(libwebsockets_get_protocol(wsi));
				
				for(int i=0;i<_ws.size();i++)
				{
					libwebsocket_write(_ws[i], (unsigned char*)_sendStr.c_str(), _sendStr.length(), LWS_WRITE_TEXT);
				}
				
			}
			break;
        /// (略)

コンパイル
libwebsocketsインストール済み環境でg++でコンパイル。

$ g++ cppserver.cpp  -lwebsockets -o cppserver

サーバーの実行
libtoolとかやっていないので、LD_LIBRARY_PATHを通してから実行。

$export LD_LIBRARY_PATH="/usr/local/lib"
$ ./cppserver
lwsts[18632]: Initial logging level 7
lwsts[18632]: Library version: 1.2
lwsts[18632]:  Started with daemon pid 0
lwsts[18632]:  static allocation: 5460 + (12 x 1024 fds) = 17748 bytes
lwsts[18632]:  canonical_hostname = ubuntu
lwsts[18632]:  Compiled without SSL support
lwsts[18632]:  per-conn mem: 172 + 1328 headers + protocol rx buf
lwsts[18632]: LWS_CALLBACK_ADD_POLL_FD
lwsts[18632]:  Listening on port 7681
lwsts[18632]: LWS_CALLBACK_PROTOCOL_INIT

クライアント
今回はC#(WebSocket4Net)で接続テストを実施。
WebSocket4Netでソケットを接続するときにサブプロトコルを指定する。今回はプロトコルに単に”chat”と文字列を指定するだけ。

var ws = new WebSocket("ws://192.168.1.22:7681","chat");

実行画面

今回作成したソースはこちら