lessでソースコードに色をつける(vimユーザ編)
はじめに
以下のページを見て脊髄反射で書いてみました。タイトル通りvimユーザ向けの内容です。
漢(オトコ)のコンピュータ道: lessでソースコードに色をつける
less.shの所在
vimがインストールされている環境であれば新たに何かをインストールする必要はありません。less.shというファイルを探しましょう!私の環境では以下の場所にありました。
Mac OSX 10.6, vim 7.2
/usr/share/vim/vim72/macros/less.sh
/usr/share/vim/vim73/macros/less.sh
このless.shは内部でvimコマンドを実行しているただのスクリプトファイルです。普通のlessコマンドのように引数にファイルを指定して実行してみて下さい。するとvimのシンタックスでカラフルに表示されると思います!
さらにlessコマンドと同様に、表示中に「v」を入力するとそのままvimでファイルの編集ができちゃいます!
WebSocketを使ってWebブラウザ間P2P通信をしてみた
はじめに
ブラウザ間でP2P通信が実現できれば、ブラウザ上で動作するP2Pアプリが作れて面白そうだなーと思ったのでWebSocketを使って実現してみました。仕組みについては以下で説明していきますが、私が実現した方法は限定的で実用性が低く色々と足りない部分もあるので、軽い気持ちで読んで頂けるとありがたいですw
仕組みの概要
なぜWebSocketを使うのか
従来、Webサーバとクライアント(Webブラウザ)間で非同期に通信するにはXHR(XMLHttpRequest)を用いてきました。基本的にこのXHRは以下の図のように同一ドメインとしか通信できないという制約がありました。*1
しかし、WebSocketのthe Origin-based security modelでは異なるドメインとも通信することが可能になります。WebSocketプロトコルでは、サーバとクライアント間で接続を確立する際にハンドシェイクデータの送受信を行いますが、クライアント側のハンドシェイクデータ内にはOriginというヘッダーフィールドがあります。以下の図のように、クライアントのハンドシェイクデータを受信したサーバはこのOriginの値を見て接続を確立するかどうかを判断することができます。この時、もしもブラウザ上でWebSocketサーバプログラムを動かすことができれば、ブラウザ間P2P通信を実現できるのではないかと考えました。
どのようにしてブラウザ上でWebSocketサーバプログラムを動かすのか
WebSocketサーバプログラムを動かすためにはTCPのソケット機能が必要不可欠ですが、JavaScriptにはそのような機能が無いためブラウザ上でサーバプログラムを動かすことができません。そこで、私はXPCOMの機能を利用することにしました。
XPCOM(Cross Platform Component Object Model)とはMozillaが開発を行なっているコンポーネント技術のことです。XPCOMのコンポーネントにはメモリ管理やソケット機能などがあり、これらの機能はFirefoxアドオンの中から呼び出すことができます。
私はこのXPCOMの機能を用いてWebSocketServerというクラス*2を作成し、ブラウザ上からそのクラスを呼び出せるようにするFirefoxアドオンを開発しました。つまり、アドオンがインストールされたFirefox上のJavaScriptではWebSocketServerクラスのインスタンスを作成でき、ブラウザ上で動作するWebSocketサーバプログラムを実行することが可能になります。
Firefoxアドオンのダウンロード・インストール
アドオンはWebSocketプロトコルのhybi-08に合わせており、動作確認は最新のFirefoxでしか試していないのでFirefox 10.0a1(Nigthly)にアドオンをインストールして下さい。他のバージョンのFirefoxにアドオンをインストールしたい方はinstall.rdfを書き換えて下さい(試していませんがv8, 9辺りでも動くと思います…多分)。
まずはgithubにあるリポジトリを落としてきます。リポジトリ内のAdd-onフォルダにはFirefoxアドオンのソースコードがあり、echoフィルダにはサンプルプログラムが入っています。
% git clone git://github.com/yogit/WebSocketServer_hybi-08.git % ls WebSocketServer_hybi-08 Add-on/ echo/
次にアドオンのソースコードからxpiファイルを作成します。Firefoxのアドオンはただのzipファイルなので、以下のようにソースコードを1つのzipファイル(ファイル名はWebSocketServer_hybi-08.xpi)に圧縮します。この時、Add-onフォルダをzipファイルに圧縮すると正常に動作しないので気をつけて下さい。
% cd WebSocketServer_hybi-08/Add-on/ % zip -r WebSocketServer_hybi-08.xpi ./chrome ./install.rdf ./chrome.manifest adding: chrome/ (stored 0%) adding: chrome/content/ (stored 0%) adding: chrome/content/DataFrame.coffee (deflated 69%) adding: chrome/content/DataFrame.js (deflated 73%) adding: chrome/content/init.coffee (deflated 43%) adding: chrome/content/init.js (deflated 42%) adding: chrome/content/overlay.xul (deflated 55%) adding: chrome/content/WebSocketClient.coffee (deflated 72%) adding: chrome/content/WebSocketClient.js (deflated 73%) adding: chrome/content/WebSocketServer.coffee (deflated 70%) adding: chrome/content/WebSocketServer.js (deflated 70%) adding: install.rdf (deflated 46%) adding: chrome.manifest (deflated 44%)
xpiファイルに圧縮できたらFirefoxにドラッグ&ドロップしてダイアログに従ってインストールを行なってください。
サンプルプログラム(echoサーバ)の実行方法
サンプルとしてブラウザ上で動作するechoサーバを作りました。echoディレクトリをDocumentRoot以下に置いてブラウザからアクセスしてください。その際、クライアント側はhybi-08のWebSocketに対応したブラウザをお使いください(Chrome v15かFirefox v10.0a1がオススメです)。以下にサンプルプログラムを動かしているときのスクショを載せます。
画面左側のアドオンインストール済みのFirefox v10.0a1ではechoサーバを動かし、右側のChrome v15ではechoクライアントを動かしてブラウザ間でP2P通信をしている様子が確認できると思います。
サンプルプログラム(echoサーバ)の解説
echo/WSServer.jsのコアになる部分だけ以下に抜粋してコメントを追加しました。
// newできなければ関数として実行する *1 server = new WebSocketServer(options) || WebSocketServer(options); // WebSocketサーバが起動したときに呼び出されるメソッド server.onstart = function () { // ページが遷移するときサーバが起動していたら終了するようにする addEventListener("beforeunload", function (e) { // クラス変数ではなくインスタンス変数(server.RUNNIN)を使う *2 if (server.readyState == server.RUNNING) { server.stop(); } }, true); log('server start'); }; // サーバが停止したときに呼び出されるメソッド server.onstop = function () { log('server stop'); }; // クライアントと接続が確立したときに呼び出されるメソッド server.onconnect = function (client) { log('[' + client.host + ':' + client.port + '] connected'); // クライアントからメッセージを受信したときに呼び出されるメソッド client.onmessage = function (data) { log('[' + client.host + ':' + client.port + '] recieved(' + data.length + '): ' + data); // send()ではなくbroadcast()を使えばチャットサーバのような挙動になる client.send(data); // 受信したデータをそのまま送り返す //client.broadcast(data); // 他の接続済みクライアントに送信する }; }; // クライアントとの接続が切れたときに呼び出されるメソッド server.ondisconnect = function (client) { log('[' + client.host + ':' + client.port + '] disconnected'); }; // サーバ起動 server.start(); // サーバ停止 // server.stop();
アドオンの解説
簡単にですがWebSocketServerアドオンについて説明します。基本的にFirefoxのアドオンはJavaScriptで書くのですが、私はCoffeeScriptでコードを書いてJavaScriptにコンパイルしています。ですので、以下の説明ではCoffeeScriptのコードやファイル名が出てきますが適宜JavaScriptに置き換えて読んでください。
# CoffeeScriptって良いですよね。これに慣れるとJavaScriptを書くのが面倒くさく感じます(^_^;)
WebSocketServerクラスについて
WebSocketSeverクラスはcontent/WebSocketServer.coffeeにて定義しています。このクラスではXPCOMのnsIServerSocketインターフェースを用いてサーバ機能を実現しています。クライアントとTCP接続するとonSocketAccepted()メソッドが呼び出され、クライアントごとにWebSocketClientクラスのインスタンスが生成されれます。このWebSocketClientクラスはcontent/WebSocketClient.coffeeにて定義され、ハンドシェイクやデータフレームの送受信などの機能があります。これらのクラス内で用いているXPCOMの機能については以下の公式ページから参照してください。
XPCOM Interface Reference - Mozilla | MDN
JavaScriptに機能を追加する
アドオン内で定義したクラスはアドオン内のコードからしか使えませんが(アドオン内のJavaScriptコードとWebサーバからダウンロードしたJavaScriptコードは別物)、content/init.coffeeにて以下のように書くことでブラウザ上で実行されるJavaScriptにWebSocketServerクラスを追加することができます。
window.addEventListener 'DOMContentLoaded', -> browserWin = window.content.window.wrappedJSObject console = browserWin.console # WebSocketServerクラスを追加する browserWin.WebSocketServer = WebSocketServer , false
window.content.window.wrappedJSObjectを経由することでブラウザ上で実行されるJavaScriptのwindowオブジェクトにアクセスすることができます。これにより、JavaScriptに本来存在しないWebSocketServerクラスをwindow以下に追加することができます。
出来ないこと・出来ること
上記のアドオンをインストールすることでブラウザ間でP2P通信が可能になりますがNAT越えの機能は実装していません。よって、構築出来るP2Pネットワーク網はプライベートLANの中に限られ、大規模なネットワーク網を構築することは出来ません(でも、グローバルIPを持つノードを中継役にすればある程度の規模のネットワーク網は構築できるかも?)。また、WebSocketServer機能はアドオンをインストールしないと利用できませんが、WebSocketは双方向に通信できるプロトコルなのでアドオンをインストールしていない(WebSocket対応)ブラウザでも以下のようなネットワークに参加できると思います。
図の中の「Fx☆」はアドオンインストール済みのFirefoxを表し、「WSS」は一般的なWebSocketサーバを表し、「ブラウザ」はWebSocketに対応した普通のWebブラウザを表しています。この図のように普通のブラウザでもFx☆かWSSの下にぶら下がるようにすることでネットワークに参加できると思います。
また、WebSocketServerはhybi-08に合わせて作りましたが以下の機能については未実装です。
Firefox以外のブラウザについて
今回の方法だとアドオンをインストールしたFirefox上でしかWebSocketサーバプログラムを動かせませんが、他のブラウザでも同様のことをする方法について考えてみました。
Google Chrome
ChromeにはNaCl(Native Client)*3というネイティブコードを実行する機能が実装されています。NaClでは全てのネイティブコードを実行できるのではなく、NaCl SDKでコンパイルされたコードしか実行できません。今現在公開されているSDKには通信機能が無いみたいですが、今後サポートされればFirefoxアドオンのようにソケット機能が使えるようになるかもしれません。
# 少し前まではabout:flagsにP2P Pepper APIという項目がありましたが、何故か今は無くなってます。
Opera
Opera UniteというOperaをサーバにする機能があったので、これを使えば似たようなことが出来そうな気がしますが詳しく調べていないので何とも言えません。
# Opera Uniteは発表されたときは良く耳にしたんですが、ここ最近は話を全然聞かないです…。
Flashを使ってクロスブラウザ
ブラウザ上で動作するAdobe Flashにはサーバ機能は無いようですが、Adobe AIRにはServerSocketクラスというものがあり、TCPサーバとして動作するようです。また、LocalConnectionクラスというものを使えばAIRアプリケーションとブラウザ上のFlash間で通信ができ、ExternalInterfaceクラスを使えばFlashとJavaScript間で連携できるので、以下の図のようにWebSocketサーバ部分をWebブラウザから独立したAIRアプリケーション化することで今回と同様のことができるのではないかと考えています。実際にサンプルプログラムを書いて確かめてみようと思ってるのですが時間が無くて放置してます…。
おわりに
以前、電子情報通信学会の研究会にて「WebSocketを用いたWebブラウザ間P2P通信の実現とその応用に関する研究」というタイトルで発表を行いました。発表後、せっかくだからブログネタにしようと思っていたのですが、後回しにしてずっと放置していました。ですが、数日前に以下のブログにて私の研究会での発表を取り上げて頂いたので更新することにしました。
[P2P]Websocketでブラウザ間P2P通信は実現できるか?: Tomo’s HotLine
研究会で発表したときは、WebSocketでブラウザ間P2P通信ができたらアツい!と思っていたのですが、最近はWebRTCが気になっています。まだドキュメントをきちんと読んではいませんが、将来有望な気がするので今後の進展が楽しみです。ついでに、以下にWebRTCについても書いておきます。
WebRTCについて
WebRTC(Web Real-Time Communications)はWebブラウザ間で音声や動画によるリアルタイム通信を可能にするもので、WebRTCのPeer Connection APIを使えばアドオンをインストールしなくてもブラウザ間P2P通信ができるのではないかと考えていますが、WebRTCは新しい技術なので対応ブラウザはまだ無いようです。ですが、以下のページによるとWebRTCの幾つかのAPI(getUserMedia, Stream API, PeerConnection API)を実装したWebkitライブラリを置き換えることでWebRTCを使ったアプリケーションの開発が行えるらしいです。ここも詳しく調べ、実際に環境を構築してサンプルプログラムを書いてみたいのですが時間が無いので放置してます…。
https://labs.ericsson.com/apis/web-real-time-communication/documentation
*1:XMLHttpRequest Level 2では異なるドメインと通信する機能が追加されているそうです
*2:JavaScriptにはクラスがありませんが便宜上クラスという単語を使ってます
*3:発音はナックルで良いのかしら?
Makefileのように更新されたファイルのみコンパイルするCakefileのメモ
Cakefile
やってることは単純で、CoffeeScriptファイルの最終更新時刻がJavaScriptファイルの時刻より新しければコンパイルするだけです。
SRCDIRにCoffeeScriptファイルがあるディレクトリのパスを、OUTDIRに生成されるJavaScriptファイルの保存先を指定してください。
sys = require 'sys' fs = require 'fs' exec = require('child_process').exec COMMAND = 'coffee' OPTIONS = '-cb' SRCDIR = '.' # *.coffeeファイルがあるディレクトリへのパス OUTDIR = './js' # *.jsファイルの保存先 targetList = ['Hoge', 'Piyo'] # SRCDIR以下のすべてのCoffeeScriptファイルを指定したいときはこっち #targetList = [] #for f in fs.readdirSync SRCDIR # targetList.push RegExp.$1 if f.match /^(\w+)\.coffee$/ task 'all', 'compile target files', -> for target in targetList try cs = fs.statSync "#{SRCDIR}/#{target}.coffee" catch error sys.puts "file not found: #{error.path}" continue try js = fs.statSync "#{OUTDIR}/#{target}.js" continue if cs.mtime < js.mtime # 更新されていなければ次のターゲットへ catch error null # jsファイルが存在しなくても特に何もしない sys.puts "#{COMMAND} #{OPTIONS} -o #{OUTDIR} #{SRCDIR}/#{target}.coffee" exec "#{COMMAND} #{OPTIONS} -o #{OUTDIR} #{SRCDIR}/#{target}.coffee" task 'clean', 'delete target files', -> for target in targetList exec "rm #{OUTDIR}/#{target}.js"
おわりに
makeコマンドのように引数を指定しなくてもmakeして欲しいなーと思って、invokeを使う方法も考えたんですがうまく書けませんでした(:´Д`)
/usr/local/lib/coffee-script/lib/cake.js辺りを書き換えたらcakeコマンドの挙動を変えれそうですが、できればCakefile内で何とかしたいです。
CoffeeScriptのプライベートメンバについてのメモ
はじめに
プライベートメンバについてメモ代わりに適当なサンプルを書いてみました。
サンプル
以下のCoffeeScriptから生成されたJavaScriptのコードを確認したいのであれば、CoffeeScriptの公式ページのTRY COFFEESCRIPTに以下のコードを貼りつけてください。
class Person # クラス変数 @DEAD = 0 @ALIVE = 1 # インスタンス変数はコンストラクタ引数で直接指定できる constructor: (@name, _age, _state=Person.ALIVE) -> # インスタンス変数。パブリック # @name # インスタンス変数。プライベート # _age, _state # インスタンスメソッド。プライベート。非共用 _increment = -> ++_age # インスタンスメソッド。パブリック。非共用 @getStatus = -> _state # インスタンスメソッド。パブリック。非共用 @setStatus = (newStatus) -> _state = newStatus # インスタンスメソッド。パブリック。非共用 @getAge = -> _age # インスタンスメソッド。パブリック。非共用 @growUp = -> _increment() if _state is Person.ALIVE # インスタンスメソッド。パブリック。共用 getName: -> @name # インスタンスメソッド。パブリック。共用 setName: (newName) -> @name = newName p1 = new Person 'yamada', 20 p2 = new Person 'kinjo', 30, Person.DEAD # インスタンス変数nameの取得 # パブリックなのでどちらの方法でも取得できる alert p1.getName() # yamada alert p2.name # kinjo # インスタンス変数_ageの取得 # プライベートなのでgetterからしか取得できない alert p1.getAge() # 20 alert p2._age # undefined # プライベート変数_ageの操作 # growUp()は内部で_increment()を呼び出している p1.growUp() alert p1.getAge() # 21 # プライベート変数_ageの操作 # _increment()メソッドはプライベートなので直接呼び出せない p2._increment() # TypeError: Object #<Person> has no method '_increment'
上記のコードのようにプライベート変数(_age, _state)にアクセスするメソッドはconstructorの中で定義する必要があります。
また、コメントに書いてある共用・非共用というのはJavaScript側のコードを見ると解ると思います。以下のコードがその一部です。
Person = (function() { Person.DEAD = 0; Person.ALIVE = 1; function Person(name, _age, _state) { 省略 this.getAge = function() { return _age; }; 省略 } Person.prototype.getName = function() { return this.name; }; 省略
getNameプロパティはprototypeに追加されているので全てのインスタンスで共有されるためメモリ効率が良いです。しかし、getAgeプロパティは各インスタンスごとに持つので(非共有なので)メモリ効率が悪いです。ですが、この方法だとプライベート変数と同じスコープなのでアクセスすることが出来ます。
hg commitと同時にRedmineのリポジトリ情報を更新する
はじめに
研究で作成しているプログラムはMercurial + Redmineで管理しているのですが、commitする度にRedmineのリポジトリのページを開くの面倒くさかったので、commitと同時にリポジトリ情報を更新する方法を調べました。
以下のページの『設定方法(Redmine 0.9 + Subversionの例)』のMercurial版をメモ代わりに残します。
小技(0.9): コミットと同時にリポジトリの情報を取得する | Redmine.JP Blog
Redmineの設定
上記のページと同じです。管理 > 設定 > リポジトリを開き「リポジトリ管理用のWebサービスを有効にする」にチェックを飛ばし、「APIキー」に適当な文字列を入力(または「キーの生成」をクリックして自動生成)します。これで、以下のようなURLを叩くだけでRedmineのリポジトリ情報を更新することができます。
http://Redmineサーバ名/sys/fetch_changesets?key=APIキー&id=プロジェクト識別子
このとき、URLのid=の部分を書かなければRedmineの全てのリポジトリ情報が更新されるそうですが、複数のリポジトリを登録しているならid=でプロジェクト識別子を指定した方が良いと思います。
Mercurialの設定
作業リポジトリの.hg/hgrcに以下を追加します。
[hooks] commit.redmine = wget -q --delete-after "http://Redmineサーバ名/sys/fetch_changesets?key=APIキー&id=プロジェクト識別子"
これにより、hg commitと同時にRedmineのリポジトリ情報を更新するURLが叩かれるようになります。また、commit以外にpushやpullと同時にURLを叩きたい場合は.hg/hgrcに以下を追加すれば良いと思います。
[hooks] changegroup.redmine = wget -q --delete-after "http://Redmineサーバ名/sys/fetch_changesets?key=APIキー&id=プロジェクト識別子"
VimでCoffeeScriptを書く準備
はじめに
最近、CoffeeScriptにハマリつつあります。Python / Rubyライクな文法でサクサク書けるのに実行速度はJavaScriptと大差無いのが素晴らしいですね!
今回は、VimでCoffeeScriptを書く上で必要な設定をメモ帳代わりに残そうと思います。
pathogen.vimのインストール
pathogenはVimプラグイン管理のためのモノで、CoffeeScriptとは直接関係は無いんですが便利なのでインストールします。
~ % mkdir .vim/bundle ~ % git clone git://github.com/tpope/vim-pathogen.git .vim/bundle/vim-pathogen ~ % mkdir .vim/autoload ~ % cd .vim/autoload ~/.vim/autoload % ln -s ../bundle/vim-pathogen/autoload/pathogen.vim ./
$HOME/.vimrcに以下の3行を追加します。
filetype off call pathogen#runtime_append_all_bundles() filetype on
callの前後でfiletypeをoff, onにしているのは、pathogenでftdetectなどをロードさせるために必要だそうです。
vim-coffee-scriptのインストール
CoffeeScript用のインデントやシンタックスなどが使えるプラグインです。VimでCoffeeScriptを書く上で必要不可欠なものだと思います。
~ % git clone git://github.com/kchmck/vim-coffee-script.git .vim/bundle/vim-coffee-script
インストールができたら.vimrcに以下の行を追加しましょう。
autocmd BufWritePost *.coffee silent CoffeeMake! -cb | cwindow | redraw!
この行を追加することで、*.coffeeファイルを保存する度に自動で-cbオプションでコンパイルするようになります。
quickrun.vimのインストール
quickrunはCoffeeScriptにも対応しています。私はコンパイルして生成されるJavaScriptコードを確認するために使っています。
~ % git clone git://github.com/thinca/vim-quickrun.git .vim/bundle/vim-quickrun
インストールができたら.vimrcに以下の行を追加します。
let g:quickrun_config = {} let g:quickrun_config['coffee'] = {'command' : 'coffee', 'exec' : ['%c -cbp %s']}
この2行を追加することで\+rを入力すると、
coffee -cbp 編集中のファイル.coffee
を実行して生成されるJavaScriptコードが表示されます。このとき気をつけて欲しいのは、コンパイルされた結果が表示されるだけということです。表示されたJavaScriptのコードは実際には.jsファイルに書き込まれないので注意して下さい。
これでVimでCoffeeScriptを書く準備ができました!
もっと探せば便利なプラグインやvimrcの設定があると思いますが、上記の方法で整えた環境だけでも楽にプログラムが書けるようになったと思います。
WebSocket(hybi-07)でechoサーバを作ってみた
はじめに
以前、WebSocket Draft 76でechoサーバーを作ってみましたが、今回はhybi-07に対応したバージョンを作ってみました。しばらく見ない間に大きく仕様が変わっていて驚きました(*_*;
今回の実行環境は以下の通りです。
最新のWebSocketプロトコルを調べるために作成したプログラムなんですが、以下の機能については未実装です。
- バイナリデータと断片化されたフレームの送受信
- クライアントからこれらを送信する方法がわからなかったので
- 拡張機能(Sec-WebSocket-Extensions)
- IETFの仕様(Section 8)を読んだけどよくわからなかった(;・∀・)
実行の手順
まず、以下からプログラムをダウンロードします。
ダウンロードしたフォルダのechoServer.pyがechoサーバの実体で、htmlフォルダ以下にクライアント側のHTMLとJavaScriptファイルがあります。
echoサーバが使用するポート番号などの設定はechoServer.pyの以下の部分を直接編集して下さい。
class TCPHandler(SocketServer.BaseRequestHandler): def setup(self): """ 初期化処理を行うメソッド SocketServer.BaseRequestHandlerのsetup()メソッドを上書きする。 handle()メソッドより前に呼び出される。 """ self.configObj = { 'host': '', 'port': self.server.server_address[1], 'resource': '/', 'origin': '', # 何も指定しなければすべてのoriginを許可する 'protocol': [''], #'protocol': ['video', 'talk', 'chat'], # 複数のサブプロトコルをサポートする場合 'pingInterval': 0 # pingする間隔(単位:秒)。0ならpingしない } 以下省略
実行時の様子
以下は、ローカルにechoサーバとWebサーバを立てhybi-07に対応しているFirefox v6(もしくは7)を用いてechoサーバとやり取りしたときの様子です。
次に、実際にechoサーバを書いてみてわかったことなどをまとめてみます。
クライアントハンドシェイク
hybi-07に対応したFirefoxからエコーサーバに接続しようとすると、以下のようなハンドシェイクデータが送信されます。
GET / HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0a1) Gecko/20110605 Firefox/7.0a1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive, Upgrade Sec-WebSocket-Version: 7 Sec-WebSocket-Origin: http://localhost Sec-WebSocket-Key: bl3e0R+mnGJ9KkCFJw0rOA== Pragma: no-cache Cache-Control: no-cache Upgrade: websocket
ヘッダがいろいろありますが、ハンドシェイクに必要なのは以下の行だけです。
GET / HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive, Upgrade Sec-WebSocket-Version: 7 Sec-WebSocket-Origin: http://127.0.0.1 Sec-WebSocket-Key: bl3e0R+mnGJ9KkCFJw0rOA== Upgrade: websocket
Draft 76と違ってSec-WebSocket-Versionフィールドが新たに増え、Sec-WebSocket-Keyは一つに減りました。また、クライアントハンドシェイク内のSec-WebSocket-Keyフィールドの値はサーバハンドシェイク内のSec-WebSocket-Acceptフィールドの値を作成されるために用いられます。
サーバハンドシェイク
クライアントから上記のハンドシェイクデータを受信したサーバは、以下のようなハンドシェイクデータをクライアントに送信します。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: YjV0s2D16GYJYuGy1aIZF0g/bIs=
Sec-WebSocket-Acceptフィールドの値は、クライアントハンドシェイク内のSec-WebSocket-Keyの値(上記の例の"bl3e0R+mnGJ9KkCFJw0rOA=="という文字列)の後に"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"という文字列を加えたもの(文字列"bl3e0R+mnGJ9KkCFJw0rOA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11")をSHA-1でハッシュ化して得た20バイトのハッシュ("0x62 0x35 0x74 0xb3 0x60 0xf5 0xe8 0x66 0x9 0x62 0xe1 0xb2 0xd5 0xa2 0x19 0x17 0x48 0x3f 0x6c 0x8b")をbase64変換することで求めることができます。
データフレーム
ハンドシェイクに成功するとサーバ・クライアント間で双方向に通信することができます。現在の仕様(Section 4.2)では以下のようなフレームを用いてデータの送受信を行います。
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/63) | |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 ... | +---------------------------------------------------------------+
フレームの送受信において、クライアントからサーバ宛のフレームはかならずMASKビットがセットされ、クライアントからのデータは4バイトのMaking-keyによってマスクされています。もし、サーバがクライアントから以下のようなMasking-keyとPayload Dataを持つテキストフレームを受信したと仮定します。
Masking-key : 0x37 0xfa 0x21 0x3d Payload Data: 0x7f 0x9f 0x4d 0x51 0x58
このとき、サーバは以下のような処理を行ってマスクされる前のデータを求めます。
maskingKey = [0x37, 0xfa, 0x21, 0x3d] payloadData = [0x7f, 0x9f, 0x4d, 0x51, 0x58] unmaskedData = [] for i in range(len(payloadData)): unmaskedData.append(payloadData[i] ^ maskingKey[i%4]) # XOR # unmaskedData => [0x48, 0x65, 0x6c, 0x6c, 0x6f] print [chr(x) for x in unmaskedData] ['H', 'e', 'l', 'l', 'o']
このようにMasking-keyとPayload Dataを1バイトづつXORを取ることでマスクされる前のデータ(この例では"Hello"という文字列)を得ることができます。
また、送受信できるデータの種類はテキスト以外にもあり、opcodeの値を用いて指定することができます。
frame-opcode = %x0 ; 継続フレーム / %x1 ; テキストフレーム / %x2 ; バイナリフレーム / %3-7 ; 非制御フレームとして予約されている / %x8 ; 接続を閉じる / %x9 ; ping / %xA ; pong / %xB-F ; 制御フレームとして予約されている
シングルフレーム
FINビットがセットされたフレームで、データが一つのフレームの中に収まっている。
継続フレーム
FINビットがセットされていないフレーム。テキストフレームやバイナリフレームでは、ある程度の大きさのデータは断片化されて複数のフレームに分けて送受信されることがある。現在の仕様(Section 4.7)には、"Hello"という文字列を"Hel"と"lo"に分けたときの例として、以下のように書かれています。
o A fragmented unmasked text message * 0x01 0x03 0x48 0x65 0x6c (contains "Hel") * 0x80 0x02 0x6c 0x6f (contains "lo")
これを上記のデータフレームの構造に当てはめると以下のようになります。
*1フレーム目 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 +-+-+-+-+-------+-+-------------+-------+-------+-------+-------+ |0|0|0|0|0 0 0 1|0|0 0 0 0 0 1 1|0 1 0 0|1 0 0 0|0 1 1 0|0 1 0 1| + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + | | | | | | 0x01 | 0x03 | 0x48("H") | 0x65("e") | | | | | | +---------------+---------------+---------------+---------------+ |0 1 1 0|1 1 0 0| + - - - + - - - + | | | 0x6c("l") | | | +---------------+ *2フレーム目 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 +-+-+-+-+-------+-+-------------+-------+-------+-------+-------+ |1|0|0|0|0 0 0 0|0|0 0 0 0 0 1 0|0 1 1 0|1 1 0 0|0 1 1 0|1 1 1 1| + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + | | | | | | 0x80 | 0x02 | 0x6c("l") | 0x6f("o") | | | | | | +---------------+---------------+---------------+---------------+
実際にFirefoxで大きなサイズ(70000バイトくらい)のテキストを送信してみましたが、仕様のように断片化されたフレームの送信を確認することが出来ませんでした。
クローズフレーム
テキストやバイナリフレームとは異なり、WebSocket接続状態に関するフレームがあります。
クローズフレームは接続を閉じる際にサーバ・クライアントの両方から送信されるフレームで、内部に2バイトのステータスコードを含んでいます。これらのコードの値を用いることで接続を閉じる原因を相手に通知することが出来ます。いくつかのコードは仕様の中(Section 7.4)で定義されていますが、2バイト以降のデータにに関しては特に定義されていないので、デバッグ用途としてUTF-8文字列を埋め込むこともできるそうです(Section 4.5.1参照)。
Pingフレーム
これまでのWebSocketではサーバ・クライアント間で接続が確立したあと一定時間通信がない場合、セッションテーブルが切れ通信が出来なくなる可能性があったそうですが、現在の仕様ではPingフレームを送信することで解決できるようです。Firefox v6, v7ではabout:configでnetwork.websocket.timeout.ping.requestの値を変更することでPingフレームを送信させることができます。その際、フレーム内のPayload Dataには"PING"という文字列が埋めこまれていました。
Pongフレーム
接続先からPingフレームを受信したら、ただちにPongフレームを返さなければなりません。その際、フレーム内のPayload Dataの値はPingフレームの値と同じものを埋め込む必要があるそうです(Section 4.5.3参照)。
おわりに
今回、実装できなかった機能は、今後新たに情報が入り次第対応していこうかなーっと思ってます。