鉄道NowのChrome Extensionプラグインを作ってみた。(β版) (2012/10/20 Update)
鉄道Nowで表示されている電車をクリックするとその電車の位置を単にGoogle Earthで表示するというプラグインを作ってみました。
このプラグインはポップアップウィンドウを使用していますので、ポップアップをブロックしている場合は、
"http://www.demap.info"のポップアップを許可してください。
今回、追尾機能追加アップデートにより、この追尾ポップアップを元に位置を取得すように修正しました。
これにより一応、どの拡大率でも動作するようになりました。
しかし、特に拡大率が小さい場合はEarthでのカメラの向きがおかしくなるので注意してください。
なお、マップの画像を元に座標を取得しているため、画像が完全に読み込まれた後に処理を行うようにクリックしてから2秒後にEarthの更新を開始しています。
「ツール」⇒「拡張機能」で拡張機能のページを表示後、ダウンロードしたdemap_plugin.crxファイルをドラッグアンドドロップしてください。
"「demap plugin」を追加しますか?"という確認ダイアログが表示されますので、「追加」ボタンをクリックすればインストールは完了です。
ポップアップウィンドウとメインウィンドウを見やすい位置/サイズに設定します。
ポップアップウィンドウに地球が表示されるまで待機してください。ただ、結構な確立でなかなか表示されない場合がありますので、
そのときは"鉄道Nowのページ"を更新してください。
地球が表示されたら、鉄道Nowに表示されている電車をクリックすればGoole Earthにその位置が表示されると思います。
このプラグインはポップアップウィンドウを使用していますので、ポップアップをブロックしている場合は、
"http://www.demap.info"のポップアップを許可してください。
今回、追尾機能追加アップデートにより、この追尾ポップアップを元に位置を取得すように修正しました。
これにより一応、どの拡大率でも動作するようになりました。
しかし、特に拡大率が小さい場合はEarthでのカメラの向きがおかしくなるので注意してください。
なお、マップの画像を元に座標を取得しているため、画像が完全に読み込まれた後に処理を行うようにクリックしてから2秒後にEarthの更新を開始しています。
プラグインのインストール
一番下にあるリンクよりdemap_plugin.crxファイルをダウンロードします。「ツール」⇒「拡張機能」で拡張機能のページを表示後、ダウンロードしたdemap_plugin.crxファイルをドラッグアンドドロップしてください。
"「demap plugin」を追加しますか?"という確認ダイアログが表示されますので、「追加」ボタンをクリックすればインストールは完了です。
プラグインを動かしてみる
インストールが終わりましたら、鉄道Nowのページにアクセス(すでに表示している場合は更新)すると、ポップアップが表示されると思います。ポップアップウィンドウとメインウィンドウを見やすい位置/サイズに設定します。
ポップアップウィンドウに地球が表示されるまで待機してください。ただ、結構な確立でなかなか表示されない場合がありますので、
そのときは"鉄道Nowのページ"を更新してください。
地球が表示されたら、鉄道Nowに表示されている電車をクリックすればGoole Earthにその位置が表示されると思います。
さくら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 でのインストールはどうやればうまくいくのだろうか。もしご存知のかたはよろしければご教授のほどお願いします。
ブラウザからmbedをLチカ
まあ、前回のブログ記事「mbedにWebSocketサーバーを載せてみた。」のコードにちょっと修正を加えただけのもの。
バイナリーフレームを受け取ったら、そのデータをledに出力させるというものです。
main.cppファイル
こちらにパブリッシュしたのでどのようなコード体系になっているのかが見れると思います。
あとはLANケーブルでつないで実行したら、jsdo.itにアクセスして、mbedのIPアドレスを入力してセットボタンを押します。
すると■ボタンが4つ表示されますのでクリックするとmbedがLチカできます。
mbed に WebSocketサーバーを載せてみた。
WebSocket関連で検索するのがほぼ日課みたいなものとなっています。
そんなある日、mbedというものを見つけました。
54mmx26mmの小さな基盤にARMのCPUが搭載されたものです。
青い基盤のもの(mbed NXP LPC1768)と、黄色い基盤のもの(mbed NXP LPC11U24)ものとがあります。(他にもいくつか種類があるみたいです。)
青い方はARM Cortex-M3を搭載しており、黄色い方はスペックを落として低消費電力化したものとなっており、ARM Cortex-M0を搭載しています。アキバではマルツや千石電商、秋月電子などで入手可能です。 開発用IDEがクラウドで用意されており、いつでもどこでもブラウザ上で開発ができるというのが素晴らしいところです。なお、オフラインでの開発環境もできるみたいです。
その他の詳しい内容はここでは割愛させていただきます。
しかし、ソースを見てみるとhybi-00(hixi-76)の古いプロトコルバージョンを使用して作成されたもので、RFCに対応していないものでした。
ですので、RFCに対応したWebSocketを載せてみようと思います。
@masato_kaさんのブログのコメントにてwebsockets_hello_world_ethernetをインポートすればというコメントが有りましたのでインポートしてみるとRFCに対応したWebSocketライブラリが使用されていました。
ただ、WebSocketクライアントを載せた場合は別途サーバーが必要となります。そこで、WebSocketサーバーをmbedに載せればブラウザーから直接データのやり取りが行えるようになるのではと思い挑戦してみたら出来たというお話です。
私は、C++はまったくといっていいほど触ったことがないため、いろいろとわからないままで組んでおりますのでご理解の程。警告は読んでわからないものは放置しています。
main.cppのソースを以下に掲載します。
なお、今回のプロジェクトファイル一式は下記のリンクからダウンロードできます。
mbed_WebSocketServer.zip
ほとんどコピペなソースなので主要なonLinkSocketEvent関数部分のみ載せます
LANのモジュラ・ジャックは千石電商などでキットとして販売されていますのでこちらを購入したほうがいいでしょう。 また、☆Board Orangeというmbed用のベースボードのも販売されています。このボードはmicroSD、USB(Host)、キャラクターLCDなどのI/Oインターフェイスを簡単に増設できるベースボードとなっています。このベースボードも千石電商などで入手可能です。(mbed用のベースボードは他にもあるようです。)
プログラムのソースは固定IPで接続するように組んでいます。DHCPでIPを自動で割り振る場合は割り振られたmbedのIPアドレスが分かる手段を整えてください。
また、Windowsの方はmbedのシリアルポートドライバーインストールし、Tera Termなどのシリアル通信モニターソフトを起動します。MacやLinuxの方はドライバーは不要なようですので、シリアル通信モニターのみ用意し起動します。
また、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側では受診したデータをブラウザーにそのまま返しますので、ブラウザーのテキストボックスの下にも送信された文字列が表示されます。
そんなある日、mbedというものを見つけました。
mbedとは?
私も触り始めたばかりで偉そうに説明できるほどではありませんが、第一印象として「簡単」ということ。54mmx26mmの小さな基盤にARMのCPUが搭載されたものです。
青い基盤のもの(mbed NXP LPC1768)と、黄色い基盤のもの(mbed NXP LPC11U24)ものとがあります。(他にもいくつか種類があるみたいです。)
青い方はARM Cortex-M3を搭載しており、黄色い方はスペックを落として低消費電力化したものとなっており、ARM Cortex-M0を搭載しています。アキバではマルツや千石電商、秋月電子などで入手可能です。 開発用IDEがクラウドで用意されており、いつでもどこでもブラウザ上で開発ができるというのが素晴らしいところです。なお、オフラインでの開発環境もできるみたいです。
その他の詳しい内容はここでは割愛させていただきます。
WebSocketサーバーを載せる
mbed用に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などのシリアル通信モニターソフトを起動します。MacやLinuxの方はドライバーは不要なようですので、シリアル通信モニターのみ用意し起動します。
実行
サンプルプログラムは固定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側では受診したデータをブラウザーにそのまま返しますので、ブラウザーのテキストボックスの下にも送信された文字列が表示されます。
WebRTCに関する疑問
最近、Chromeのdev版およびCanary版にWebRTC関連のAPIが実装され話題になっています。(実は、去年すでにWebRTCを実装しているブラウザーが存在しているみたい。)
私も、興味を持ってWebRTCのことを調べ始めているところです。
恥ずかしながらWebRTCを調べて初めて知ったのがSTUNというもの。
このSTUNがどういうものかを調べてみました。
参考資料
STUNはサーバーを設置して、そのサーバーにリクエストを投げるとリクエストを投げた端末のグローバルIPを返します。あとは他の端末からそのグローバルIPで接続を行えばP2P通信が行えるというものらしいです。
"らしいです"というのはすでにこの時点で理解ができなくなっているからです。
そうなってくると、どうやってP2P接続を開始するのでしょうか? つまり、相手にどうやってグローバルIPを教えるのでしょうか? 逆に相手のグローバルIPアドレスをどうやって取得するのでしょうか?
また、IPアドレスなどの管理を行うために別のサーバーを用意しないといけないのでしょうか?となってくると、なんかWebSocketより色々と用意しないといけなくなりますね。
そもそもこのような疑問を持ったきっかけは、サンプルコードが
となると自分で試したい場合はIPアドレスを1つだけしか割り当てられないレンタルサーバーの場合はSTUNサーバーを立てることができないのでしょうか?
あと、以降の疑問はまだ時期尚早と思われますが気になっているのでちょっと取り上げます。
WebRTCではこういったテキストやバイナリデータ(特に対戦ゲームを行なっている時のコントローラー入力情報など))はP2Pで送受信できないのでしょうか?
一応、WebKitGTK+というブラウザーに実装されたWebRTCの解説を見るとPeerConnectionオブジェクトにはSendメソッドがあり、このメソッドでメッセージを送信できるようなことが書かれています。しかし、W3CのWebRTC仕様のページにはPeerConnectionにはsendメソッドがありませんし、Chromeに実装されたwebkitPeerConnectionオブジェクトにもsendメソッドがありませんでした。
認識できたものの共通点としてKsproxyドライバを使用しているところです。(たぶん出力フォーマットが大いに関係していると思われます。YUVフォーマット対応のもの)
認識出来なかったもの 認識できたもの
これは、P2Pではないにせよ、WebSocketを使用して映像などをやり取りできるということだと思うのですが、私がすぐに思いついたのは、Media Source APIを使用した方法です。
Media Source APIはチャンクを次々に送信し、クライアントは受信したチャンクをSourceAppendメソッドで追加していくというものです。
しかし、こちらではこのMedia Source APIを使用していません。
ソース(wow_feature.js)を見てみたいものですが、アップされていないようで見ることができませんでした。
WebSocketをつかった映像のやり取りはどのようなコードとなるのでしょうか?
私も、興味を持ってWebRTCのことを調べ始めているところです。
恥ずかしながらWebRTCを調べて初めて知ったのがSTUNというもの。
このSTUNがどういうものかを調べてみました。
参考資料
- STUNサーバーの簡単な説明:http://www.3cx.jp/voip-sip/stun-server.php
- STUNのちょっと詳しい説明(Chromeでは画像が表示されないため他のブラウザーでアクセスしてください):
http://www.ekouhou.net/%EF%BC%A9%EF%BC%B0%E9%80%9A%E4%BF%A1%E8%A3%85%E7%BD%AE%E3%80%81%E9%80%9A%E4%BF%A1%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%80%81%E5%88%A4%E5%AE%9A%E6%96%B9%E6%B3%95%E3%81%8A%E3%82%88%E3%81%B3%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0/disp-A,2010-287982.html - STUNサーバーは基本的にポート番号は3478を使用する。
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フォーマット対応のもの)
認識出来なかったもの 認識できたもの
疑問その6 WebSocketを使用したWebRTC
前日開催された東北デベロッパーズカンファレンスで、@agektmrさんが、WebSocketを使用したWebRTC?をちょこっと言っていました。これは、P2Pではないにせよ、WebSocketを使用して映像などをやり取りできるということだと思うのですが、私がすぐに思いついたのは、Media Source APIを使用した方法です。
Media Source APIはチャンクを次々に送信し、クライアントは受信したチャンクをSourceAppendメソッドで追加していくというものです。
しかし、こちらではこのMedia Source APIを使用していません。
ソース(wow_feature.js)を見てみたいものですが、アップされていないようで見ることができませんでした。
WebSocketをつかった映像のやり取りはどのようなコードとなるのでしょうか?
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)
続いて、ハンドシェイクレスポンスの例を見てみます。
聞いた話なのですが、hybi-00では特にプロキシを通したときにこのコンテンツ部分が削られてたものがサーバーに届き、ハンドシェイクが行えずWebSocketをつなぐことができないという状況が発生するようです。
ハンドシェイクの仕様変更はセキュリティの問題もそうですが、こういった障害に対する対応も含めたものと思われます。
hybi-07以降のハンドシェイクレスポンスに含めなければならないフィールドは
各行およびフィールドに設定する値は次のようになります。
例として、ハンドシェイクリクエストで
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
という値から、Sec-WebSocket-Acceptフィールドの値を作成してみます。
これらの値を設定してハンドシェイクレスポンスを返します。
ハンドシェイクレスポンスを返す処理をC#のコードで組みますと以下のようになります。
hybi-07以降のフレーム構成図を見てみましょう。一番上の数字はビットオフセットの値です。
マスクは、4バイトごとにループして行います。
実際にC#でマスク処理を書くと以下のようになります。
Pingで送られてきたデータをそのままPongとして返さなければならないと仕様ではなっています。
ただし、Pingを送る際にペイロードデータはなくてもいいらしいです。つまり、0x89 0x10 0x9a 0x6e 0x33 0x72という感じで(0x9a 0x6e 0x33 0x72はマスクキー)、ペイロードデータがないフレームでもかまいません。(ちょっと自信ない)
Pongを返す場合は、Pingで送られてきたフレームの1バイト目を0x8Aにしてそのまま返せばいいです。
C#コード
hybi-00での解説でも書きましたがSocketオブジェクトのCloseメソッドを実行するだけでもとりあえずは問題ないかと思います。
以上を踏まえてプログラムを組めば、とりあえずWebSocketでデータの送受信が行えるhybi-07以降に対応したサーバーの実装ができると思います。
※ブラウザーに実装されている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: 13hybi-07以降のハンドシェイクリクエストで送られてくるフィールドは
- Upgrade
- Connection
- Host
- Origin
- Sec-WebSocket-Key
- Sec-WebSocket-Version
続いて、ハンドシェイクレスポンスの例を見てみます。
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
各行およびフィールドに設定する値は次のようになります。
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フィールドの値を作成してみます。
- "dGhlIHNhbXBsZSBub25jZQ=="と"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を文字列結合 = "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"からSHA1ハッシュ値を計算 = sha1[0] = 0xb3, sha1[1] = 0x7a, sha1[2] = 0x4f, sha1[3] = 0x2c ...
- SHA1ハッシュ値をBase64文字列に変換 = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
これらの値を設定してハンドシェイクレスポンスを返します。
ハンドシェイクレスポンスを返す処理を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の値でこのフレーム(のデータ)がどんなものかを識別します。
|
||||||||||||||||||
2バイト目 | 先頭ビット | 2バイト目の先頭ビットはMASKフラグとなっており、このフラグが1の場合はデータがマスクされています。なお、クライアントからサーバーにデータを送信する場合は必ずマスクを行わなければなりません。逆にサーバーからクライアントにデーターを送るときはマスクなしで、hybi-15からは"MUST"が付き必ずマスク無しで送信するようにとなっています。 | |||||||||||||||||
下位7ビット | ここにはデータ長か、126または127が入ります。
|
||||||||||||||||||
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クライアント対応プロトコルバージョン確認編で、ブラウザーによって対応するプロトコルバージョンが違うということがわかりました。どのブラウザーがどのプロトコルバージョンに対応しているのかをここで再度確認しましょう。
英語が読める方はhybi-00の仕様を見ながら読み進めていただければ理解がしやすかと思います。(逆に仕様と違う所があれば教えて下さいw)
ハンドシェイク処理はHTTPプロトコルを使用して行います。
ハンドシェイクリクエストの例を見てみます。
ハンドシェイクリクエストの例を見ると最後に文字化けしたようなデータがあります(8バイトのバイナリデータ)。これも重要なデータとなります。
ハンドシェイクリクエストを元にハンドシェイクレスポンスを返します。
ハンドシェイクレスポンスの例を見てみましょう。
各行およびフィールドに設定する値は次のようになります。
上記以外にハンドシェイクリクエストの最後に文字化けしたようなデータ(16バイトのバイナリデータ)があります。ここには定められた方法によって作成されたデータを設定します。作成するデータのことをAcceptデータと呼ぶことにします。
Sec-WebSocket-key1の値に含まれる数字部分を抜き出し結合してそれを数値にしたものを、値に含まれるスペースの個数で割った値をバイナリデータ(4バイト)に変換します。
例として Sec-WebSocket-Key1: 3 7 78B 67 5 W%89
という値でバイナリデータをを求めてみます。
上記で求めた2つの4バイトバイナリデータとハンドシェイクリクエストの最後にあった8バイトバイナリデータを結合し、16バイトのバイナリデータにします。
4つのフィールドの後ろ(HTTPプロトコルのコンテンツ部分)にこの16バイトのAcceptデータをつなげたものをハンドシェイクレスポンスとしてクライアントに返します。
以上がハンドシェイク処理となります。
ハンドシェイクレスポンスを返すとブラウザー側で検証を行い通れば、WebSocketが接続されます。(WebSocketクライアントのonopenイベントが発生します。)
もし、検証に失敗した場合は接続に失敗します。(WebSocketクライアントのoncloseイベントが発生します。)
ハンドシェイクレスポンスを返す処理をC#のコードに書いてみます。
しかし、Opera及びSafariはバイナリデータの送受信には対応していません。(これはChromeやFirefoxがまだhybi-00を実装していた頃も同様にテキストデータの送受信のみにしか対応していませんでした。)
ですので、データ送受信処理の実装は簡単です。
C#コードでの実装例
切断時のハンドシェイクは以下の方法で行います。
クライアントから接続を切りたい場合のハンドシェイク
ハンドシェイクを行わなくても、SocketオブジェクトのCloseメソッドを実行するだけでもとりあえずは問題ないかと思います。
以上を踏まえてプログラムを組めば、とりあえずWebSocketでデータの送受信が行えるhybi-00のサーバー側の実装ができると思います。
※ブラウザーに実装されている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 |
ハンドシェイク処理
ハンドシェイク処理は、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
ハンドシェイクリクエストの例を見ると最後に文字化けしたようなデータがあります(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 となります。 |
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
上記で求めた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はバイナリデータの送受信には対応していません。(これはChromeやFirefoxがまだ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); }
クローズ処理
仕様においては接続を切るときもハンドシェイクを行うことになっています。切断時のハンドシェイクは以下の方法で行います。
クライアントから接続を切りたい場合のハンドシェイク
- クライアントから 0xFF 0x00 の2バイトのデータを(サーバーに)送信します。
- サーバーは 0xFF 0x00 を受信すると 0xFF 0x00 をクライアントに送り返します。
- クライアントが 0xFF 0x00 を受信するとクライアント側から接続を切ります。
- サーバーから 0xFF 0x00 の2バイトのデータを(クライアントに)送信します。
- クライアントは 0xFF 0x00 を受信すると 0xFF 0x00 をサーバーに送り返します。
- サーバーが 0xFF 0x00 を受信するとサーバー側から接続を切ります。
ハンドシェイクを行わなくても、SocketオブジェクトのCloseメソッドを実行するだけでもとりあえずは問題ないかと思います。
以上を踏まえてプログラムを組めば、とりあえずWebSocketでデータの送受信が行えるhybi-00のサーバー側の実装ができると思います。