[Webアプリ]nginxを使ってWebSocketのリバースプロキシを設定する。

nginxのWebSocketのリバースプロキシはversion 1.3.13から対応。
nginx本体にWebSocketのリバースプロキシが対応したみたいなので、SSL接続まで使ってみました。

nginx.org – WebSocket proxying
http://nginx.org/en/docs/http/websocket.html

バージョンは1.3.13から対応しているようです。stable版ではないので、ソースからコンパイルします。

テストとして、nginxのリバースプロキシ機能を使用して、(http)80,(https)443でlibwebsocketsのテストサーバーに接続してみます。

パッケージダウンロードと展開(2013/03/22時点での最新)

$wget http://nginx.org/download/nginx-1.3.14.tar.gz
$tar -zxvf nginx-1.3.14.tar.gz

SSLを有効にしてコンパイル & インストール

$./configure --with-http_ssl_module
$make
$sudo make install

リバースプロキシを設定してlibwebsocketsのWebSocketサーバーに接続してみる。
ここを参考にして、WebSocketのリバースプロキシを設定します。
※ちなみに、この設定はnginx1.3.13以前のバージョンでも有効な設定であり、WebSocketをクライアントが繋ぎにいこうとするとhttpハンドシェイクがWebSocketサーバー側に伝わります。しかし、この場合はサーバーのみクライアントの接続を認識しますが、クライアント側に応答が返らない状態になります。

◇Port:80 http libwebsocketsのリバースプロキシの設定例。
設定ファイルパス(default):/usr/local/nginx/conf/nginx.conf
libwebsocketsはtest-server/libwebsockets-test-server(同PC内で起動 Port:7681で待受)を使用します。

http{
// 略
       server {
                listen 80;
                server_name localhost;

                location / {
                        proxy_pass http://localhost:7681/;
                        proxy_http_version 1.1;
                        proxy_set_header Upgrade $http_upgrade;
                        proxy_set_header Connection "upgrade";
                }
        }
}

libwebsokcets-test-serverの起動

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

nginxの起動

$sudo /usr/local/nginx/sbin/nginx -s stop  // 停止
$sudo /usr/local/nginx/sbin/nginx          // 起動

ブラウザでアクセスしてみる。
httpで接続!正常に動作します。
Img20130322145850
libwebsocketsはエイリアスに”/”のみでアクセスし、WebSocketのサブプロトコルが”/”,”dumb-increment-protocol”,”lws-mirror-protocol”で動作が変わります。(nginx.confに”root”パスを書かなくてよいのはlibwebsocketsにhttpサーバーの機能があるからです)。nginxの設定だけでサブプロトコルも引き継ぎられているようです。

アクセスがないソケットはnginxによって閉じられてしまう。
どうやら動きのないソケットはnginxによって閉じられてしまうようです。
Img20130322150035
“dumb-increment-protocol”はサーバーから一方的にデータ(カウンター値)を受信しているのに対し、”lws-mirror-protocol”は任意のタイミングで送受信が発生するので何もしないと約110秒くらいでソケットが閉じられてしまいます。今のところこのtimeoutの設定はわかりません。

◇Port:443 SSL libwebsocketsのリバースプロキシの設定例。
設定ファイルパス(default):/usr/local/nginx/conf/nginx.conf
libwebsocketsはtest-server/libwebsockets-test-server(同PC内で起動 Port:7681で待受)を使用します。

http{
// 略
        server {
                listen 443;
                server_name localhost;

                ssl on;
                ssl_certificate /home/user/work/libwebsockets/libwebsockets-1.22-chrome26-firefox18/test-server/libwebsockets-test-server.pem; // テストサーバーの証明書
                ssl_certificate_key /home/user/work/libwebsockets/libwebsockets-1.22-chrome26-firefox18/test-server/libwebsockets-test-server.key.pem; // テストサーバーの秘密鍵

                location /{
                        proxy_pass https://localhost:7681/;
                        proxy_http_version 1.1;
                        proxy_set_header Upgrade $http_upgrade;
                        proxy_set_header Connection "upgarede";
                }
        }
}

libwebsokcets-test-serverのSSL起動
※configure時に”–enable-openssl”オプションが必要。

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

nginxの起動

$sudo /usr/local/nginx/sbin/nginx -s stop  // 停止
$sudo /usr/local/nginx/sbin/nginx          // 起動

ブラウザでアクセスしてみる。
httpsで接続!!正常に動作します。
Img20130322151711
ちなみに、動きの無いソケットが閉じられるのはhttpと同じ現象。

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

リアルタイム監視マップ作成続き。ブラウザで操作編。

やりたいこと。
・全国の拠点(工場や事務所)で発生したイベント(警報、警告など)をWebSocketとGoogleMapsを使ってリアルタイム監視したい。
・重大な警告が発生したら発生場所を示すマーカーを点滅表示させたい。
・重大な警告が発生したら警告音を鳴らしたい。尚、警告状態が収束したら警告音を止めること。
・デバッグ用にブラウザからイベントを送信できるようにしたい。←NEW

ブラウザからイベントを発生させる。
JavaScriptでJSON文字列を作成し、監視マップに反映させてみます。

実行画面(音出ます)

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

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

WebSocketを使用してのWebアプリ作成続き。
JavaScriptをあまりやったことがないので、四苦八苦しながら。。

やりたいこと。
・全国の拠点(工場や事務所)で発生したイベント(警報、警告など)をWebSocketとGoogleMapsを使ってリアルタイム監視したい。
・重大な警告が発生したら発生場所を示すマーカーを点滅表示させたい。←NEW
・重大な警告が発生したら警告音を鳴らしたい。尚、警告状態が収束したら警告音を止めること。←NEW

イベントの発生場所の点滅と警告音を鳴らしてみる。
重大な警告レベルのイベントが発生したらマップ上のマーカーを点滅させるのと、警告音を鳴らし続ける機能を追加してみる。
重大なイベントがない場合は警告音を止める。

サウンドはこちらのサイトの方のを使わせていただいた。
音楽素材/魔王魂
http://maoudamashii.jokersounds.com/

実行画面(音出ます)

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

[C#]WebSocketを試してみる。クライアントSSL通信編。

WebSocketでSSL通信。
この投稿ではSSL通信を行うサーバーを試したのでクライアント編。
SSL通信を行うにはWebSocketサーバーへの接続URIが”ws://”ではなく”wss://”になります。

基本的にSSLの検証が通ればソース変更の必要はなし。
C#に関わらず、各WebSocketライブラリはWebSocketの規格に準拠して作られているはずなので、”SSL/TLS Support”とかライブラリのドキュメントに書いてあればSSLが使えるのだと思います。
SSLは通信が安全であるかを証明したうえで通信を行います。.NetFrameworkなどは検証を行う仕組みが存在するので、そこが通ってしまえばソースコードの改修は要らないはずです。

ちゃんとやろうとするとちょっと面倒だし、SSLを使った開発やテストなどの目的で検証を無理やり通してしまうやり方もあるので、ここでは無理やり突破してSSL通信を試します。

WebSocket4Net編
WebSocketインスタンスのAllowUnstrustedCertificateプロパティにtrueを設定します。

            var ws = new WebSocket("wss://192.168.1.22:7681/");
            ws.AllowUnstrustedCertificate = true; // 信頼されない検証を通す

System.Net.WebSockets.ClientWebSocket編
System.Net.WebSockets.ClientWebSocketはWindows8向けです。
下記の記事などを参照に通します。

@IT – SSL通信で信頼されない証明書を回避するには?
http://www.atmarkit.co.jp/fdotnet/dotnettips/867sslavoidverify/sslavoidverify.html

接続前に検証を通すコールバックを設定します。

                    ServicePointManager.ServerCertificateValidationCallback += (s, certificate, chain, sslPolicyError)=>true; // 信頼されない検証を通す

                    await _ws.ConnectAsync(new Uri(textBox1.Text), CancellationToken.None); // WebSocketの接続

ちなみにWebSocket4Netはソケットの接続部分がラップされているので、System.Net.WebSockets.ClientWebSocketと同じ方法ではダメでした。

実行画面(左上:WebSocket4Net,左下:ClientWebSocket,右:libwebsocketsサーバー)

Windows8上でブラウザでのWebSocket(SSL)接続ができず。。。
SSL通信を無理やり通している為か、Windows8上でChrome,FireFox,IEを使用してwssによる接続を試しましたが、どれも接続完了しませんでした。
IE10に関しては無理やり突破すらできないようです。
Img20130319172309

[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#]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/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>