[C/C++]libwebsocketsを試してみる。その3。SSL通信を試す。

libwebsocketsのサンプルでSSL通信を試す。
まず、libwebsocketsのサンプルで試してみます。
SSL通信によりサンプルサーバーを動作させるにはconfigureにオプションを付加してMakefileを作ります。

$./congifure --enable-openssl
$ make clean
$ make && sudo make install

test-serverにあるlibwebsockets-test-serverを起動します。
SSL通信を有効にするにはパラメータ付きで起動させます。

$ ./libwebsockets-test-server -h
Usage: test-server [--port=<p>] [--ssl] [-d <log bitfield>]

起動

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

ブラウザからアクセスしてみる。
httpsでアクセスします。サンプルの認証鍵を使用しているので、ブラウザが信頼されないサイト、とか、セキュリティ証明書に問題あるとかいわれますが、続行して接続してみます。
Img20130317011820
※ちなみに、IE10ではHTMLは表示されますが、WebSocketの部分は動作しませんでした。たぶんセキュリティかなぁ。

その2のチャットサーバーをSSL通信化。
その2で作成したチャットソフトをSSL通信化させてみます。SSLの認証関連が面倒なので、サンプルサーバー同様、Webサーバー経由でhtmlをロードさせてからhtmlの中でWebSocketを使ってみます。

今回の実行環境
Ubuntu 12.04 LTS
Apache/2.2.22
OpenSSL 1.0.1
Windows7(クライアント実行環境)

apache2+SSLの準備
apache2+SSLを設定し、そこで設定した認証鍵をWebSocketサーバーで使用したいと思います。
今回はテストなのでSSL通信に必要な設定はサンプル等を使用します。

apache2+SSLのデフォルト設定を使用。

$ sudo aptitude -y install apache2
$ sudo a2enmod ssl
$ sudo a2ensite default-ssl
$ sudo /etc/init.d/apache2 restart

default-sslが使用する鍵のパス
設定ファイル:/etc/apache2/sites-available/default-ssl

SSLCertificateFile    /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key

チャットフォームの設置
DocumentRoot(/var/www)にhtmlファイルを保存します。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>WebSocket Sample</title>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>Chat</h1>
            </div>
        </header>
        <section id="main">
        URI <input id="uri" type="text" />
        <input type="button" value="Connect" onClick="connect()" /><br>
		SEND<input id="chatmessage" type="text" />
		<input type="button" value="Send" onClick="send()" />
		<hr />
		<div id="log"></div>
		<script type="text/javascript">
			var ws;
			
			function connect(){
				if ("WebSocket" in window) {
					ws = new WebSocket(document.querySelector("#uri").value);
				} else if ("MozWebSocket" in window) {
					ws = new MozWebSocket(document.querySelector("#uri").value);
				}
				
				ws.onmessage = function(event){
				    output(event.data);
				}
				
				ws.onopen = function(event){
				    output("WebSocket Open!");
				}

				ws.onclose = function(event){
				    output("WebSocket Close.");
				}				
			}	
			
			function send(){
			    var str = document.querySelector("#chatmessage");
			    ws.send(str.value);
			    str.value="";
			}

			function disconnect(){
				ws.close();
				ws = null;
			}
			
			function output(str) {
			    document.getElementById("log").innerHTML += str + "<hr />";
			}
			
		</script>
        </section>
        <footer>
        </footer>
    </div>
</body>
</html>

サーバー側の改修は楽。
ソース

int main()
{
	int n = 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);
	

	info.iface = iface;
	info.protocols = protocols;
    
//	info.ssl_cert_filepath = NULL;
//	info.ssl_private_key_filepath = NULL;
	
	info.ssl_cert_filepath = "/etc/ssl/certs/ssl-cert-snakeoil.pem";
	info.ssl_private_key_filepath = "/etc/ssl/private/ssl-cert-snakeoil.key";
    
	info.gid = -1;
	info.uid = -1;
	info.options = opts;
	
	signal(SIGINT, sighandler);
	
	setlogmask(LOG_UPTO (LOG_DEBUG));
	openlog("lwsts", syslog_options, LOG_DAEMON);
	
    lwsl_notice("libwebsockets chat server -\n");
	
    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;
}

SSL通信を有効にするにはlws_context_creation_info構造体に認証鍵を指定するだけのようです。

	info.ssl_cert_filepath = "/etc/ssl/certs/ssl-cert-snakeoil.pem";
	info.ssl_private_key_filepath = "/etc/ssl/private/ssl-cert-snakeoil.key";

コンパイル。

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

起動してみる。尚、デフォルトではSSL通信用の認証鍵のパーミッションが無いのでsudoで実行します。

$ sudo ./wsserver_ssl
[sudo] password for user: 
lwsts[27128]: libwebsockets chat server -
lwsts[27128]: Initial logging level 7
lwsts[27128]: Library version: 1.2 
lwsts[27128]:  Started with daemon pid 0
lwsts[27128]:  static allocation: 5472 + (12 x 1024 fds) = 17760 bytes
lwsts[27128]:  canonical_hostname = ubuntu
lwsts[27128]:  Compiled with OpenSSL support
lwsts[27128]:  Using SSL mode
lwsts[27128]:  per-conn mem: 184 + 1328 headers + protocol rx buf
lwsts[27128]: LWS_CALLBACK_ADD_POLL_FD
lwsts[27128]:  Listening on port 7681
lwsts[27128]: LWS_CALLBACK_PROTOCOL_INIT

ブラウザでアクセスしてみる。
Img20130317000419

[Webアプリ]WebSocketとGoogleMapsでリアルタイム監視マップを作る。その1。

結構簡単に出来る。
全国に複数ある工場の異常をリアルタイムに監視できるマップをWebSocketとGoogleMapsを使って作ってみたいと思います。

やりたいこと。
・全国の拠点(工場や事務所)で発生したイベント(警報、警告など)をWebSocketとGoogleMapsを使ってリアルタイム監視したい。

イメージ
Img20130315001252

実行画面

JSONとWebSocketのおかげ。
動作の仕組みとしてはモニター対象の各クライアントが送信したJSON文字列をサーバーが接続中のWebSocketクライアントにブロードキャストしています。
WebSocketなので、サーバーはモニター対象のクライアントをポーリングする必要はなく、クライアントはイベントが発生したタイミングでサーバーに情報を送信することができます。また、ブラウザもWebSocketセッションを張りっぱなしなので、ブラウザリロードなどの必要はありません。まぁ、セッションが切れないという保証はないと思いますので、その辺の考慮は必要ですが。。。

GoogleMapsAPIの使用方法については、下記のサイト様を参考にしました。このサイト作った人ほんとすごいですよねー。

ドットインストール – Google Maps API入門
http://dotinstall.com/lessons/basic_google_maps

[C#]System.Net.WebSocketsを試す。その2。サーバー編。

System.Net.WebSocketsを使用してサーバーを作成。
System.Net.WebSocketsには”WebSocketServer”とか分かりやすい名前はなく、クライアントからの待受はSystem.Net.HttpListenerクラスが行います。これはWebSocketのはじめのハンドシェイクはhttpにより行われることからですね。

詳しいことはMSDNの記事に載っています。

Windows 8 のネットワーク接続 Windows 8 と WebSocket プロトコル
http://msdn.microsoft.com/ja-jp/magazine/jj863133.aspx

上記の記事内のサンプルソースもとても参考になります。

エコーサーバーを作成してみる。
以前作成したSuperWebSocketを使用したサーバーと同じ動作をするものを作成してみます。

ソース

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

using System.Net;
using System.Net.WebSockets;

using System.Diagnostics;

namespace WsEchoServerForWin8
{
    class Program
    {
        static void Main(string[] args)
        {
            StartServer();
            Console.WriteLine("{0}:Server start.\nPress any key to exit.", DateTime.Now.ToString());
            Console.ReadKey();
            Parallel.ForEach(_client,p=>
            {
                if (p.State == WebSocketState.Open) p.CloseAsync(WebSocketCloseStatus.NormalClosure, "", System.Threading.CancellationToken.None);
            });
        }

        /// <summary>
        /// クライアントのWebSocketインスタンスを格納
        /// </summary>
        static List<WebSocket> _client = new List<WebSocket>();

        /// <summary>
        /// WebSocketサーバースタート
        /// </summary>
        static async void StartServer()
        {
            /// httpListenerで待ち受け
            var httpListener = new HttpListener();
            httpListener.Prefixes.Add("http://192.168.1.10:12345/");
            httpListener.Start();

            while (true)
            {
                /// 接続待機
                var listenerContext = await httpListener.GetContextAsync();
                if (listenerContext.Request.IsWebSocketRequest)
                {
                    /// httpのハンドシェイクがWebSocketならWebSocket接続開始
                    ProcessRequest(listenerContext);
                }
                else
                {
                    /// httpレスポンスを返す
                    listenerContext.Response.StatusCode = 400;
                    listenerContext.Response.Close();
                }
            }
        }
        
        /// <summary>
        /// WebSocket接続毎の処理
        /// </summary>
        /// <param name="listenerContext"></param>
        static async void ProcessRequest(HttpListenerContext listenerContext)
        {
            Console.WriteLine("{0}:New Session:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());

            /// WebSocketの接続完了を待機してWebSocketオブジェクトを取得する
            var ws = (await listenerContext.AcceptWebSocketAsync(subProtocol:null)).WebSocket;

            /// 新規クライアントを追加
            _client.Add(ws);

            /// WebSocketの送受信ループ
            while (ws.State == WebSocketState.Open)
            {
                try
                {
                    var buff = new ArraySegment<byte>(new byte[1024]);

                    /// 受信待機
                    var ret = await ws.ReceiveAsync(buff, System.Threading.CancellationToken.None);

                    /// テキスト
                    if (ret.MessageType == WebSocketMessageType.Text)
                    {
                        Console.WriteLine("{0}:String Received:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                        Console.WriteLine("Message={0}", Encoding.UTF8.GetString(buff.Take(ret.Count).ToArray()));

                        /// 各クライアントへ配信
                        Parallel.ForEach(_client,
                            p => p.SendAsync(new ArraySegment<byte>(buff.Take(ret.Count).ToArray()),
                            WebSocketMessageType.Text,
                            true,
                            System.Threading.CancellationToken.None));
                    }
                    else if(ret.MessageType == WebSocketMessageType.Close) /// クローズ
                    {
                        Console.WriteLine("{0}:Session Close:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                        break;
                    }
                }
                catch
                {
                    /// 例外 クライアントが異常終了しやがった
                    Console.WriteLine("{0}:Session Abort:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                    break;
                }
            }

            /// クライアントを除外する
            _client.Remove(ws);
            ws.Dispose();
            
        }
    }
}

実行画面

※実行環境はWindows8で、実行するには管理者権限が必要でした。デバッグ時にはVS自体に管理者権限を付加。ホストPC外からの接続待ちには該当Portに対してファイアーウォールの設定を行う必要がありました。

なぜArraySegmentか、という考察。
送受信時に使用しているArraySegmentについて。ArraySegmentなんて初めて使いましたが。ArraySegmentを使う理由は上部の記事(MSDN)と記事内のサンプルソースを読むと分かってきます。サンプルソースではデータ受信時の処理がループを使用し複数回に分けて送られてくることを想定して作られています。SendAsyncに指定しているendOfMessage(3つめパラメータ)が重要で、要は、大きなデータを分割して送信し、最後の送信だけendOfMessage=trueを指定することでデータ送信の終わりを受信側に知らせることができます。受信側は最後のメッセージを受信した時点で1つのデータを完成させるわけですね。ArraySegmentを使用する理由は1次配列の操作をするためだからだでしょう、楽かどうか別として。

WebSocketライブラリによって動作が違う。
文字列”Hello WebSockets World!!”を3回に分けてSendAsyncを使って送信してみます。

string s = "Hello WebSockets World!!";
var sendData = Encoding.UTF8.GetBytes(s);
await ws.SendAsync(new ArraySegment<byte>(sendData.Take(5).ToArray()), WebSocketMessageType.Text, false, System.Threading.CancellationToken.None); // endOfMessage = false
await ws.SendAsync(new ArraySegment<byte>(sendData.Skip(5).Take(5).ToArray()), WebSocketMessageType.Text, false, System.Threading.CancellationToken.None); // endOfMessage = false
await ws.SendAsync(new ArraySegment<byte>(sendData.Skip(5 + 5).ToArray()), WebSocketMessageType.Text, true, System.Threading.CancellationToken.None); // endOfMessage = true

System.Net.WebSockets.ClientWebSocketの動作。
以前作成したClientWebSocketを使用したクライアントでデータを受信した時のトレース。

Receive
Count = 5
Type  = Text
EndOfMessage= False
Msg   = Hello

Receive
Count = 5
Type  = Text
EndOfMessage= False
Msg   =  WebS

Receive
Count = 14
Type  = Text
EndOfMessage= True
Msg   = ockets World!!

ReceiveAsyncが3回呼ばれます。これらはReceiveAsyncが返す戻り値の値ですが、EndOfMessageのフラグが送信通りになっています。

しかし、WebSocket4Netを使用したクライアントでは受信処理は1度だけで送信文字列は完成された状態で受信できます。WebSocket4NetのMessageReceivedイベントで渡ってくる受信データには文字列が格納されている変数しかなく、最後のデータであるかどうかは知るすべはありません。これはWebSocketの仕様に合わせた各ライブラリの仕様上の動作だと思われますが、そのへんは使用するライブラリによりある程度はコーディングなどでフォローするしかないようです。
ちなみに、WebSocket4Netを使用したクライアントはSendAsync間にSleep(or Task.Delay)を入れてあげないと、中途半端な文字列になったりしてうまく受信できませんでした。

System.Net.WebSockets.ClientWebSocketを使用したクライアントが受信した時。
Img20130312013413
WebSocket4Netを使用したクライアントが受信した時。
Img20130312013648

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

[C#]非同期でSleep()したい時にはTask.Delay()を使う。

定周期処理を作るとき便利。

使い方は簡単です。
ボタンクリックしたら1秒おきにラベルに0~9までの文字を表示するプログラムを考えます。

Sleep()でやってた処理。

private async void button1_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Run(() => System.Threading.Thread.Sleep(1000));
        label1.Text = i.ToString();
    }
}

Task.Delay()を使う場合。

private async void button1_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(1000);
        label1.Text = i.ToString();
    }
}

実践プログラミング。指定されたIPアドレスに対して1秒周期でPingを送信して死活監視を行う。
以前、同じようなプログラムを作って、えらいめんどくさい思い出があったのですが、async/awaitを使用すると驚くほど簡単に出来ます。

実行画面

ソース

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;

namespace async_ping
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
            this.MaximizeBox = false;
        }

        private async void Ping(TextBox tbox)
        {
            Color setcolor = Color.WhiteSmoke;
            if (tbox.Text == "") goto CHANGECOLOR;
            try
            {
                System.Net.IPAddress ipAdr;
                var ipchk = System.Net.IPAddress.TryParse(tbox.Text,out ipAdr);
                if(ipchk ==false) goto CHANGECOLOR;

                var reply = await (new System.Net.NetworkInformation.Ping()).SendPingAsync(ipAdr, 1000);

                if (reply.Status == System.Net.NetworkInformation.IPStatus.Success)
                {
                    setcolor = Color.Lime;
                }
                else
                {
                    setcolor = Color.Red;
                }
            }
            catch
            {
                setcolor = Color.WhiteSmoke;
            }
CHANGECOLOR:
            tbox.BackColor = setcolor;
        }

        private async void Form1_Load(object sender, EventArgs e)
        {
            LoadSaveValue(isLoad: true);

            while (true)
            {
                await Task.Delay(1000);
                foreach (var ctrl in this.Controls)
                {
                    var tbox = ctrl as TextBox;
                    if (tbox != null)
                    {
                        Ping(tbox);
                    }
                }
            }
        }
        private const string SAVEFILE = "savefile.csv";
        private void LoadSaveValue(bool isLoad = false, bool isSave = false)
        {
            if (isLoad)
            {
                if(System.IO.File.Exists(SAVEFILE))
                {
                    var vals = System.IO.File.ReadAllText(SAVEFILE).Split(',');
                    textBox1.Text = vals[0];
                    textBox2.Text = vals[1];
                    textBox3.Text = vals[2];
                    textBox4.Text = vals[3];
                    textBox5.Text = vals[4];
                    textBox6.Text = vals[5];
                    textBox7.Text = vals[6];
                    textBox8.Text = vals[7];
                }
            }
            else if (isSave)
            {
                var s = string.Format("{0},{1},{2},{3},{4},{5},{6},{7}",
                    textBox1.Text,
                    textBox2.Text,
                    textBox3.Text,
                    textBox4.Text,
                    textBox5.Text,
                    textBox6.Text,
                    textBox7.Text,
                    textBox8.Text);
                System.IO.File.WriteAllText(SAVEFILE,s);
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            LoadSaveValue(isSave: true);
        }
    }
}

[C#]System.Net.WebSocketsを試す。その1。ClientWebSocket編。

.NET Framework 4.5からサポートされているWebSocketのクライアント版。
これまでHTML5LabsのWCF WebSocketsを使用してきましたが、こちらは開発がストップ(?)しているようで、WebSocketsのプロトコルバージョンも古かったりします。もし、Windows7でWebSocketsを使用したデスクトップアプリケーションを作成する場合はSuperWebSocketsWebSocket4Netを導入するほうがよいでしょう。

本題のSystem.Net.WebSockets.ClientWebSocketですが、こちらが.NetFrameworkとして実装しているWebSocketsです。サポートOSがWindows8/WindowsServer2012となっておりWindows7はサポートしていません(コーディングはできるが、実行できない)。Windows8は持っていないので体験版を使用してVM上で試してみたい思います。

とりあえずチャットソフト。
とりあえずチャットソフトを作成。ClientWebSocketなので今回はクライアント版。サーバーはとりあえず以前作成したエコーサーバーを使用。

画面
Img20130307144817

動作画面

ソース

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.Threading;
using System.Net.WebSockets;

namespace WsForm_WCF_ForWin8
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// Send message buffer size.
        /// </summary>
        const int MessageBufferSize = 256;

        /// <summary>
        /// ClientWebSocket instance.
        /// </summary>
        ClientWebSocket _ws = null;

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Server connect.
        /// </summary>
        private async void Connect()
        {
            if (_ws == null)
            {
                _ws = new ClientWebSocket();
            }

            if (_ws.State != WebSocketState.Open)
            {
                await _ws.ConnectAsync(new Uri(textBox1.Text), CancellationToken.None);

                while (_ws.State == WebSocketState.Open)
                {
                    var buff = new ArraySegment<byte>(new byte[MessageBufferSize]);
                    var ret = await _ws.ReceiveAsync(buff, CancellationToken.None);
                    listBox1.Items.Add((new UTF8Encoding()).GetString(buff.Take(ret.Count).ToArray()));
                    listBox1.TopIndex = listBox1.Items.Count - 1;
                }
            }
        }

        /// <summary>
        /// Connect button.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            Connect();
        }

        /// <summary>
        /// Send message button.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            var buff = new ArraySegment<byte>(Encoding.UTF8.GetBytes(textBox2.Text));
            if (_ws.State == WebSocketState.Open)
            {
                _ws.SendAsync(buff, WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }

        /// <summary>
        /// Close websockets.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if ((_ws != null) && (_ws.State == WebSocketState.Open))
            {
                _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
            }
        }
    }
}

果たしてこれで良いのかどうかわかりませんが、細かいレベルの動作についてはおいおい。。。
尚、MSDNのサンプルを参考にしました。
WebSockets middle-tier sample using ClientWebSocket
http://code.msdn.microsoft.com/WebSockets-middle-tier-5b2972ce

全然違うコーディング。
これまでやってきたC#のWebSocketsのAPI(接続、送信、受信、接続断)はイベントハンドラを利用して処理をコーディングしましたが、ClientWebSocketはasync/awaitなAPIなので、コードが全然違います。Eventと違い非同期メソッドなので、その処理を待つ・処理後のコールバックなどは自分でコーディングする必要があります。コールバック的なものはawaitの直下の処理であり、この部分は別スレッドからのUIのアクセスにInvoke()が必要ありません。但し、awaitが使用できるのはasyncメソッド内だけだったり、awaitが指定できる処理はTaskを返すメソッドである必要があったり、色々考えて作る必要がありそうです。うーん、これは慣れるのに時間がかかりそう。。。

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

[C#]async/awaitの使い方メモ、その1。

今後の.NET開発では必須スキルか。
.NETでの非同期処理の書き方があまりにも色々ありすぎて覚えるのも大変。でも、async/awaitを使用した非同期処理は今後重要なキーワードとなってきそうです。

参考
連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
http://www.atmarkit.co.jp/ait/subtop/features/da/ap_masterasync_index.html

ボタンをクリックすると1秒間隔でラベルの文字が変更するプログラムを書く。
フォームのボタンをクリックすると1秒間隔で10回、ラベルの文字が変更するプログラムを考えます。

非同期処理を使わない例。
非同期処理の効用を知るために、非同期処理を使わないで処理を書いてみます。

        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread.Sleep(1000);
                label1.Text = i.ToString();
            }
        }

実行画面

ボタンクリック後、処理が終わるまでコントロールはフリーズしたようになります。

非同期処理を使う例。
非同期処理をTaskクラスで実装してみます。

        private void button1_Click(object sender, EventArgs e)
        {
            var task = new TaskFactory().StartNew(() =>
                {
                    for (int i = 0; i < 10; i++)
                    {
                        System.Threading.Thread.Sleep(1000);
                        this.Invoke(new Action(() => label1.Text = i.ToString()));
                    }
                });
        }

実行画面

非同期処理を使うことでクリック後もコントロールの操作が可能になります。

async/awaitで非同期処理を書く例。
async/awaitを使用して非同期処理を書いてみます。

        private async void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < 10; i++)
            {
                await Task.Run(() => System.Threading.Thread.Sleep(1000));
                label1.Text = i.ToString();
            }
        }

メソッドの定義にasyncキーワードを追加しています。これは非同期メソッドであることを宣言しています。awaitは処理本体に記述する演算子で、単純に言えば非同期で行う処理を示しています。asyncが宣言されたメソッドにはawaitの処理が必要であり(awaitがない場合はコンパイル警告が発生)、基本的にはペアで覚えれば良いと思います。
awaitが指定できる処理にはFuncもしくはActionを実行するTaskを指定します。上記のような書き方をすれば別スレッドからのUI操作にInvokeメソッドが必要ありません。これは非同期処理が別スレッドで動作した後に、再度そのメソッドの続きから始められるようにコンパイラがしてくれているそうです。

ラムダ式の場合は次のように書きます。

        private void button1_Click(object sender, EventArgs e)
        {
            Action func = async () =>
            {
                for (int i = 0; i < 10; i++)
                {
                    await Task.Run(() => System.Threading.Thread.Sleep(1000));
                    label1.Text = i.ToString();
                }
            };

            func();
        }          

まとめ。
async/awaitの書き方はLINQやラムダ式同様、一つの糖衣構文ですね。手軽に非同期処理書けまっせということでしょう。また、WindowsPhoneやタブレットなどが増えて、ユーザーがUIに触れている時間が増えたということと、開発者がこれまで作成できなかった部分というのが広がり、UIをフリーズさせないようにするために非同期処理の重要性が高まったというのが背景にあるのかも。つまり、今後、.NETが提供する時間がかかる系のAPIはasyncメソッドなので、使い方覚えてね!ということのようです。

[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>