[C#]Microsoft Expression Encoderを使用して簡易画面キャプチャソフトを作る、その4。

簡易キャプチャソフト改良。
Formクラスの枠を使ってキャプチャ範囲を指定するという手抜きソフトですが、意外と使いやすいというwww
Img20130228204806

改良点
◇動画録画中途中キャンセル可
 これは、録画中に録画停止ボタン(右上)を右クリック、左クリック同時押しでその録画をキャンセルできるというもの。
◇録画範囲のエラーチェック
 キャプチャ対象エリアをディスプレイ外に指定された場合に強制終了する不具合をエラーを表示させるように修正。
◇キャプチャサイズ保持
 最後に使用した時のキャプチャサイズを設定ファイルに保持できるように修正。
◇簡易エンコーダー(EasyEncoder)同梱
 このキャプチャソフトは録画ファイルを00.xesc~99.xescまで作成するが、簡易エンコーダー(EasyEncoder)はこのファイルをWMVエンコードするソフト。通常は録画停止時にエンコードするが、されなかった場合用。
xescファイルと同ディレクトリ内で実行するか、xescファイルのディレクトリをコマンドライン引数で指定することでwmvへエンコードすることができる。
◇操作パネル小さくした
◇動画録画時、音声の録画とマウスカーソルのキャプチャありなしをコントロールに表示させるようにした。

既知のバグ
◇動画録画時の一時停止について
 試用版のExpression Encoderは録画開始時から10分経つと一時停止中でも構わず停止する。そのため、一時停止中に10分経ってしまい自動停止された後に新規JOBが開始され録画が始まってしまうという現象。そこまでして一時停止にこだわらないので今回はメッセージを表示して録画を停止してしまうようにした。
◇キャプチャフォームの大きさ変更不可問題
 これはキャプチャフォームの大きさを変更しているとあるところから変更できなくなる(Formの境界線に変更矢印が表示されなくなる)という致命的な問題。キャプチャフォームは録画中はWndProc()で移動不可、大きさ変更不可にしているのだが、この現象はわかんない状態。
◇設定ファイルバッティング問題
 このソフトは複数起動が可能なのだが、アプリケーション終了時に設定ファイルに設定データを保存している。なので、どっかでファイルIOがバッティングするとエラーになる。まぁ、しょうがない。

今後の改良
◇全画面キャプチャ
 全画面を対象としたキャプチャ。Expression EncoderはHD画質での録画も可能なのでWXGAのモニタ全画面もおk。
◇Expression Encoderのプリセット選択
 Expression Encoderには豊富なプリセットが用意されていてプログラム的に指定が可能だったりする。

プリセットの種類
Expression Bumper
Silverlight トレーラー
VC-1 256k DSL CBR
VC-1 256k DSL VBR
VC-1 512k DSL CBR
VC-1 512k DSL VBR
VC-1 HD 1080p VBR
VC-1 HD 720p VBR
VC-1 IIS スムーズ ストリーミング – 720p CBR
VC-1 IIS スムーズ ストリーミング – HD 1080p VBR
VC-1 IIS スムーズ ストリーミング – HD 420p VBR
VC-1 IIS スムーズ ストリーミング – SD 480p VBR
VC-1 IIS スムーズ ストリーミング – 画面エンコード VBR
VC-1 Windows Mobile
VC-1 Xbox 360 HD 1080p
VC-1 Xbox 360 HD 720p
VC-1 Zune 1
VC-1 Zune 2
VC-1 Zune 2 (AV ドック再生)
VC-1 Zune HD
VC-1 Zune HD (AV ドック再生)
VC-1 ブロードバンド CBR
VC-1 ブロードバンド VBR
VC-1 モーション サムネイル VBR
VC-1 画面エンコード VBR
VC-1 高速ブロードバンド CBR
VC-1 高速ブロードバンド VBR
WMA Lossless 5.1 オーディオ
WMA 音声オーディオ
WMA 高品質オーディオ
WMA 最良 VBR
WMA 低品質オーディオ
WMA 良質オーディオ
サンプルのオーバーレイ
ソース エンコード設定の適用
バランス
最速
最良

エンコード実施時のプリセットの指定

var job = new Job();
job.MediaItems.Add(item);

// プリセットの設定
job.ApplyPreset(Presets.AACGoodQualityAudio);

// エンコードの開始
job.Encode();

プリセットを指定することで出力されるファイルがそれぞれに応じた音声コーデック、画面の大きさに変更されるようになる。

Expression Encoder4自体のソフトを見ているとかなりカスタマイズが可能なようなので、プログラムレベルでこれらが指定できるのは非常にありがたい。うーん、本当に開発が終わってしまうのが惜しい。
Img20130228205755

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

[C/C++]C++でMessagePackを試す。

バイナリデータの扱いが難しかった。
C++のMessagePackライブラリは自作クラスのシリアル化が可能ですので、あるクラスのシリアル化を考えます。

InterfaceClassの定義
    int cmd;     // 数値
    string guid; // 文字列
    string time; // 文字列
    char[] data; // バイト配列

変数dataにはバイナリ(1バイト型の配列)を保持させたいのですが、ちょっとよくわからなくて、C#のように簡単にシリアライズすることはなりませんでした。もうちょっと調べてみないとよくわかりませんー。

配列を作成しデータを作成。
以前、python版のMessagePackを使用した時のように、配列を作成した上で、配列内に様々なデータ型を挿入することにしました。

InterfarceClassのシリアライズ

    // 対象データ
    int cmd = 0x10;
    string guid = "f65a9f53-15dc-48f4-b0f2-eac3f96df93a";
    string time = "20130226192100";
    
    printf("pack   cmd[%d],guid[%s],time[%s]\n", cmd, guid.c_str(), time.c_str());
    
    vector<char> rData;
    ReadAllBytes(IMAGEFILE_A, rData);
    
    
    msgpack::sbuffer buffer;
    msgpack::packer<msgpack::sbuffer> pk(&buffer);
    
    pk.pack_array(4);           // データが4つ
    pk.pack(cmd);
    pk.pack(guid);
    pk.pack(time);
    pk.pack_raw(rData.size());  // バイナリを入れる処理
    pk.pack_raw_body(&rData[0], rData.size());

    WriteAllBytes(MSGPACKFILE, buffer.data(), buffer.size());

pk.pack_array(4)の処理がないと配列型として認識されません。これは重要で、クラスをシリアル化した結果と同じになります。ちなみに、配列はデータ型と認識されるので、最終的には1つのデータになります。

デシリアライズの処理。
シリアル化したデータから元のデータを取り出すのも手動でやってみます。MessagePackの実装部分のソースを調べないとわからない部分がたくさんあった。

InterfarceClassのデシリアライズ

    vector<char> packData;
    
    // MessagePackデータ読み取り
    ReadAllBytes(MSGPACKFILE, packData);
    
    msgpack::unpacked msg;
    msgpack::unpack(&msg, &packData[0], packData.size());
    
    msgpack::object obj = msg.get();
    
    // msgapck::objectの配列を取り出す
    msgpack::object_array obj_array = obj.via.array;
    
    // デシリアライズ対象データ
    int cmd;
    string guid, time;
    
    // それぞれの型に変換して取り出す
    (obj_array.ptr[0]).convert(&cmd);
    (obj_array.ptr[1]).convert(&guid);
    (obj_array.ptr[2]).convert(&time);
   
    printf("unpack cmd[%d],guid[%s],time[%s]\n", cmd, guid.c_str(), time.c_str());
    
    // バイナリの取り出し
    msgpack::object_raw obj_raw = (obj_array.ptr[3]).via.raw;
    
    // バイナリの書き出し
    WriteAllBytes(IMAGEFILE_B, obj_raw.ptr, obj_raw.size);

正直、やりかた合っているかわからないですが、C#で同様の処理を作成し、C++版でシリアル化したデータが無事デシリアライズできたので、たぶん大丈夫だと思います。
ちょっと残念なのは、例えばコマンドによって専用のpack,unpackの処理をコーディングしないといけないところでしょうか。ちゃんとやり方があるんだろうなぁ。。。

C#での例。
以前作成したObject2MessagePack()の拡張メソッドを使うと以下のようにかけます。

    public class MyInterface
    {
        public int _0CMD { get; set; }
        public string _1GUID { get; set; }
        public string _2TIME { get; set; }
        public byte[] _3DATA { get; set; }
    }
    static void Main(string[] args)
    {
        var m = new MyInterface();
           
        m._0CMD = 0x10;
        m._1GUID = "f65a9f53-15dc-48f4-b0f2-eac3f96df93a";
        m._2TIME = "20130226192100";
        m._3DATA = new byte[] { 1, 2, 3 };
     
        System.IO.File.WriteAllBytes("csharp_MsgPack.bin", m.Object2MessagePack());
    }

C++で同じデータ作成します。

void pack2()
{
    // 対象データ
    int cmd = 0x10;
    string guid = "f65a9f53-15dc-48f4-b0f2-eac3f96df93a";
    string time = "20130226192100";
    
    printf("pack   cmd[%d],guid[%s],time[%s]\n", cmd, guid.c_str(), time.c_str());
    
    char data[] = {1,2,3};
    
    msgpack::sbuffer buffer;
    msgpack::packer<msgpack::sbuffer> pk(&buffer);
    
    pk.pack_array(4);           // データが4つ
    pk.pack(cmd);
    pk.pack(guid);
    pk.pack(time);
    pk.pack_raw(sizeof(data));  // バイナリを入れる処理
    pk.pack_raw_body(&data[0], sizeof(data));
    
    printf("size =%d\n", sizeof(data));
    
    WriteAllBytes("cpp_MsgPack.bin", buffer.data(), buffer.size());
}

シリアライズしたデータをダンプしてみます。
Img20130228132404

テストソース
InterfaceClassに合うようにMessagePackデータを作成し、pack,unpackを試してみました。
バイト配列には画像データを読み込み、処理終了後、元画像ファイルとデシリアライズ後出力した画像ファイルのmd5ハッシュ値を表示させ同じデータであるかを確認します。

テストソース(msgpack_main.cpp)

#include <msgpack.hpp>
#include <vector>
#include <string>
#include <iostream>
#include <fstream>

using namespace std;

// ファイル読み込み
int ReadAllBytes(const string& path,vector<char>& buf)
{
	ifstream fin(path.c_str(), ios::in | ios::binary );
	
	if(!fin)
	{
		return 1;
	}
	
	char c;
	for(;;)
	{
		fin.read(&c,sizeof(char));
		
		if(!fin.eof()) buf.push_back(c);
		else break;
	}
	fin.close();
	
	
	return 0;
}

// ファイル書き込み
int WriteAllBytes(const string& path,const  char* buf, int size)
{
	ofstream ofs(path.c_str(), ios::out | ios::binary | ios::trunc );
	
	if(!ofs)
	{
		return 1;
	}
	
    ofs.write(buf,size);
	ofs.close();
	
	
	return 0;
}

// md5の表示
void DispMd5(const string& fileName)
{
    FILE *pipe;
    
    char buf[1024]={0};
    string cmd = "md5sum " + fileName;
    
    pipe = popen(cmd.c_str(), "r");
    if( pipe )
    {
        fread(buf, sizeof(char), sizeof(buf), pipe);
        pclose(pipe);
    }
    
    printf("MD5> %s",buf);
}



// MessagePackデータ
const char* MSGPACKFILE = "MsgPack.bin";
const char* IMAGEFILE_A = "icon.png";
const char* IMAGEFILE_B = "_icon.png";

// シリアライズ
void pack()
{
    // 対象データ
    int cmd = 0x10;
    string guid = "f65a9f53-15dc-48f4-b0f2-eac3f96df93a";
    string time = "20130226192100";
    
    printf("pack   cmd[%d],guid[%s],time[%s]\n", cmd, guid.c_str(), time.c_str());
    
    vector<char> rData;
    ReadAllBytes(IMAGEFILE_A, rData);
    
    
    msgpack::sbuffer buffer;
    msgpack::packer<msgpack::sbuffer> pk(&buffer);
    
    pk.pack_array(4);           // データが4つ
    pk.pack(cmd);
    pk.pack(guid);
    pk.pack(time);
    pk.pack_raw(rData.size());  // バイナリを入れる処理
    pk.pack_raw_body(&rData[0], rData.size());
    
    WriteAllBytes(MSGPACKFILE, buffer.data(), buffer.size());
}

// デシリアライズ
void unpack()
{
    vector<char> packData;
    
    // MessagePackデータ読み取り
    ReadAllBytes(MSGPACKFILE, packData);
    
    msgpack::unpacked msg;
    msgpack::unpack(&msg, &packData[0], packData.size());
    
    msgpack::object obj = msg.get();
    
    // msgapck::objectの配列を取り出す
    msgpack::object_array obj_array = obj.via.array;
    
    // デシリアライズ対象データ
    int cmd;
    string guid, time;
    
    // それぞれの型に変換して取り出す
    (obj_array.ptr[0]).convert(&cmd);
    (obj_array.ptr[1]).convert(&guid);
    (obj_array.ptr[2]).convert(&time);
   
    printf("unpack cmd[%d],guid[%s],time[%s]\n", cmd, guid.c_str(), time.c_str());
    
    // バイナリの取り出し
    msgpack::object_raw obj_raw = (obj_array.ptr[3]).via.raw;
    
    // バイナリの書き出し
    WriteAllBytes(IMAGEFILE_B, obj_raw.ptr, obj_raw.size);
}



// interfaces
    //  int cmd
    //  string guid
    //  string time
    //  char[] data
int main()
{
    pack();
    unpack();
    
    printf("\n");
    
    DispMd5(IMAGEFILE_A);
    DispMd5(IMAGEFILE_B);
    
    return 0;
}

実行結果

$ ./msgpack_main
pack   cmd[16],guid[f65a9f53-15dc-48f4-b0f2-eac3f96df93a],time[20130226192100]
unpack cmd[16],guid[f65a9f53-15dc-48f4-b0f2-eac3f96df93a],time[20130226192100]

MD5> 5b2eb4f3de00f5d7477faa850514f641  icon.png
MD5> 5b2eb4f3de00f5d7477faa850514f641  _icon.png

[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");

実行画面

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

[C#]WebSocketを試してみる。サーバーへの自動再接続機能を実装するの巻。

引き続きWebSocket。

クライアント側のサーバーから切断されてしまった時の再接続機能をさくっと作ってみた。
基本的にはクローズイベントかエラー発生のイベントで再接続をすれば良い。ただ、APIによって仕様が違ったりする。将来的には実装されるAPIまで標準化されることでしょう。

ここでは画面上にサーバーが送信してくる時刻を常に表示し続けるクライアントを作成します。
画面Img20130218153954

C# – WCF WebSockets版

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.ServiceModel.WebSockets;

namespace WsForm_WCF
{
    public partial class Form1 : Form
    {

        /// <summary>
        /// WebSocketsインスタンス
        /// </summary>
        private WebSocket _ws = null;

        /// <summary>
        /// リトライカウンタ
        /// </summary>
        private int _retryCount = 0;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            /// サーバー接続開始
            Connect();
        }

        /// <summary>
        /// サーバーへ接続する
        /// </summary>
        private void Connect()
        {

            /// listBoxへ文字列を挿入する。
            Action<string> AddText = (s) =>
                {
                    listBox1.Items.Add(s);
                    listBox1.TopIndex = listBox1.Items.Count - 1;
                };

            if (_ws == null)
            {
                AddText("サーバー接続を開始します。");
            }
            _ws = new WebSocket("ws://192.168.1.2:12345");

            /// 文字列受信
            _ws.OnData += (s, e) =>
            {
                AddText(e.TextData);
            };

            /// サーバー接続完了
            _ws.OnOpen += (s, e) =>
            {
                _retryCount = 0;
                AddText("サーバーに接続しました。");
            };

            /// 接続断の発生
            _ws.OnClose += (s, e) =>
            {
                AddText("サーバー接続中..リトライ" + (++_retryCount).ToString() + "回目");

                /// 再接続を試行する
                Connect();
            };

            /// サーバー接続開始
            _ws.Open();

        }
    }
}

C# – WebSocket4Net版

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using WebSocket4Net;

namespace WsForm_WebSocket4Net
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// WebSocketsインスタンス
        /// </summary>
        private WebSocket _ws = null;

        /// <summary>
        /// リトライカウンタ
        /// </summary>
        private int _retryCount = 0;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            /// サーバー接続開始
            Connect();
        }

        /// <summary>
        /// サーバーへ接続する
        /// </summary>
        private void Connect()
        {

            /// listBoxへ文字列を挿入する。
            Action<string> AddText = (s) =>
            {
                this.Invoke(new Action(() =>
                    {
                        listBox1.Items.Add(s);
                        listBox1.TopIndex = listBox1.Items.Count - 1;
                    }));
            };

            if (_ws == null)
            {
                AddText("サーバー接続を開始します。");
            }
            _ws = new WebSocket("ws://192.168.1.2:12345");

            /// 文字列受信
            _ws.MessageReceived += (s, e) =>
            {
                AddText(e.Message);
            };

            /// サーバー接続完了
            _ws.Opened += (s, e) =>
            {
                _retryCount = 0;
                AddText("サーバーに接続しました。");
            };

            /// 接続断の発生
            _ws.Error += (s,e)=>
            {
                AddText("サーバー接続中..リトライ" + (++_retryCount).ToString() + "回目");
                /// 再接続を試行する
                Connect();
            };

            /// 接続断の発生
            _ws.Closed += (s, e) =>
            {
                AddText("サーバー接続中..リトライ" + (++_retryCount).ToString() + "回目");
                /// 再接続を試行する
                Connect();
            };

            /// サーバー接続開始
            _ws.Open();

        }
    }
}

WCF WebSocketsとWebSocket4Netの違いはほぼないものの、エラーハンドリングがあるだけWebSocket4Netはちょっと分かりやすいかもしれません。
WCF WebSocketsのエラー内容はOnClose()のsenderから得ることができます。

            ws.OnClose += (s, e) =>
            {
                Console.WriteLine("Last Error:{0}.", ((WebSocket)s).LastError.ToString());
            };

実行画面

気になったのはエラー発生後、サーバーへの接続を行うsleepは指定していませんが、どれも1秒おきくらいにやっているように見えることです。ちなみに、ブラウザ(Javascript版)では実行環境の違いによって試行までの時間(他のPC環境では10秒おきくらい)が違ったりしました。

おまけ
JavaScript版

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>WebSocket Sample</title>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>WebSocket アプリケーション</h1>
            </div>
        </header>
		<div id="log"></div>
		<script type="text/javascript">
			var ws;
			var retryCount = 0;
			var logCount = 0;
			var connect = function(){
				
				if ("WebSocket" in window) {
					ws = new WebSocket("ws://192.168.1.2:12345");
				} else if ("MozWebSocket" in window) {
					ws = new MozWebSocket("ws://192.168.1.2:12345");
				}

				ws.onmessage = function(event){
				    output(event.data);
				}
				
				if( retryCount > 0 ){
					output("サーバー接続中..リトライ" + retryCount.toString() + "回目");
				}

				
				ws.onopen = function(event){
				    output("サーバーに接続しました。");
				    retryCount = 0;
				}

				ws.onclose = function(event){
					retryCount++;
					connect();
				}

				function output(str){
					document.getElementById("log").innerHTML += str + "<hr />";
					logCount++;
					scroll(0,logCount*100);
				}

				function disconnect(){
					ws.close();
					ws = null;
				}
			}
			
			connect();
		</script>
        </section>
        <footer>
        </footer>
    </div>
</body>
</html>

[C#]WebSocketを試してみる。お手軽シンプルブロードキャストエコーサーバー作成の巻。

ブロードキャストエコーサーバー
WebSocketのテスト用。
送信時にちょっと工夫。ユーザーに送信する際にParallel.ForEach()を使用して並列化してみた。これを使うと複数ユーザーに送信する処理を並列化できる。例えば大きいデータなどを分割して送りたい処理などに有効。但し、リソースの消費量が大きくなるので注意が必要。Parallel.ForEach()はマルチコアCPUをフル活用するからパソコンのファンがブーンって鳴る。

並列化の効用。
複数の画像を各クライアントにプッシュ配信するサーバーの例。合計1.77MBの画像データを8KBパケットに分割して送信するとします。実行画面では1フォルダにつき1クライアントが動作しています。

以下、シンプルブロードキャストサーバーのソース。
サーバーソース

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using SuperSocket.Common;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketEngine;
using SuperWebSocket;

namespace WsEchoServer
{
    class Program
    {
        static void Main(string[] args)
        {
            /// WebSocket初期化
            var server = new SuperWebSocket.WebSocketServer();
            var rootConfig = new SuperSocket.SocketBase.Config.RootConfig();
            var serverConfig = new SuperSocket.SocketBase.Config.ServerConfig()
            {
                Port = 12345,
                Ip = "Any",
                MaxConnectionNumber = 100,       /// 最大ユーザセッション数
                Mode = SocketMode.Sync,
                Name = "Echo Server"
            };

            /// クライアントセッションを格納
            var clinet_sessions = new List<WebSocketSession>();

            /// セッションの接続
            server.NewSessionConnected += (s) =>
                {
                    Console.WriteLine("{0}:New Session:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());

                    /// 新クライアントとして格納
                    clinet_sessions.Add(s);
                };

            /// セッションのクローズ
            server.SessionClosed += (s, r) =>
                {
                    Console.WriteLine("{0}:Session Close:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());

                    /// 該当クライアントの除外
                    clinet_sessions.Remove(s);
                };

            /// データ受信(バイナリ)
            server.NewDataReceived += (s, d) =>
                {
                    Console.WriteLine("{0}:Binary Received:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());
                    Console.WriteLine("Length={0}", d.Length);
                    
                    /// 全ユーザに送信
                    Parallel.ForEach(clinet_sessions, p => p.SendResponse(d));
                };

            /// データ受信(文字列)
            server.NewMessageReceived += (s, m) =>
                {
                    Console.WriteLine("{0}:String Received:{1}",DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());
                    Console.WriteLine("Message={0}", m);

                    /// 全ユーザに送信
                    Parallel.ForEach(clinet_sessions, p => p.SendResponse(m));
                };


            /// サーバセットアップ
            server.Setup(rootConfig, serverConfig, SocketServerFactory.Instance);
            /// サーバ起動
            server.Start();

            Console.WriteLine("{0}:Server start.",DateTime.Now.ToString());
        }
    }
}

クライアントにはWebSocket4Netを使用。
これまでクライアントにはWCF WebSocketsを使っていたけど、SuperWebSocketsに含まれていたWebSocket4Netを使用してみる。
ほぼWCF WebSocketsと使用感は変わらない。

WebSocket4Net
http://websocket4net.codeplex.com/

クライアントソース

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using WebSocket4Net;

namespace WsClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var ws = new WebSocket("ws://192.168.1.2:12345");

            /// 文字列受信
            ws.MessageReceived += (s, e) =>
                {
                    Console.WriteLine("{0}:String Received:{1}", DateTime.Now.ToString(), e.Message);
                };

            /// バイナリ受信
            ws.DataReceived +=(s,e)=>
                {
                    Console.WriteLine("{0}:Binary Received Length:{1}", DateTime.Now.ToString(), e.Data.Length);
                };

            /// サーバ接続完了
            ws.Opened += (s, e) =>
            {
                Console.WriteLine("{0}:Server connected.", DateTime.Now.ToString());
            };

            /// サーバ接続開始
            ws.Open();

            /// 送受信ループ
            while (true)
            {
                var str = Console.ReadLine();
                if (str == "END") break;

                if (ws.State == WebSocketState.Open)
                {
                    ws.Send(str);
                }
                else
                {
                    Console.WriteLine("{0}:wait...", DateTime.Now.ToString());
                }
            }


            /// ソケットを閉じる
            ws.Close();

        }
    }
}

実行画面

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

[C/C++,Linux]sysinfo()で取得できるメモリ量についてメモ。

メモリの枚数に気をつける。
sysinfo()で取得できるメモリ量(totalramなど)は1枚あたりの量になるらしい。全体を求めるにはmem_unit分計算する必要がある。

C – sysinfo() returning bad values i686
http://stackoverflow.com/questions/4229415/c-sysinfo-returning-bad-values-i686

sysinfo(2) – Linux man page
http://linux.die.net/man/2/sysinfo

#include <stdio.h>
#include <linux/kernel.h>
#include <sys/sysinfo.h>

int main()
{
	struct sysinfo info;

	unsigned long totalram = 0;
	unsigned long freeram = 0;
	
	if( sysinfo(&info) != 0 )
	{
		return -1;
	}

	totalram = (info.totalram * info.mem_unit) / 1024;
	freeram = (info.freeram * info.mem_unit ) / 1024;
	
	printf("{\"val\":{\"totalram\":%ld,\"freeram\":%ld,\"useram\":%ld},\"err\":\"\"}",
		totalram,
		freeram,
		totalram - freeram);

    return 0;
	
}

VMのLinuxでテストしてから本物で動かすまで気づかなかった。これ、メモリ容量違うのが同時に刺さっていたらどうなるのだろうか。。。

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

C言語実装のWebSocketsライブラリ libwebsocketsを試す。
導入メモ。

(更新)2013/2/21での最新版に更新しました。

・実行環境
Ubuntu 12.04 LTS
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
autoconf (GNU Autoconf) 2.68
テストブラウザ Google Chrome v24 for win

・libwebsocketsのダウンロード
http://git.warmcat.com/cgi-bin/cgit/libwebsockets/

現時点での最新版Downloadの libwebsockets-master.tar.gz をダウンロードし、解凍する。

・autogen.shの実行

$./autogen.sh

・configureの実行

$ ./configure

・make & make installの実行

$ make
$ sudo make install

・テスト
test-serverというフォルダがあるので、この中のサーバーを実行します。

$ ./libwebsockets-test-server
lwsts[7584]: libwebsockets test server - (C) Copyright 2010-2013 Andy Green <andy@warmcat.com> - licensed under LGPL2.1
lwsts[7584]: Initial logging level 7
lwsts[7584]: Library version: 1.2 
lwsts[7584]:  Started with daemon pid 0
lwsts[7584]:  static allocation: 5460 + (12 x 1024 fds) = 17748 bytes
lwsts[7584]:  canonical_hostname = ubuntu
lwsts[7584]:  Compiled without SSL support
lwsts[7584]:  per-conn mem: 172 + 1328 headers + protocol rx buf
lwsts[7584]:  Listening on port 7681

ブラウザ(Chrome)でアクセスしてみます。
この環境の場合、http://192.168.1.22:7681 のようにhttpでアクセスします。
ウィンドウを2つ開き動作を確認します。下部のキャンバスエリアをなぞると、他のウィンドウと同期することがわかります。
Img20130221213716

[上原ひろみ]先輩ROCKYOU心ゆさぶれ 矢野顕子X清水ミチコ回。

矢野顕子さん、清水ミチコさん。
2/9の日テレ系先輩ROCKYOUは矢野顕子さんと清水ミチコさんがゲストでした。以前ひろみちゃんも出演したことのある番組。今回のゲストの2人はひろみちゃんと交友深い御二人ですね。

矢野さんが歌うのをイメージして書いたというひろみちゃん作曲の「Green Tea Farm」。
数年前の東京JAZZで二人で出たときがあって、GreenTeaFarmの演奏になったとき、矢野さんが歌詞を忘れてやり直すっていうシーンがありました(笑)。でも忘れられないのが、ひろみちゃんが伴奏しながら涙ぐんでいたシーンですね。あれは本当に幸せそうでした。
今ではこの御二人はライブをやったり、アルバムをリリースしたり、プライベートで食事したり、すごく仲の良い友達のようです♪

清水ミチコさんは矢野さん繋がりでひろみちゃんと交友がありますね。矢野さんのことが本当に好きなようで、普段はちょっとおちゃらけたイメージがありますが、初めて共演したとき、感動からか影で涙を拭っていたのをテレビでやっていました。清水ミチコさん宅には防音仕様の部屋にピアノが置いてあり、ひろみちゃんが弾きにくるそうです。そういえば清水ミチコさんのラジオでひろみちゃんがゲストで出演したとき、清水さんに当時流行していた楽しんごのドドスコスコスコラブ注入~を無理やりやらされていました、さすがです!

今回の番組には案の定ひろみちゃんの話題がちょっと上がりました。
このお二人のブログやtwitterに、たまにひろみちゃんが登場したりするので、要チェックですね♪

A9hOfebCUAAVWQE

[C#]WebSocketを試してみる。C#チャットサーバ作成の巻。

とうとうできたWebSocketのC#サーバ♪
WCFのWebSocketではサーバ側プログラムがどうにもこうにも動作できないでいたので、別のコンポーネントを試してみた。

SuperWebSocket, a .NET WebSocket Server
http://superwebsocket.codeplex.com/

こちらの方のサイトをちょっと参考。感謝感謝。
つくるよ
http://blog.livedoor.jp/tukuruyo/tag/SuperWebSocket

簡単な動作テストを行った所、動作が確認できたので、感動。でも、SuperWebSocketの機能をどうやって使用していいのかよくわかりません。全部英語だし。検索してみても使っている人はまだあまりいなそう。

Pythonのサーバをコーディングし直し。
以前作成したチャットソフトのサーバ側プログラムをフルスクラッチで作成。今までVMでPythonやっていたけど、VM起動する必要なくなった。

◇サーバプログラム・・・C#, SuperWebSocket
できること。
・Pythonで作ったサーバプログラムと同等の機能。
・ソケットのデータフォーマットにはMessagePackを使用。
・なんか、動いているのがわかるようなビューを作成。

◇クライアントプログラム・・・C#, WCF WebSockets
以前作成したチャットソフトと同じもの。

クライアントプログラム コーディング
サーバプログラムを作成するにあたり、共通部分についてはクラスライブラリ化した。なので、改修箇所はその部分のみ。WebSocketは規格なので、ほかにいじるところなし。

サーバプログラム コーディング
キモな部分だけ抽出。

ユーザソケットのセッション管理について
Pythonでコーディングした時、結構ややこしい処理になっていたところをちょと反省。ちゃんとデータクラスを作成して管理することに。
基本的に下記のSessionInfoクラスのリストをグローバルに置いて、各処理がユーザセッションにアクセスできるようにした。
WebSocketSession というのがユーザソケットのインスタンスになる。

        /// <summary>
        /// ユーザセッション管理クラス
        /// </summary>
        public class SessionInfo
        {
            /// <summary>
            /// WebSocketSession
            /// </summary>
            public WebSocketSession Session { get; set; }

            /// <summary>
            /// User GUID
            /// </summary>
            public string Guid { get; set; }

            /// <summary>
            /// UserData
            /// </summary>
            public Interfaces.UserInfo UserInfo { get; set; }

            public SessionInfo(WebSocketSession session, string guid, Interfaces.UserInfo userinfo)
            {
                Session = session;
                Guid = guid;
                UserInfo = userinfo;
            }
        }
        /// <summary>
        /// ユーザセッション管理
        /// </summary>
        private List<SessionInfo> _sessions = new List<SessionInfo>();

サーバ起動処理

                /// WebSocket初期化
                _server = new SuperWebSocket.WebSocketServer();
                var rootConfig = new SuperSocket.SocketBase.Config.RootConfig();
                var serverConfig = new SuperSocket.SocketBase.Config.ServerConfig()
                {
                    Port = int.Parse(textBoxPort.Text),
                    Ip = "Any",
                    MaxConnectionNumber = 100,       /// 最大ユーザセッション数
                    Mode = SocketMode.Sync,
                    Name = "Chat Server"
                };

                _server.NewSessionConnected += HNewSessionConnected;
                _server.SessionClosed += HCloseSession;
                _server.NewDataReceived += HReceiveMessage;
                _server.Setup(rootConfig, serverConfig,SocketServerFactory.Instance);
                /// サーバ起動
                _server.Start();

だいぶイミフwだけど、なかなかシンプル。
なんたらConfigに関してはほぼわからない。MaxConnectionNumberは試してみたらちゃんと同時接続数であることを確認できた。あとのパラメータはあとで調べる。

以下の部分がユーザ接続の新規接続、クローズ、受信部分のイベント。NewDataReceivedはバイナリデータ受信時、文字列はNewMessageReceivedになる。このチャットソフトはMessagePackを使用しているのでバイナリしか扱わない。

_server.NewSessionConnected += HNewSessionConnected;
_server.SessionClosed += HCloseSession;
_server.NewDataReceived += HReceiveMessage;

新規接続(NewSessionConnected)
今回のチャットソフトはソケットのセッション確立後、ユーザからログイン情報(GUID、名前、アイコン)を送信してくるので、今回ほぼ未使用。

ユーザセッションクローズ(SessionClosed)
クローズされたSessionを受け取ることができるので、自分で管理している情報にアクセスし、全ユーザに抜けたユーザ情報を送信できるような仕組みにした。

HCloseSession


        /// <summary>
        /// セッションクローズ要求
        /// </summary>
        /// <param name="session"></param>
        /// <param name="e"></param>
        private void HCloseSession(SuperWebSocket.WebSocketSession session, SuperSocket.SocketBase.CloseReason e)
        {
            Trace.WriteLine("HReceiveMessage");

            /// 該当ユーザのログアウトを知らせる
            RemoveSession(session);
        }

RemoveSession関数


        /// <summary>
        /// セッションのログアウト処理
        /// </summary>
        /// <param name="session"></param>
        private void RemoveSession(SuperWebSocket.WebSocketSession session)
        {
            /// 接続中のユーザ情報を検索
            var sessionInfo = _sessions.Where(p => p.Session == session);
            if (sessionInfo.Any())
            {
                var delSession = sessionInfo.First();

                /// 抜けたユーザ以外にユーザが抜けたことを知らせる。
                var sendIf = new Interfaces.CmdInfo();
                sendIf._1CMDNO = (int)Interfaces.COMMAND.CMDLOGOUT;
                sendIf._2GUID = delSession.Guid;
                sendIf._3TIME = DateTime.Now.ToString();
                SendMessage(session, sendIf, SENDTO.OTHER);

                AddLog(LOGCMD.SYSTEM, "ADMIN", delSession.Guid + " IS LOGOUT");

                _sessions.Remove(sessionInfo.First());

            }
            SetUserCount();
        }

ソケット受信処理
例によってbyte[]をMessagePackでクラスに変換するので、解析超楽。もう僕の中でパターン化しつつある。

HReceiveMessage

        /// <summary>
        /// 受信要求
        /// </summary>
        /// <param name="session"></param>
        /// <param name="message"></param>
        private void HReceiveMessage(SuperWebSocket.WebSocketSession session, byte[] message)
        {
            Trace.WriteLine("HReceiveMessage");

            /// コマンド解析
            var rcvIf = message.MessagePack2Object<Interfaces.CmdInfo>();
            
            /// 送信要求を行う処理を格納
            var sendIfs = new List<Action>();
            /// ブロードキャスト(全てのユーザ)に送信する必要あるならフラグ
            var bcastFlag = false;
            /// 送信要求パケット作成
            var sendIf = new Interfaces.CmdInfo();
            sendIf._1CMDNO = rcvIf._1CMDNO;
            sendIf._2GUID = rcvIf._2GUID;
            sendIf._3TIME = DateTime.Now.ToString();

            AddLog(LOGCMD.RECEIVE, sendIf._2GUID, Enum.GetName(typeof(Interfaces.COMMAND), (Interfaces.COMMAND)rcvIf._1CMDNO));

            switch ((Interfaces.COMMAND)rcvIf._1CMDNO)
            {
                /// ユーザログイン要求
                case Interfaces.COMMAND.CMDLOGIN:
                    {
                        /// 受信パケットからユーザデータ(Interfaces.UserInfo)を取り出す。
                        var newsession = new SessionInfo(session, rcvIf._2GUID, rcvIf._4SENDDATA.MessagePack2Object<Interfaces.UserInfo>());
                        _sessions.Add(newsession);

                        AddLog(LOGCMD.SYSTEM, "ADMIN", newsession.Guid + " IS LOGIN");

                        /// 新規ユーザに現在ログイン中のすべてのユーザ情報を送信
                        Action alluserinfo = new Action(() =>
                            {
                                var sendCmdIf = new Interfaces.CmdInfo();
                                sendCmdIf._1CMDNO = (int)Interfaces.COMMAND.CMDALLUSERINFO;
                                sendCmdIf._2GUID = "ADMIN";
                                sendCmdIf._3TIME = DateTime.Now.ToString();
                                var infos = new List<Interfaces.UserInfo>();
                                _sessions.ForEach(p => infos.Add(p.UserInfo));
                                sendCmdIf._4SENDDATA = CommonLibrary.Extensions.ExtensionsClass.Object2MessagePack1Serializer(infos.ToArray());
                                SendMessage(session, sendCmdIf, SENDTO.ONE);
                            });

                        /// 送信処理に追加
                        sendIfs.Add(alluserinfo);
                        /// 全員に知らせる
                        bcastFlag = true;
                    }
                    break;
                /// チャットメッセージ受信
                case Interfaces.COMMAND.CMDSENDSTRING:

                    var rcvmsg = rcvIf._4SENDDATA.MessagePack2Object<Interfaces.MessageInfo>();
                    
                    /// 送信先が空なら宛先全員
                    if (rcvmsg._1TOGUID == "")
                    {
                        bcastFlag = true;
                    }
                    /// 宛先が特定ユーザ
                    else
                    {
                        /// 接続中ユーザへのメッセージ送信処理
                        var toUserInfo = _sessions.Where(p => p.Guid == rcvmsg._1TOGUID);
                        if (toUserInfo.Any())
                        {
                            Action sendMsg = new Action(() =>
                                {
                                    sendIf._4SENDDATA = rcvIf._4SENDDATA;

                                    /// 送信先->送信元ユーザ
                                    SendMessage(session, sendIf, SENDTO.ONE);
                                    /// 送信先->特定ユーザ
                                    SendMessage(toUserInfo.First().Session, sendIf, SENDTO.ONE);
                                });
                            sendIfs.Add(sendMsg);
                        }

                        /// 他の人には送らない
                        bcastFlag = false;
                    }
                    break;
                /// その他のコマンド
                default:
                    {
                        /// ブロードキャスト
                        bcastFlag = true;
                    }
                    break;
            }

            /// 全員宛のパケット作成
            if (bcastFlag)
            {
                var sendAll = new Action(() =>
                    {
                        sendIf._4SENDDATA = rcvIf._4SENDDATA;
                        SendMessage(session, sendIf, SENDTO.ALL);   /// 送信先=全員
                    });
                sendIfs.Add(sendAll);
            }

            /// 送信処理
            sendIfs.ForEach(p => p.DynamicInvoke());

            SetUserCount();
        }

これでもPythonのサーバと同じ処理のつもり。。。WebSocketsはどんなタイミングでも送信できるから楽だなぁ。
SendMessage関数がデータをパックして送信する処理になる。

SendMessage関数

        /// <summary>
        /// 送信処理
        /// </summary>
        /// <param name="session"></param>
        /// <param name="cmdif"></param>
        /// <param name="sendto"></param>
        /// <param name="delSession"></param>
        private void SendMessage(SuperWebSocket.WebSocketSession session, Interfaces.CmdInfo cmdif,SENDTO sendto,bool delSession = true)
        {
            /// 異常セッション回収用
            var delsession = new List<SuperWebSocket.WebSocketSession>();

            /// ログ文字生成
            var str = string.Format("{0}  FROM {1}  SENDTO {2}",
                Enum.GetName(typeof(Interfaces.COMMAND),(Interfaces.COMMAND)cmdif._1CMDNO),
                cmdif._2GUID,
                Enum.GetName(typeof(SENDTO),sendto));

            AddLog(LOGCMD.SEND, "ADMIN", str);
                
            /// 各セッション宛に送信処理を行う
            for (int i = 0; i < _sessions.Count(); i++)
            {
                var current = _sessions[i];

                /// 送信先指定==自分自身,current != 自分以外 OR
                /// 送信先指定==自分以外,current == 自分  の場合は抜ける
                if ( ((sendto == SENDTO.ONE) && (current.Session != session)) ||
                     ((sendto == SENDTO.OTHER) && (current.Session == session))
                    )
                {
                    continue;
                }

                try
                {
                    /// これが送信処理
                    current.Session.SendResponse(cmdif.Object2MessagePack());
                }
                catch
                {
                    /// 異常セッション回収
                    delsession.Add(current.Session);
                }

                /// 送信先指定==自分の場合は処理終了
                if (sendto == SENDTO.ONE) break;
            }

            /// 異常終了セッションをログアウト通知
            if (delsession.Any())
            {
                for (int i = 0; i < delsession.Count; i++)
                {
                    try
                    {
                        RemoveSession(delsession[i]);
                    }
                    catch { }
                }
            }
        }

送信処理は特定ユーザのみにデータを送信する場合があるので、ちょっとわかりづらいコードになってしまったなう。SuperWebSocket.WebSocketSessionクラスのSendResponse()が送信処理本体で、byte[]とstringの2種類がある。

実行画面

まとめ
さくっとできたところはよかっと思います。

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

[C#,Python]WebSocketを試してみる。同期するUIを作ってみた編。

WebSocketで実現する同期するUI。group1とgroup2のコントロールの値を同期させてみる。

なんか思いつきで作ってみた。
サーバはPython,画面はC#。
クラスライブラリ化したので部品として使用したい場合は、さくっと使用出来る。

staticクラスを使用してWebSocketの接続部を作成。ソケットの部分はシングルトンイメージ。
WsExtensionsクラス

    /// <summary>
    /// Contolの拡張メソッドと静的変数の定義
    /// </summary>
    public static class WsExtensions
    {
        /// <summary>
        /// WebSocket接続用
        /// </summary>
        private static WebSocket _ws = null;

        /// <summary>
        /// WebSocketでの受信時イベントハンドラ
        /// </summary>
        public static EventHandler OnData;
        
        /// <summary>
        /// 受信イベント処理
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="e"></param>
        private static void OnRcvHandler(Control ctrl, WebSocketEventArgs e)
        {
            if (OnData != null)
            {
                OnData(ctrl, e);
            }
        }

        /// <summary>
        /// コントロールに紐つけるIDをセットする
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="name"></param>
        public static void WSSetUIName(Control ctrl, string name)
        {
            ctrl.Tag = name;
        }

        /// <summary>
        /// WebSocket初期化
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="wsuri"></param>
        /// <param name="uiname"></param>
        public static void WSConnect(System.Windows.Forms.Control ctrl, string wsuri, string uiname)
        {
            /// 対象のControl(パラメータ ctrl)とIDをセットする
            WSSetUIName(ctrl, uiname);

            /// WebSocket初期化
            if (_ws == null)
            {
                _ws = new WebSocket(wsuri);
                _ws.Open();
                _ws.OnData += (s, e) =>
                    {
                        OnRcvHandler(ctrl, e);
                    };
            }
            ctrl.Disposed += (s, e) =>
                {
                    if ((_ws != null) && (_ws.ReadyState != WebSocketState.Closed))
                    {
                        _ws.Close();
                    }
                };

        }

        /// <summary>
        /// WebSocket送信処理
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="value"></param>
        public static void SendMessage(Control ctrl, byte[] value)
        {
            /// IDとデータをパックして送信
            var uimessage = new UIMessage();
            uimessage._0UIName = (string)ctrl.Tag;
            uimessage._1Value = value;

            _ws.SendMessage(uimessage.Object2MessagePack());
        }

        /// <summary>
        /// object -> MessagePack(byte[])
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static byte[] Object2MessagePack<T>(this object obj)
        {
            var serializer = MessagePackSerializer.Create<T>();
            var ms = new System.IO.MemoryStream();
            serializer.Pack(ms, (T)obj);
            var ret = ms.ToArray();
            ms.Close();
            return ret;
        }

        public static byte[] Object2MessagePack(this object obj)
        {
            var serializer = MessagePackSerializer.Create<object>();
            var ms = new System.IO.MemoryStream();
            serializer.Pack(ms, (object)obj);
            var ret = ms.ToArray();
            ms.Close();
            return ret;
        }

        /// <summary>
        /// MessagePack(byte[]) -> object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static object MessagePack2Object<T>(this byte[] bytes)
        {
            var serialize = MessagePackSerializer.Create<T>();
            var ms = new System.IO.MemoryStream(bytes);
            var ret = serialize.Unpack(ms);
            ms.Close();
            return ret;
        }
   
        /// <summary>
        /// 送信用データクラス
        /// </summary>
        public class UIMessage
        {
            /// <summary>
            /// 対象となるControlに割り振ったIDを指定する
            /// </summary>
            public string _0UIName { get; set; }

            /// <summary>
            /// 値
            /// </summary>
            public byte[] _1Value { get; set; }
        }
     }

UIにより値を格納している方法が違うので、元のコントロールクラス(System.Windows.Form)を継承した各コントロールクラスを作成。そして、値が変更になった場合にデータをソケットで送信し、データが受信された場合にはコントロールの値を変更する。

UIクラス

    public class UI
    {
        /// <summary>
        /// TextBox
        /// </summary>
        public class wsTextBox : System.Windows.Forms.TextBox
        {
            public wsTextBox()
            {

                WsExtensions.OnData += (s,e)=>
                    {
                        var rcvData = (WebSocketEventArgs)e;
                        var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();
                        
                        if (uiMes._0UIName != this.Tag.ToString()) return;
                        if (!this.Focused)
                        {
                            this.Text = (string)uiMes._1Value.MessagePack2Object<string>();
                        }
                    };

                this.TextChanged += (s, e) =>
                    {
                        if (this.Focused) WsExtensions.SendMessage(this,this.Text.Object2MessagePack());
                    };
            }
        }

        /// <summary>
        /// HScrollBar
        /// </summary>
        public class wsHScrollBar : System.Windows.Forms.HScrollBar
        {
            public wsHScrollBar()
            {
                bool flg = false;

                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    if (!flg)
                    {
                        this.Value = (int)uiMes._1Value.MessagePack2Object<int>();
                    }
                };

                this.MouseHover += (s, e) => flg = true;
                this.MouseLeave += (s, e) => flg = false;

                this.ValueChanged += (s, e) =>
                {
                    if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// VScrollBar
        /// </summary>
        public class wsVScrollBar : System.Windows.Forms.VScrollBar
        {
            public wsVScrollBar()
            {
                bool flg = false;

                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    if (!flg)
                    {
                        this.Value = (int)uiMes._1Value.MessagePack2Object<int>();
                    }
                };

                this.MouseHover += (s, e) => flg = true;
                this.MouseLeave += (s, e) => flg = false;

                this.ValueChanged += (s, e) =>
                {
                    if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// Label(受信専用)
        /// </summary>
        public class wsOptInLabel : System.Windows.Forms.Label
        {
            public wsOptInLabel()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Text = (string)uiMes._1Value.MessagePack2Object<string>();
                };
            }
        }

        /// <summary>
        /// CheckBox
        /// </summary>
        public class wsCheckBox : System.Windows.Forms.CheckBox
        {
            public wsCheckBox()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>();
                };

                this.CheckedChanged += (s, e) =>
                {
                    WsExtensions.SendMessage(this, this.Checked.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// RadioButton
        /// </summary>
        public class wsRadioButton : System.Windows.Forms.RadioButton
        {
            public wsRadioButton()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>();
                };

                this.CheckedChanged += (s, e) =>
                {
                    WsExtensions.SendMessage(this, this.Checked.Object2MessagePack());
                };
            }
        }
    }

同期用のGUIコントロール(WebSocketUILib.UI)はすべてSystem.Windows.Formsからの派生型。
デザイナで配置したあと、親FormのDesigner.csでInitializeComponent()と変数定義を書き換える。

        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.textBox1 = new WebSocketUILib.UI.wsTextBox();
            this.textBox2 = new WebSocketUILib.UI.wsTextBox();
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.checkBox2 = new WebSocketUILib.UI.wsCheckBox();
            this.checkBox1 = new WebSocketUILib.UI.wsCheckBox();
            this.groupBox2 = new System.Windows.Forms.GroupBox();
            this.radioButton2 = new WebSocketUILib.UI.wsRadioButton();
            this.radioButton1 = new WebSocketUILib.UI.wsRadioButton();
            this.vScrollBar1 = new WebSocketUILib.UI.wsVScrollBar();
            this.label2 = new WebSocketUILib.UI.wsOptInLabel();
            this.label1 = new WebSocketUILib.UI.wsOptInLabel();
            this.hScrollBar1 = new WebSocketUILib.UI.wsHScrollBar();
            this.groupBox1.SuspendLayout();
            this.groupBox2.SuspendLayout();
            this.SuspendLayout();

            /// 省略
        }

        /// 変数定義の書き換え
        private WebSocketUILib.UI.wsTextBox textBox1;
        private WebSocketUILib.UI.wsTextBox textBox2;
        private System.Windows.Forms.GroupBox groupBox1;
        private WebSocketUILib.UI.wsCheckBox checkBox2;
        private WebSocketUILib.UI.wsCheckBox checkBox1;
        private System.Windows.Forms.GroupBox groupBox2;
        private WebSocketUILib.UI.wsRadioButton radioButton2;
        private WebSocketUILib.UI.wsRadioButton radioButton1;
        private WebSocketUILib.UI.wsVScrollBar vScrollBar1;
        private WebSocketUILib.UI.wsOptInLabel label2;
        private WebSocketUILib.UI.wsOptInLabel label1;
        private WebSocketUILib.UI.wsHScrollBar hScrollBar1;
    }

親Formのコンストラクタで同期対象コントロールのWebSocketを初期化する。全部のUIで初期化しているけど、実際には1接続しか確立しないようになっている。
親Formのコンストラクタ

        public Form1()
        {
            InitializeComponent();

            // group1
            WsExtensions.WSConnect(textBox1,"ws://192.168.1.22:12345/ui", "Text1");        /// -> label1と同期
            WsExtensions.WSConnect(textBox2,"ws://192.168.1.22:12345/ui", "Text2");        /// -> label2と同期
            WsExtensions.WSConnect(hScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue");  /// -> vScrollBar1と同期
            hScrollBar1.Minimum = 0;
            hScrollBar1.Maximum = 100;
            WsExtensions.WSConnect(checkBox1,"ws://192.168.1.22:12345/ui", "Check1");      /// -> radioButton1と同期
            WsExtensions.WSConnect(checkBox2,"ws://192.168.1.22:12345/ui", "Check2");      /// -> radioButton2と同期

            // group2
            WsExtensions.WSConnect(label1,"ws://192.168.1.22:12345/ui", "Text1");          /// -> textBox1と同期
            WsExtensions.WSConnect(label2,"ws://192.168.1.22:12345/ui", "Text2");          /// -> textBox2と同期
            WsExtensions.WSConnect(vScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue");  /// -> hScrollBar1と同期
            vScrollBar1.Minimum = 0;
            vScrollBar1.Maximum = 100;
            WsExtensions.WSConnect(radioButton1,"ws://192.168.1.22:12345/ui", "Check1");   /// -> checkBox1と同期
            WsExtensions.WSConnect(radioButton2,"ws://192.168.1.22:12345/ui", "Check2");   /// -> checkBox2と同期
        }

ポイントはWSConnectで渡している2つめのパラメータ。これを定義することで、同じIDが定義された他のコントロールともデータの共有ができる。
UIクラスの各コントロールソース内で、受信データとして送られてくるIDと自分自身の値(ControlクラスのTagプロパティ)を比較し、受信すべきデータであるか判定している。C#は多重継承ができないのでControlクラスがもっているTag(object型)は助かった。
今回は値だけだったけど、他のプロパティを変更するコードを書けばもっと面白くなるかもしれない。テキストボックスやスライドバーなどは他の言語でもよくある型なので、連携させるのはあっさりできるかもしれない。

ちなみに、以下サーバソース。サーバはgeventwebsocketのブロードキャストサーバのほぼまんま。
ui_server.py

#!/usr/bin/env python2.6
# -*- coding:utf-8 -*-

import os
import geventwebsocket
import msgpack
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler


websocketCons = []

def connection_handle(wenv):
    ws = wenv['wsgi.websocket']
    wskey = wenv['HTTP_SEC_WEBSOCKET_KEY']
    websocketCons.append(ws)
    print "add memlen = " + str(len(websocketCons))
    while True:
        msg = ws.receive()
        if msg is None:
           break
        
        i = 0
        while i < len(websocketCons):
           try:
                websocketCons[i].send(msg)
           except:
                websocketCons.remove(websocketCons[i])
           i += 1
    print "remove"
    websocketCons.remove(ws)


def websocket_app(environ, response):
    path = environ["PATH_INFO"]
    res_ConText = [("Content-Type", "text/html")]
 
    if path == "/":
        response("200 OK", res_ConText)
        return open('./index.html').read()
    elif path == "/ui":
        connection_handle(environ)
    else:
        response("404 Not Found",res_ConText)
        return "=======   Not Found!   ======="

if __name__ == "__main__":
   server = pywsgi.WSGIServer(('192.168.1.22', 12345), websocket_app, handler_class=WebSocketHandler)
   server.serve_forever()

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