鉄道NowのChrome Extensionプラグインを作ってみた。(β版) (2012/10/20 Update)

鉄道Nowで表示されている電車をクリックするとその電車の位置を単にGoogle Earthで表示するというプラグインを作ってみました。
このプラグインはポップアップウィンドウを使用していますので、ポップアップをブロックしている場合は、
"http://www.demap.info"のポップアップを許可してください。

今回、追尾機能追加アップデートにより、この追尾ポップアップを元に位置を取得すように修正しました。
これにより一応、どの拡大率でも動作するようになりました。
しかし、特に拡大率が小さい場合はEarthでのカメラの向きがおかしくなるので注意してください。
なお、マップの画像を元に座標を取得しているため、画像が完全に読み込まれた後に処理を行うようにクリックしてから2秒後にEarthの更新を開始しています。
プラグインのインストール
一番下にあるリンクよりdemap_plugin.crxファイルをダウンロードします。
「ツール」⇒「拡張機能」で拡張機能のページを表示後、ダウンロードしたdemap_plugin.crxファイルをドラッグアンドドロップしてください。
"「demap plugin」を追加しますか?"という確認ダイアログが表示されますので、「追加」ボタンをクリックすればインストールは完了です。
プラグインを動かしてみる
インストールが終わりましたら、鉄道Nowのページにアクセス(すでに表示している場合は更新)すると、ポップアップが表示されると思います。
ポップアップウィンドウとメインウィンドウを見やすい位置/サイズに設定します。
ポップアップウィンドウに地球が表示されるまで待機してください。ただ、結構な確立でなかなか表示されない場合がありますので、
そのときは"鉄道Nowのページ"を更新してください。
地球が表示されたら、鉄道Nowに表示されている電車をクリックすればGoole Earthにその位置が表示されると思います。
そのほか
現在のプラグインは、鉄道Nowにアクセスすると必ずポップアップを表示するようにしていますので、うざくなってきたら
「ツール」⇒「拡張機能」で"demap plugin 0.0"を無効にするか削除してください。

さくらVPS(CentOS6.2) に Node.js を git clone で インストールすると失敗するけど、 wget では成功する。(※解決済)

まずは git clone でオプション無しでインストールを試してみる。

# git clone git://github.com/joyent/node.git
# cd node
# ./configure
# make
とすると、make実行時に


ACTION v8_snapshot_run_mksnapshot /root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc
pure virtual method called
terminate called without an active exception
/bin/sh: line 1: 16824 Aborted "/root/node/out/Release/mksnapshot" --log-snapshot-positions --
logfile "/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.log"
"/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc"
make[1]: *** [/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc] Error 134
make[1]: Leaving directory `/root/node/out'
make: *** [node] Error 2
とエラーが発して、この後に make install を実行してもインストールできない。
# make install
make -C out BUILDTYPE=Release
make[1]: Entering directory `/root/node/out'
ACTION v8_snapshot_run_mksnapshot /root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc
pure virtual method called
terminate called without an active exception
/bin/sh: line 1: 16851 Aborted "/root/node/out/Release/mksnapshot" --log-snapshot-positions --
logfile "/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.log"
"/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc" make[1]: *** [/root/node/out/Release/obj.target/v8_snapshot/geni/snapshot.cc] Error 134
make[1]: Leaving directory `/root/node/out'
make: *** [node] Error 2
となってインストールに失敗する。

今度は --without-snapshot オプション付きでインストールを試してみる。

snapshot関連でエラーが発生しているみたいなので、ググってみるとよく見かける--without-snapshotオプションつきでやってみる。(※OSの再インストールをやってから)
# git clone git://github.com/joyent/node.git
# cd node
# ./configure --without-snapshot
# make
とこんどは


LINK(target) /root/node/out/Release/node
LINK(target) /root/node/out/Release/node: Finished
TOUCH /root/node/out/Release/obj.target/node_dtrace_header.stamp
TOUCH /root/node/out/Release/obj.target/node_dtrace_provider.stamp
TOUCH /root/node/out/Release/obj.target/node_dtrace_ustack.stamp
make[1]: Leaving directory `/root/node/out'
ln -fs out/Release/node node
となり、この後に make install を実行しても
# make install
make -C out BUILDTYPE=Release
make[1]: Entering directory `/root/node/out'
make[1]: Nothing to be done for `all'.
make[1]: Leaving directory `/root/node/out'
ln -fs out/Release/node node
out/Release/node tools/installer.js install
pure virtual method called
terminate called without an active exception
make: *** [install] Aborted
となって結局インストールに失敗する。

wgetでインストールすると

しかし、wgetでインストールすると(※これもOSの再インストールをしてから。ただしopenssl-develをインストール後)
# wget http://nodejs.org/dist/node-latest.tar.gz
# tar zxvf node-latest.tar.gz
# cd node-v0.6.17
# ./configure
# make


Waf: Leaving directory `/root/node-v0.6.17/out'
'build' finished successfully (4m54.537s)
-rwxr-xr-x 1 root root 11M May 7 21:42 out/Release/node
となり、make install すると
# make install


Waf: Leaving directory `/root/node-v0.6.17/out'
'install' finished successfully (0.755s)
インストールが成功する。
# node --version
v0.6.17

git clone でのインストールはどうやればうまくいくのだろうか。もしご存知のかたはよろしければご教授のほどお願いします。

追記

@koichikさんご教授ありがとうございます。
教えてくださった git checkout を行えば、./configure以降はwgetと同様なログ表示となり、インストールが成功しました。
# git clone git://github.com/joyent/node.git
# cd node
# git checkout v0.6.17
# ./configure
# make
# make install

ブラウザからmbedをLチカ

まあ、前回のブログ記事「mbedにWebSocketサーバーを載せてみた。」のコードにちょっと修正を加えただけのもの。

バイナリーフレームを受け取ったら、そのデータをledに出力させるというものです。



main.cppファイル



こちらにパブリッシュしたのでどのようなコード体系になっているのかが見れると思います。

あとはLANケーブルでつないで実行したら、jsdo.itにアクセスして、mbedのIPアドレスを入力してセットボタンを押します。

すると■ボタンが4つ表示されますのでクリックするとmbedがLチカできます。

mbed に WebSocketサーバーを載せてみた。

WebSocket関連で検索するのがほぼ日課みたいなものとなっています。
そんなある日、mbedというものを見つけました。

mbedとは?

私も触り始めたばかりで偉そうに説明できるほどではありませんが、第一印象として「簡単」ということ。
54mmx26mmの小さな基盤にARMのCPUが搭載されたものです。
青い基盤のもの(mbed NXP LPC1768)と、黄色い基盤のもの(mbed NXP LPC11U24)ものとがあります。(他にもいくつか種類があるみたいです。)
青い方はARM Cortex-M3を搭載しており、黄色い方はスペックを落として低消費電力化したものとなっており、ARM Cortex-M0を搭載しています。アキバではマルツ千石電商秋月電子などで入手可能です。 開発用IDEクラウドで用意されており、いつでもどこでもブラウザ上で開発ができるというのが素晴らしいところです。なお、オフラインでの開発環境もできるみたいです。
その他の詳しい内容はここでは割愛させていただきます。

WebSocketサーバーを載せる

mbed用にWebSocketクライアントが開発されているのが見つけるきっかけでした。
しかし、ソースを見てみるとhybi-00(hixi-76)の古いプロトコルバージョンを使用して作成されたもので、RFCに対応していないものでした。 ですので、RFCに対応したWebSocketを載せてみようと思います。
@masato_kaさんのブログのコメントにてwebsockets_hello_world_ethernetをインポートすればというコメントが有りましたのでインポートしてみるとRFCに対応したWebSocketライブラリが使用されていました。
ただ、WebSocketクライアントを載せた場合は別途サーバーが必要となります。そこで、WebSocketサーバーをmbedに載せればブラウザーから直接データのやり取りが行えるようになるのではと思い挑戦してみたら出来たというお話です。

ソース

SHA1ハッシュの計算を行うにあたり、NetServiceSourceからsha1.h, sha1config.h, sha1.cのソースを使用させていただいています。
私は、C++はまったくといっていいほど触ったことがないため、いろいろとわからないままで組んでおりますのでご理解の程。警告は読んでわからないものは放置しています。
main.cppのソースを以下に掲載します。

なお、今回のプロジェクトファイル一式は下記のリンクからダウンロードできます。
mbed_WebSocketServer.zip

ほとんどコピペなソースなので主要なonLinkSocketEvent関数部分のみ載せます
void onLinkSocketEvent(TCPSocketEvent e) {
    switch (e) {
        case TCPSOCKET_CONNECTED:
            printf("TCP Socket Connected\n");
            break;
        case TCPSOCKET_WRITEABLE:
            //Can now write some data...
            printf("TCP Socket Writable\n");
            break;
        case TCPSOCKET_READABLE:
            //Can now read dome data...
            printf("TCP Socket Readable\n");
            // Read in any available data into the buffer
            char buff[1024];
            while ( int len = link->recv(buff, 1024) ) {
                // And send straight back out again
                //link->send(buff, len);
                
                if (wsState == 0) {
                    // ハンドシェイクステート
                    buff[len]=0; // make terminater
                    printf("%s\n", (char*)buff);
                    for (int i = 0; i < len; i++) {
                        if (buff[i] == 'K' && buff[i + 1] == 'e' && buff[i + 2] == 'y') {
                            for (int j = i + 1; j < len; j++) {
                                if (buff[j] == '\r') {
                                    i += 5;
                                    int keyLen = j - i;
                                    char strKey[keyLen + 1];
                                    strKey[keyLen] = 0;
                                    // Sec-WebSocket-Keyフィールドの値をstrKeyに取得
                                    strncpy(strKey, buff + i, keyLen);
                                    // Acceptデータ作成用の固定GUID文字列
                                    char guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

                                    // Sec-WebSocket-Acceptデータを作成
                                    strcat(strKey, guid);
                                    unsigned char hash[20];
                                    sha1((unsigned char*)strKey,strlen((char*)strKey),hash);
                                    string accept = encode64((char*)hash, 20);

                                    // ハンドシェイクレスポンスを作成
                                    string hsRes = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
                                    hsRes += accept;
                                    hsRes += "\r\n\r\n";
                                    // ハンドシェイクレスポンスデバッグ出力
                                    printf("%s\n", hsRes.c_str());
                                    
                                    // ハンドシェイクレスポンスをクライアント(ブラウザー)に送信
                                    link->send(hsRes.c_str(), hsRes.size());
                                    // データ送受信ステートに移行する
                                    wsState = 1;
                                    return;
                                }
                            }
                        }
                    }
                } else {
                    // データ受信ステート
                    // ここでクライアントから送られてきたデータを処理する。
                    // 今回は送られてきたデータをデバッグ出力し、
                    // そのままクライアント(ブラウザー)に送信する。
                    // なお単純にエコーで返す場合は以下の1行だけでいい。
                    // link->send(buff, len);
                    bool fin = (buff[0] & 0x80) == 0x80;
                    int opcode = buff[0] & 0x0f;
                    if(opcode == 9) {
                        // Ping(opcode = 0x9)が送られた場合は、Pong(opcde = 0xA)を返す 
                        buff[0]++;
                        link->send(buff, len);
                        return;
                    }
                    int dataLen = buff[1] & 0x7f;
                    if (!fin || dataLen > 125) {
                        // 今回は、FINフラグが立っていないフレーム、
                        // または126バイト以上のデータは扱わないことにする
                        link->close();
                        return;
                    }
                    int i = 0;
                    // アンマスクを行う
                    for (i = 0; i < dataLen; i++) {
                        buff[6 + i] = buff[6 + i] ^ buff[2 + (i % 4)];
                    }
                    if(opcode == 1) {
                        // 送られてきたフレームがテキストフレームの場合
                        // 送られてきたテキストデータをデバッグ出力
                        char dispData[dataLen + 1];
                        strncpy(dispData, buff + 6, dataLen);
                        dispData[dataLen] = 0;
                        printf("%s", dispData);
                    }
                    // 送信フレームの作成
                    char sendData[2 + dataLen + 1];
                    sendData[0] = buff[0];
                    sendData[1] = buff[1] & 0x7f;
                    for (i = 0; i < dataLen; i++) {
                        sendData[2 + i] = buff[6 + i];
                    }
                    sendData[2 + dataLen] = 0;
                    // クライアント(ブラウザー)に送信
                    link->send(sendData, 2 + dataLen);
                }
            }
            break;
        case TCPSOCKET_CONTIMEOUT:
            printf("TCP Socket Timeout\n");
            break;
        case TCPSOCKET_CONRST:
            printf("TCP Socket CONRST\n");
            break;
        case TCPSOCKET_CONABRT:
            printf("TCP Socket CONABRT\n");
            break;
        case TCPSOCKET_ERROR:
            printf("TCP Socket Error\n");
            link->close();
            break;
        case TCPSOCKET_DISCONNECTED:
            printf("TCP Socket Disconnected\n");
            // wsStateをリセット
            wsState = 0;
            link->close();
            break;
        default:
            printf("DEFAULT\n");
    }
}

LANのモジュラ・ジャックの増設

mbed単体ではLANケーブルを繋げることは困難ですので(直接ハンダ付けすれば出来なくは無いですが)、LANの(RJ45)モジュラ・ジャックを増設する必要があります。
LANのモジュラ・ジャックは千石電商などでキットとして販売されていますのでこちらを購入したほうがいいでしょう。 また、☆Board Orangeというmbed用のベースボードのも販売されています。このボードはmicroSD、USB(Host)、キャラクターLCDなどのI/Oインターフェイスを簡単に増設できるベースボードとなっています。このベースボードも千石電商などで入手可能です。(mbed用のベースボードは他にもあるようです。)

準備

mbedとPCをUSBとLAN(クロス)ケーブルで接続します。
プログラムのソースは固定IPで接続するように組んでいます。DHCPでIPを自動で割り振る場合は割り振られたmbedのIPアドレスが分かる手段を整えてください。
また、Windowsの方はmbedのシリアルポートドライバーインストールし、Tera Termなどのシリアル通信モニターソフトを起動します。MacLinuxの方はドライバーは不要なようですので、シリアル通信モニターのみ用意し起動します。

実行

サンプルプログラムは固定IPで接続するように組んでいます。DHCPで接続したい場合は適宜ソースを修正してください。
また、1:1での接続のみ対応しています。
なお、ブラウザープロトコルバージョンhybi-07以上に対応したChromeまたはFirefox(Windowsの場合)で接続してください。 コンパイルしてできたbinファイルをmbed(USBで接続するとマスストレージとして認識されます)にコピーまたは移動します。
コピーまたは移動したらmbedのリセットボタンを押し実行します。
シリアル通信モニターソフトの画面にListeningと表示されたら、プロジェクトファイル一式内にあるmbedWebSocketTest.htmを開きます。
(なお、Chromeで開く場合は、Chromeが起動している場合はすべて閉じたあとにChromeを --allow-file-access-from-files オプション付きで起動したあとに開いてください。)
開いたら、テキストボックスに半角で文字列を入力しエンターキーを押したら送信されます。
送信されたデータはシリアル通信モニターソフトの画面に
received data:送信データ
と表示されます。
また、mbed側では受診したデータをブラウザーにそのまま返しますので、ブラウザーのテキストボックスの下にも送信された文字列が表示されます。

最後に

今回は、WebSocketサーバーを実装しテキストデータの送受信を行いました。WebSocketプロトコルはバイナリデータの送受信にも対応していますので(Chromeはすでにバイナリデータの送受信に対応していますが、Firefoxはバージョン11から対応します。)、画像データなどの送受信も可能です。

WebRTCに関する疑問

最近、Chromeのdev版およびCanary版にWebRTC関連のAPIが実装され話題になっています。(実は、去年すでにWebRTCを実装しているブラウザーが存在しているみたい。)
私も、興味を持ってWebRTCのことを調べ始めているところです。
恥ずかしながらWebRTCを調べて初めて知ったのがSTUNというもの。
このSTUNがどういうものかを調べてみました。
参考資料
STUNはいわゆるNAT越えを行うためのプロトコルらしいです。
STUNはサーバーを設置して、そのサーバーにリクエストを投げるとリクエストを投げた端末のグローバルIPを返します。あとは他の端末からそのグローバルIPで接続を行えばP2P通信が行えるというものらしいです。
"らしいです"というのはすでにこの時点で理解ができなくなっているからです。

疑問その1 相手にどうやってグローバルIPアドレスを教えるのか、逆に相手のグローバルIPアドレスをどうやって取得するのか?

STUNサーバーがIPアドレスを管理してくれるのでしたら(いわゆるスーパーノード)少しは理解できるのですけど、調べてみるとSTUNサーバーの役目は単にグローバルIPアドレスを教えるためだけみたいですね。
そうなってくると、どうやってP2P接続を開始するのでしょうか? つまり、相手にどうやってグローバルIPを教えるのでしょうか? 逆に相手のグローバルIPアドレスをどうやって取得するのでしょうか?
また、IPアドレスなどの管理を行うために別のサーバーを用意しないといけないのでしょうか?となってくると、なんかWebSocketより色々と用意しないといけなくなりますね。
そもそもこのような疑問を持ったきっかけは、サンプルコードが
pc = new webkitPeerConnection("STUN stun.l.google.com:19302", onSignalingMessage);
pc.onaddstream = onAddStream;
pc.onremovestream = onRemoveStream;
と言った感じとなっており、アドレスといったものはSTUNサーバーと思われるアドレスだけだからです。 もし、STUNのことに詳しい方がいっらっしゃればどうかご教授のほどよろしくお願いします。

疑問その2 レンタルサーバーにSTUNサーバーを立てるのは無理?

STUNサーバーはその仕組みにより、(NATの種類がフルコーンじゃない場合はIPアドレスを変えて接続を行うため)IPアドレスを2つ持つ必要があるそうです。
となると自分で試したい場合はIPアドレスを1つだけしか割り当てられないレンタルサーバーの場合はSTUNサーバーを立てることができないのでしょうか?


あと、以降の疑問はまだ時期尚早と思われますが気になっているのでちょっと取り上げます。

疑問その3 WebRTCではVIDEOやAUDIOなど以外のデータはP2Pでやり取りできない?

WebRTCのサンプルコードを見てみるとメッセージ(チャット)の送受信はXHRを使用しています(相手からのメッセージ取得は、XHRのタイムアウトのコールバックで再度、メッセージ取得リクエストを投げるという方法をとっているようです)。
WebRTCではこういったテキストやバイナリデータ(特に対戦ゲームを行なっている時のコントローラー入力情報など))はP2Pで送受信できないのでしょうか?
一応、WebKitGTK+というブラウザーに実装されたWebRTCの解説を見るとPeerConnectionオブジェクトにはSendメソッドがあり、このメソッドでメッセージを送信できるようなことが書かれています。しかし、W3CのWebRTC仕様のページにはPeerConnectionにはsendメソッドがありませんし、Chromeに実装されたwebkitPeerConnectionオブジェクトにもsendメソッドがありませんでした。

疑問その4 デバイスは選択できない?

今のところ仮想カメラ+ノートカメラでしか試していないのですが、複数のデバイスが接続されている場合は選択ダイアログを出して欲しいのですけどね。将来実装する予定で今はまだ実装されていないのだと期待しましょう。

疑問その5 デバイスによって認識されない?。

これはとても気になるところなんですが、色々と試したところ以下のとおりとなりました。(同様に仮想カメラ+ノートカメラでしか試していません)
認識できたものの共通点としてKsproxyドライバを使用しているところです。(たぶん出力フォーマットが大いに関係していると思われます。YUVフォーマット対応のもの)
認識出来なかったもの
  • ニコ生デスクトップキャプチャー(NDC)の仮想カメラ
  • ニコ生ビジュアルステーション(NVS)の仮想カメラ。
  • ニコ生Effectの仮想カメラ。
  • Virtual Desktop Camera for Skypeの仮想カメラ
認識できたもの
  • USB 2.0 2.0M UVC WebCam (ASUS ノート G51Jx 3Dについてるカメラ)
  • WebCamMax(仮想カメラ)
  • ManyCam(仮想カメラ)

疑問その6 WebSocketを使用したWebRTC

前日開催された東北デベロッパーズカンファレンスで、@agektmrさんが、WebSocketを使用したWebRTC?をちょこっと言っていました。
これは、P2Pではないにせよ、WebSocketを使用して映像などをやり取りできるということだと思うのですが、私がすぐに思いついたのは、Media Source APIを使用した方法です。
Media Source APIはチャンクを次々に送信し、クライアントは受信したチャンクをSourceAppendメソッドで追加していくというものです。
しかし、こちらではこのMedia Source APIを使用していません。
ソース(wow_feature.js)を見てみたいものですが、アップされていないようで見ることができませんでした。
WebSocketをつかった映像のやり取りはどのようなコードとなるのでしょうか?

疑問その7 ChromeとはWebMに対応したブラウザーにしかつなげることができない?

ChromeのvideoタグはWebMのみしかサポートしていませんよね?たしか。Chromeは映像をWebRTCで送信するときWebMにエンコードしながら送信しているのでしょうか?
となると、WebMに対応したブラウザーのみにしか映像を受信できないのでは?

以上、現在、私が抱いているWebRTCに関する疑問です。

3.WebSocketプロトコルバージョンhybi-07〜RFC(hybi-17)仕様解説編 ずっとβ版

ブラウザーは現時点(2012/02/01)での各最新のブラウザーを対象とします。(Chrome16,Firefox10,Opera11,Safari5)
ブラウザーに実装されているWebSocket(API)のことを"WebSocketクライアント"と呼ぶことにします。
※単に"hybi-"で始まる単語はプロトコルバージョンを指す言葉とします。また、hixi-76はhybi-00と同じものですのでhixi-76をhybi-00と呼ぶことにし、hybi-17がRFC6455の仕様となったためhybi-17を単にRFCと呼ぶことにします。
※2012/04/24 マスクについて勘違いしていました。hybi-15からはサーバーからクライアントにデータを送る場合は必ずマスク無しで送らないといけないようです。その部分を修正しました。

気になったところなどがありましたらコメントにてツッコんでください。

一気にhybi-07以降の仕様の解説です。
というのも、hybi-07からRFCのハンドシェイク処理およびデータ送受信時のフレーム構成、クローズ処理においてはほとんど同じためです。(これ以外の処理においては違いはあります。)
違いがあるとすれば、hybi-11以降からはhybi-04〜hybi-10で使用されたSec-WebSocket-Originというフィールド名が"Origin"に変更されたというところだけです。(hybi-03以前でも"Origin"が使用されていたため"戻された"という表現が正しいかな?)
このことにより、hybi-07に対応したWebSocketサーバーを(フィールド名の違いや細かい仕様の違いは吸収して)実装すれば(Sec-WebSocket-Versionのチェックを行わなければ)RFCでもつながるサーバーとなります。

英語が読める方はRFC6455の仕様を見ながら読み進めていただければ理解がしやすかと思います。(逆に仕様と違う所があればツッコミお願いしますw)

ハンドシェイク処理

Chromeのハンドシェイクリクエストを例に見てみましょう。
GET /Chrome HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:8181
Origin: http://d.hatena.ne.jp
Sec-WebSocket-Key: 7r5Lzy+riXX12fjRYxBGMw==
Sec-WebSocket-Version: 13
hybi-07以降のハンドシェイクリクエストで送られてくるフィールドは
  • Upgrade
  • Connection
  • Host
  • Origin
  • Sec-WebSocket-Key
  • Sec-WebSocket-Version
の6つです。
続いて、ハンドシェイクレスポンスの例を見てみます。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
hybi-00との大きな違いは、HTTPプロトコルでのコンテンツ部分(バイナリデータ部分)がなく、ヘッダーのみかつすべて有効な文字でハンドシェイク処理を行っているというところです。
聞いた話なのですが、hybi-00では特にプロキシを通したときにこのコンテンツ部分が削られてたものがサーバーに届き、ハンドシェイクが行えずWebSocketをつなぐことができないという状況が発生するようです。
ハンドシェイクの仕様変更はセキュリティの問題もそうですが、こういった障害に対する対応も含めたものと思われます。
hybi-07以降のハンドシェイクレスポンスに含めなければならないフィールドは
  • Upgrade
  • Connection
  • Sec-WebSocket-Accept
の3つとなります。 hybi-07以降のハンドシェイク処理においてもhybi-00のハンドシェイク処理同様、サブプロトコルが設定された場合は、Sec-WebSocket-protocolフィールドがリクエストおよびレスポンスに追加されます(今回も同様にサブプロトコルに関する解説は割愛させていただきます)。
各行およびフィールドに設定する値は次のようになります。
HTTPレスポンスヘッダー "HTTP/1.1 101 Switching Protocols"固定です。
Upgradeフィールド "websocket"固定です。(大文字小文字の区別はありません。)
Connectionフィールド "Upgrade"固定です。(大文字小文字の区別はありません。)
Sec-WebSocket-Acceptフィールド ハンドシェイクリクエストのSec-WebSocket-Keyの値から定められた方法で作成した文字列を設定します。詳しくは次の「Sec-WebSocket-Acceptフィールド値の作成方法」で解説します。
Sec-WebSocket-Acceptフィールド値の作成方法
ハンドシェイクリクエストで送られてくるSec-WebSocket-Keyフィールドの値と"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"(このGUID文字列は固定です)を文字列結合したものからSHA1ハッシュを計算した結果をBase64に変換したものがSec-WebSocket-Acceptフィールドに設定する値となります。
例として、ハンドシェイクリクエストで
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
という値から、Sec-WebSocket-Acceptフィールドの値を作成してみます。
  1. "dGhlIHNhbXBsZSBub25jZQ=="と"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を文字列結合 = "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  2. "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"からSHA1ハッシュ値を計算 = sha1[0] = 0xb3, sha1[1] = 0x7a, sha1[2] = 0x4f, sha1[3] = 0x2c ...
  3. SHA1ハッシュ値Base64文字列に変換 = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
この"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="をSec-WebSocket-Acceptフィールドに設定します。
これらの値を設定してハンドシェイクレスポンスを返します。
ハンドシェイクレスポンスを返す処理をC#のコードで組みますと以下のようになります。
// Sec-WebSocket-Keyの値を取得
string webSocketKey = Regex.Match(handshakeRequest, "Sec-WebSocket-Key: (.+?)\r\n", RegexOptions.IgnoreCase).Groups[1].Value;
// ハンドシェイクリクエストの固定部分文字列作成
string responseString = "HTTP/1.1 101 Switching Protocols\r\n" +
                        "Upgrade: websocket\r\n" +
                        "Connection: Upgrade\r\n" +
                        "Sec-WebSocket-Accept: {0}\r\n\r\n";
// Sec-WebSocket-Keyの値と"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を文字列結合し、SHA1ハッシュを計算後Base64文字列に変換する
string webSocketAccept = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(webSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")));
// Sec-WebSocket-Acceptに値を設定
handshakeResponse = Encoding.UTF8.GetBytes(string.Format(responseString, webSocketAccept));
// ハンドシェイクレスポンスを返す
socket.Send(handshakeResponse);

データ送受信処理

hybi-00と同様に、hybi-07以降においてもフレームにしてから送信します。ただし、hybi-00でのフレーム構成と比べると結構複雑なフレーム構成となっています。
hybi-07以降のフレーム構成図を見てみましょう。一番上の数字はビットオフセットの値です。
      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+
フレームの構成を見てみます。
バイト位置
()は2バイト目下位7ビットの値
ビット位置 説明
1バイト目 先頭ビット 1バイト目の先頭ビットはFINフラグとなっており、このフラグが0の場合は分割したフレームで送信されたときにまだ次のフレームがあることを示します。普段は、このFINフラグを1に設定して(最終フレーム)データを送ります。(このフラグの使い道がいまだによくわかっていません)
下位4ビット opcodeの値が入ります。このopcodeの値でこのフレーム(のデータ)がどんなものかを識別します。
opcodeの値 データ種別
0x0 継続フレーム:(FINフラグとともに使用されるもの?すいません。この値もよくわかっていません。)
0x1 テキストフレーム:文字列データ
hybi-00と同様に、テキストデータの文字コードUTF-8を使用しなければなりません。
また、テキストフレームを受信した場合はデータがUTF-8の有効な文字列かチェックしなければならなくなりました。
0x2 バイナリフレーム:バイナリデータ
Firefox10はまだバイナリデータの送受信に対応していません。
0x3〜0x7 Reserved:未使用
0x8 クローズフレーム:このフレームを受信したら接続を切ります。
0x9 Pingフレーム:Pingを送る場合はopcodeをこの値に設定して送ります。
0xA Pongフレーム:Pongを返すときにopcodeをこの値に設定します。
0xB〜0xF Reserved:未使用
2バイト目 先頭ビット 2バイト目の先頭ビットはMASKフラグとなっており、このフラグが1の場合はデータがマスクされています。なお、クライアントからサーバーにデータを送信する場合は必ずマスクを行わなければなりません。逆にサーバーからクライアントにデーターを送るときはマスクなしで、hybi-15からは"MUST"が付き必ずマスク無しで送信するようにとなっています。
下位7ビット ここにはデータ長か、126または127が入ります。
説明
126未満 126未満の場合は、その値がデータ長となります。
126 このバイトの後に続く2バイトの値がデータ長となります。
127 このバイトの後に続く8バイトの値がデータ長となります。
MASKフラグ=1の場合のみ
3〜6バイト目(126未満)
5〜8バイト目(126)
10〜13バイト目(127)
MASKフラグが0の場合はこの4バイトはなくデータ長の後にデータが続きます。MASKフラグが1の場合、データ長のあとに続く4バイトにはマスクキーが入ります。マスクされたデータをこのマスクキーでアンマスクすれば元のデータに戻すことができます。マスクについては次の「データのマスク」の項目で解説します。
MASKフラグ=0の場合
3バイト目以降(126未満)
5バイト目以降(126)
10バイト目以降(127)

MASKフラグ=1の場合
7バイト目以降(126未満)
9バイト目以降(126)
14バイト目以降(127)
ここにデータが入ります。
データのマスク
マスク方法は4バイトのランダムなデータを作成し、これをマスクキーとします。送信するデータをこのマスクキーとのXORしたものがマスクデータとなります。
マスクは、4バイトごとにループして行います。
実際にC#でマスク処理を書くと以下のようになります。
//byte[] sendData = 送信データ;
byte[] maskedData = new byte[sendData.Length];
byte[] maskKey = ランダムな4バイト;
for(int i = 0; i < sendData.Length; i++)
{
    maskedData[i] = sendData[i] ^ maskKey[i % 4];
}
// maskedDataを送信フレームに設定する。
同様に、マスクされたデータをマスクキーでXORすれば元のデータとなります。
//byte[] maskedData = マスクデータ;
//byte[] maskKey = マスクキー;
byte[] data = new byte[maskedData.Length];
for(int i = 0; i < maskedData.Length; i++)
{
     data[i] = maskedData[i] ^ maskKey[i % 4];
}
// テキストフレームの場合はUTF8エンコードで文字列にする。
string str = Encoding.UTF8.GetString(data);
これらのことを踏まえて、送信データ引数にWebSocketのフレームに変換したものを返す関数を実装すると以下のようなコードとなります。
// サーバー側の実装のためマスク処理は省略しています。
// 第1引数:送信するデータ。文字列を送信する場合は、バイト配列にしてからこの関数を呼ぶ。
// 第2引数:opcode
private byte[] WebSocketFrameEncode(byte[] data, byte opcode)
{
    byte[] webSocketFrame;
    // フレームのヘッダーを入れる配列
    byte[] hd;
    if (data.Length <= 125)
    {
        // ヘッダーを2バイトにする
        hd = new byte[2];
        // データ長が126未満の場合は2バイト目にデータ長を設定
        hd[1] = (byte)data.Length;
    }
    else if (data.Length <= 65535)
    {
        // ヘッダーを4バイトにする
        hd = new byte[4];
        // データ長が126以上65536未満の場合は2バイト目に126を設定
        hd[1] = (byte)126;
        // データ長をバイト配列に変換
        byte[] lenData = BitConverter.GetBytes((UInt16)data.Length);
        // エンディアンが逆なため配列を反転
        Array.Reverse(lenData);
        // フレームヘッダーの3バイト目からデータ長をコピー
        Array.Copy(lenData, 0, hd, 2, 2);
    }
    else
    {
        // ヘッダーを10バイトにする
        hd = new byte[10];
        データ長が65536以上の場合は2バイト目に127を設定
        hd[1] = (byte)127;
        // データ長をバイト配列に変換
        byte[] lenData = BitConverter.GetBytes((UInt64)data.Length);
        // エンディアンが逆なため配列を反転
        Array.Reverse(lenData);
        // フレームヘッダーの3バイト目からデータ長をコピー
        Array.Copy(lenData, 0, hd, 2, 8);
    }
    // FINフラグとopcodeを設定する
    hd[0] = 0x80 | opcode;
    // ヘッダーと送信データをつなげる
    webSocketFrame = new byte[hd.Length + data.Length];
    Buffer.BlockCopy(hd, 0, webSocketFrame, 0, hd.Length);
    Buffer.BlockCopy(data, 0, webSocketFrame, hd.Length, data.Length);
    // 作成したフレームを戻す
    return webSocketFrame;
}
続いて、WebSocketのフレームからデータを取り出す関数を実装すると以下のようなコードとなります。
private byte[] WebSocketFrameDecode(byte[] webSocketFrame)
{
    // FINフラグ、Opcode、MASKフラグ、データ長を取得
    bool fin = (webSocketFrame[0] & 0x80) == 0x80;
    byte opcode = webSocketFrame[0] & 0x0f;
    bool mask = (webSocketFrame[1] & 0x80) == 0x80;
    // 一応、データ長をUInt64として宣言する。
    UInt64 payloadLen = (UInt64)(webSocketFrame[1] & 0x7f);
    // データのオフセットを2に初期化する
    int offset = 2;
    if (payloadLen == 126)
    {
        // データ長が126だった場合
        // WebSocketフレームからデータ長のバイト配列(2バイト)を取得
        byte[] lenArray = new byte[2];
        Array.Copy(webSocketFrame, offset, lenArray, 0, 2);
        // エンディアンが逆なので反転
        Array.Reverse(lenArray);
        // バイト配列からデータ長を取得
        payloadLen = BitConverter.ToUInt16(lenArray, 0);
        // データのオフセットを+2する
        offset += 2;
    }
    else if (payloadLen == 127)
    {
        // データ長が127だった場合
        // WebSocketフレームからデータ長のバイト配列(8バイト)を取得
        byte[] revArray = new byte[8];
        Array.Copy(webSocketFrame, offset, revArray, 0, 8);
        // エンディアンが逆なので反転
        Array.Reverse(lenArray);
        // バイト配列からデータ長を取得
        payloadLen = BitConverter.ToUInt64(revArray, 0);
        // データのオフセットを+8する
        offset += 8;
    }
    byte[] maskKey = new byte[4];
    if (mask)
    {
        // MASKフラグがたっていた場合(=1)マスクキーを取得する
        Array.Copy(webSocketFrame, offset, maskKey, 0, 4);
        // データのオフセットを+4する
        offset += 4;
    }
    // データを取得する
    payloadData = new byte[payloadLen];
    Array.Copy(webSocketFrame, offset, payloadData, 0, payloadLen);
    if (mask)
    {
        // MASKフラグがたっていた場合はアンマスクを行う
        for (int i = 0; i < payloadLen; i++)
        {
            payloadData[i] = (byte)(payloadData[i] ^ maskKey[i % 4]);
        }
    }
    // フレームから取り出したデータを戻す
    return payloadData;
}

Ping(Pong)処理

hybi-01からPing(Pong)を仕様において定義されるようになりました。ただし、hybi-06まではhybi-01と同じフレーム構成およびopcode体系が使用されていましたが、hybi-07以降においてはフレーム構成の変更とともにopcodeの体系も変更となりました。hybi-01〜hybi-06ではopcode=0x2がPing、opcode=0x3がPongであったのに対し、hybi-07以降はopcde=0x9がPing、opcde=0xAがPongとなりました。
Pingで送られてきたデータをそのままPongとして返さなければならないと仕様ではなっています。
ただし、Pingを送る際にペイロードデータはなくてもいいらしいです。つまり、0x89 0x10 0x9a 0x6e 0x33 0x72という感じで(0x9a 0x6e 0x33 0x72はマスクキー)、ペイロードデータがないフレームでもかまいません。(ちょっと自信ない)
Pongを返す場合は、Pingで送られてきたフレームの1バイト目を0x8Aにしてそのまま返せばいいです。
C#コード
byte[] receiveFrame = 送られてきたPingフレーム;
receiveFrame[0] = 0x8A;
// Pongを返す
socket.send(receiveFrame);

クローズ処理

hybi-00と同様、クローズにおいてもハンドシェイクを行うようですが、すいません。英語が読めないのでめhybi-07以降におけるハンドシェイクの詳しい仕様がわからないため、クロー処理の解説は省略します。 ただ、opcode=0x8のフレームを受信した場合は、接続を切ります。
hybi-00での解説でも書きましたがSocketオブジェクトのCloseメソッドを実行するだけでもとりあえずは問題ないかと思います。


以上を踏まえてプログラムを組めば、とりあえずWebSocketでデータの送受信が行えるhybi-07以降に対応したサーバーの実装ができると思います。

2.WebSocketプロトコルバージョンhybi-00(hixi-76)仕様解説編 ずっとβ版

ブラウザーは現時点(2012/02/01)での各最新のブラウザーを対象とします。(Chrome16,Firefox10,Opera11,Safari5)
ブラウザーに実装されているWebSocket(API)のことを"WebSocketクライアント"と呼ぶことにします。
※単に"hybi-"で始まる単語はプロトコルバージョンを指す言葉とします。また、hixi-76はhybi-00と同じものですのでhixi-76をhybi-00と呼ぶことにします。

気になったところなどがありましたらコメントにてツッコんでください。

さて、今回はWebSocketプロトコルの仕様を(私が理解している範囲で)解説します。サーバーの実装を目標としますのでサーバーを中心にして解説します。
とりあえず最低限通信(データ送受信)ができるレベルを目標としますので、ハンドシェイク、データ送受信、Ping(Pong)、クローズの4つの処理に関する部分のみとします。
実装WebSocketクライアント対応プロトコルバージョン確認編で、ブラウザーによって対応するプロトコルバージョンが違うということがわかりました。どのブラウザーがどのプロトコルバージョンに対応しているのかをここで再度確認しましょう。
ブラウザー プロトコルバージョン
Chrome RFC(hybi-17)
Firefox(PC版及びAndroid版) hybi-10
Opera(PC版およびAndroid版) Safari(PC版およびiOS版) hybi-00
英語が読める方はhybi-00の仕様を見ながら読み進めていただければ理解がしやすかと思います。(逆に仕様と違う所があれば教えて下さいw)

ハンドシェイク処理

ハンドシェイク処理は、WebSocketクライアントから送られてくるハンドシェイクデータ(以降、ハンドシェイクリクエストと呼ぶことにします。)を受信し、それに対するハンドシェイクデータ(以降、ハンドシェイクレスポンスと呼ぶことにします。)を返し、WebSocketクライアントがハンドシェイクレスポンスを検証後、(検証が通れば)接続を開始する処理です。
ハンドシェイク処理はHTTPプロトコルを使用して行います。
ハンドシェイクリクエストの例を見てみます。
GET /Safari HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: localhost:8181
Origin: http://www.apple.com
Sec-WebSocket-Key1: c98  713K41 x  05
Sec-WebSocket-Key2: 2m&0  1t` 1Q| 7V   0GM5    4<U9A8

?,m?YZ??
HTTPプロトコルヘッダーのGETメソッド文の他に、ハンドシェイクリクエストで送られてくるフィールドは
  • Upgrade
  • Connection
  • Host
  • Origin
  • Sec-WebSocket-Key1
  • Sec-WebSocket-Key2
の6つです。この他にもWebSocketクライアント側でサブプロトコルを指定した場合はSec-WebSocket-Protocolというフィールドも送られてきますが、今回はサブプロトコルに関する解説は割愛させて頂きます。
ハンドシェイクリクエストの例を見ると最後に文字化けしたようなデータがあります(8バイトのバイナリデータ)。これも重要なデータとなります。
ハンドシェイクリクエストを元にハンドシェイクレスポンスを返します。
ハンドシェイクレスポンスの例を見てみましょう。
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo

8jKS'y:G*Co,Wxa-
ハンドシェイクレスポンスに含めなければならないフィールドは、Upgrade,Connection,Sec-WebSocket-Origin,Sec-WebSocket-Locationの4つです。
各行およびフィールドに設定する値は次のようになります。
HTTPレスポンスヘッダー "HTTP/1.1 101 WebSocket Protocol Handshake" 固定です。
Upgradeフィールド "WebSocket" 固定です。(大文字小文字の区別はありません。)
Connectionフィールド "Upgrade" 固定です。(同様に大文字小文字の区別はありません。)
Sec-WebSocket-Originフィールド ハンドシェイクリクエストのOriginフィールドの値をそのまま設定します。
上記のハンドシェイクリクエストの値を例にすると
Sec-WebSocket-Origin: http://www.apple.com
となります。
Sec-WebSocket-Locationフィールド "ws://" + ハンドシェイクリクエストのHostフィールドの値 + GETメソッド文に含まれるパスを設定します。
上記のハンドシェイクリクエストを例にすると
Sec-WebSocket-Location: ws://www.apple.com/Safari
となります。
上記以外にハンドシェイクリクエストの最後に文字化けしたようなデータ(16バイトのバイナリデータ)があります。ここには定められた方法によって作成されたデータを設定します。作成するデータのことをAcceptデータと呼ぶことにします。
Acceptデータの作成方法
AcceptデータはハンドシェイクレスポンスのSec-WebSocket-key1の値、Sec-WebSocket-Key2の値、ハンドシェイクリクエストの最後の8バイトバイナリデータを使用して作成します。
Sec-WebSocket-key1の値に含まれる数字部分を抜き出し結合してそれを数値にしたものを、値に含まれるスペースの個数で割った値をバイナリデータ(4バイト)に変換します。
例として Sec-WebSocket-Key1: 3 7 78B 67  5      W%89
という値でバイナリデータをを求めてみます。
  • 数字を抜き出して数値に変換した値 = 377867589
  • スペースの数 = 11
  • 数値 / スペースの数 = 377867589 / 11 = 34351599
  • 34351599 をバイナリデータに変換 > bin[0] = 0x02, bin[1] = 0x0c, bin[2] = 0x29, bin[3] = 0xef
Sec-WebSocket-Key2も同じ方法で値からバイナリデータを作成します。

上記で求めた2つの4バイトバイナリデータとハンドシェイクリクエストの最後にあった8バイトバイナリデータを結合し、16バイトのバイナリデータにします。
 0                       4                       8                       16
 +-----------------------+-----------------------+-----------------------+
 |   Key1バイナリデータ  |  Key2 バイナリデータ  | 8バイトバイナリデータ | 
 +-----------------------+-----------------------+-----------------------+
この16バイトのバイナリデータからMD5ハッシュ値(16バイト)を計算したものがAcceptデータとなります。
4つのフィールドの後ろ(HTTPプロトコルのコンテンツ部分)にこの16バイトのAcceptデータをつなげたものをハンドシェイクレスポンスとしてクライアントに返します。
以上がハンドシェイク処理となります。
ハンドシェイクレスポンスを返すとブラウザー側で検証を行い通れば、WebSocketが接続されます。(WebSocketクライアントのonopenイベントが発生します。)
もし、検証に失敗した場合は接続に失敗します。(WebSocketクライアントのoncloseイベントが発生します。)
ハンドシェイクレスポンスを返す処理をC#のコードに書いてみます。
//string hr = Encoding.UTF8.GetString(ハンドシェイクリクエストバイト配列);

// パスを取得
string path = Regex.Match(hr, @"^GET\s([^\s]+)\s", RegexOptions.IgnoreCase).Groups[1].Value;
// Hostフィールドの値を取得
string host = Regex.Match(hr, "Host: (.+?)\r\n", RegexOptions.IgnoreCase).Groups[1].Value;
// Originフィールドの値を取得
string origin = Regex.Match(hr, "Origin: (.+?)\r\n", RegexOptions.IgnoreCase).Groups[1].Value;
// Sec-WebSocket-Key1の値を取得
string sk1 = Regex.Match(hr, "Sec-WebSocket-Key1: (.+?)\r\n", RegexOptions.IgnoreCase).Groups[1].Value;
// Sec-WebSocket-Key2の値を取得
string sk2 = Regex.Match(hr, "Sec-WebSocket-Key2: (.+?)\r\n", RegexOptions.IgnoreCase).Groups[1].Value;
// 最後の8バイトバイナリデータを取得
byte[] last8Bytes = new byte[8];
Array.Copy(entry.receiveData, entry.receiveData.Length - 8, last8Bytes, 0, 8);
// ハンドシェイクリクエストの固定部分文字列作成
string responseString = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
                        "Upgrade: WebSocket\r\n" +
                        "Connection: Upgrade\r\n" +
                        "Sec-WebSocket-Origin: {0}\r\n" +
                        "Sec-WebSocket-Location: {1}\r\n\r\n"; // 注意:レスポンスヘッダーの最後は\r\n2つ
// Sec-WebSocket-Origin 及び Sec-WebSocket-Location の値を設定後バイト配列に変換する
byte[] responseStringBin = utf8.GetBytes(string.Format(responseString, origin, "ws://" + host + path));
string digit1 = "", digit2 = "";
int spc1 = 0, spc2 = 0;
// Sec-WebSocket-Key1フィールド及びSec-WebSocket-Key2の値からそれぞれ数字抽出及びスペースの数を取得
foreach (char c in sk1) if (char.IsDigit(c)) digit1 += c; else if (c == ' ') spc1++;
foreach (char c in sk2) if (char.IsDigit(c)) digit2 += c; else if (c == ' ') spc2++;
// 数値 / スペースの数 を計算しバイナリデータを作成
byte[] skb1 = BitConverter.GetBytes((Int32)(Int64.Parse(digit1) / spc1));
byte[] skb2 = BitConverter.GetBytes((Int32)(Int64.Parse(digit2) / spc2));
// エンディアンが逆のため配列を反転させる
Array.Reverse(skb1);
Array.Reverse(skb2);
// 2つのバイナリデータ及びハンドシェイクリクエストの最後の8バイトバイナリデータを結合する
byte[] concatenatedKeys = new byte[16];
Array.Copy(skb1, 0, concatenatedKeys, 0, 4);
Array.Copy(skb2, 0, concatenatedKeys, 4, 4);
Array.Copy(last8Bytes, 0, concatenatedKeys, 8, 8);
// MD5ハッシュ値を計算
byte[] acceptData = md5.ComputeHash(concatenatedKeys);
// ハンドシェイクレスポンスデータを作成
byte[] handshakeResponse = new byte[responseStringBin.Length + 16];
Array.Copy(responseStringBin, handshakeResponse, responseStringBin.Length);
Array.Copy(acceptData, 0, handshakeResponse, responseStringBin.Length, 16);

// ハンドシェイクレスポンスを送信
soket.send(handshakeResponse);

データ送受信処理

WebSocketでデータを送信するとき、データをフレームという形にしてから送らなければなりません。 hybi-00のフレームはシンプルです。
        Handshake
           |
           V
        Frame type byte <--------------------------------------.
           |      |                                            |
           |      `--> (0x00 to 0x7F) --> Data... --> 0xFF -->-+
           |                                                   |
           `--> (0x80 to 0xFE) --> Length --> Data... ------->-'
テキストデータを送信する場合は、先頭に0x00〜0x7Fの1バイト、その後にペイロードデータ、最後に0xFFの1バイトという構成となります。なお、テキストデータの文字コードUTF-8を使用しなければなりません。バイナリデータを送信する場合は、先頭に0x80〜0xFEの1バイト、続けてデータの長さ(可変長と思います)、その後にペイロードデータという構成となります。
しかし、Opera及びSafariはバイナリデータの送受信には対応していません。(これはChromeFirefoxがまだhybi-00を実装していた頃も同様にテキストデータの送受信のみにしか対応していませんでした。)
ですので、データ送受信処理の実装は簡単です。
C#コードでの実装例
// クライアントからのデータ受信
int len = socket.EndReceive(asyncResult);
string receiveData = Encoding.GetString(1, クライアントからの送信データバイト配列, len - 2);

// 送信データをフレームに変換(先頭1バイトは0x00固定にします。)
List<byte> sendData = new List<byte>(new byte[]{0x00, 0xff});
sendData.InsertRange(1, Encoding.UTF8.GetBytes(receiveData));
byte[] sendDataBin = sendData.ToArray();
// すべてのクライアントにブロードキャスト送信
foreach(Member m in members)
{
    m.Socket.Send(sendDataBin);
}

Ping(Pong)処理

hybi-00の仕様には、Ping(Pong)に関することがかかれていません。ですので必要な場合は自分でPing送信文字列を決めてそれを送受信するといった処理を追加します。

クローズ処理

仕様においては接続を切るときもハンドシェイクを行うことになっています。
切断時のハンドシェイクは以下の方法で行います。
クライアントから接続を切りたい場合のハンドシェイク
  1. クライアントから 0xFF 0x00 の2バイトのデータを(サーバーに)送信します。
  2. サーバーは 0xFF 0x00 を受信すると 0xFF 0x00 をクライアントに送り返します。
  3. クライアントが 0xFF 0x00 を受信するとクライアント側から接続を切ります。
サーバーから接続を切りたい場合のハンドシェイクも同様に
  1. サーバーから 0xFF 0x00 の2バイトのデータを(クライアントに)送信します。
  2. クライアントは 0xFF 0x00 を受信すると 0xFF 0x00 をサーバーに送り返します。
  3. サーバーが 0xFF 0x00 を受信するとサーバー側から接続を切ります。
このハンドシェイク中はWebSocketクライアント(JavaScript)のWebSocketオブジェクトのreadyStateプロパティがCLOSING(=2)になるようです。
ハンドシェイクを行わなくても、SocketオブジェクトのCloseメソッドを実行するだけでもとりあえずは問題ないかと思います。


以上を踏まえてプログラムを組めば、とりあえずWebSocketでデータの送受信が行えるhybi-00のサーバー側の実装ができると思います。