lessでソースコードに色をつける(vimユーザ編)

はじめに

以下のページを見て脊髄反射で書いてみました。タイトル通りvimユーザ向けの内容です。
漢(オトコ)のコンピュータ道: lessでソースコードに色をつける

less.shの所在

vimがインストールされている環境であれば新たに何かをインストールする必要はありません。less.shというファイルを探しましょう!私の環境では以下の場所にありました。

Mac OSX 10.6, vim 7.2

/usr/share/vim/vim72/macros/less.sh

Ubuntu 11.10, vim 7.3

/usr/share/vim/vim73/macros/less.sh

このless.shは内部でvimコマンドを実行しているただのスクリプトファイルです。普通のlessコマンドのように引数にファイルを指定して実行してみて下さい。するとvimシンタックスでカラフルに表示されると思います!
さらにlessコマンドと同様に、表示中に「v」を入力するとそのままvimでファイルの編集ができちゃいます!

エイリアスを登録

毎回less.shのパスを打つのは面倒なのでエイリアスを登録しましょう。せっかくなので、インストールされているvimのバージョンを意識しなくてもすむ方法で設定しましょう。以下の2行を$HOME/.zshrcにでも追加してください。

vim_version=`vim --version | head -1 | sed 's/^.*\ \([0-9]\)\.\([0-9]\)\ .*$/\1\2/'`
alias less='/usr/share/vim/vim${vim_version}/macros/less.sh'

おわりに

私はvimユーザなのでemacsでも似たようなものがあるかわかりませんが、emacsユーザの方はこれを機にvimに移行してみてはいかがでs(ry

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();
「new出来なければ関数として実行する *1」について

Firefox v8, 9辺りではnew WebSocketServer(...)でインスタンス化できていましたが、v10.0a1ではnewしてもundefinedが返ってくるようになりました。原因はよく分からないのですが、関数呼び出しにして内部でnewすることでインスタンス化することができたので上記のような書き方になっています。

「クラス変数ではなくインスタンス変数(server.RUNNIN)を使う *2」について

new WebSocketServer(...)でインスタンス化できなくなったのと同時にWebSocketServerクラスのクラス変数が使えなくなりました(原因も同じかも?)。console.dir(WebSocketServer)で確認するとクラス変数(STOP: 0, RUNNING: 1)はちゃんと見れるのですが、実際にコードの中でクラス変数にアクセスするとundefinedになっています。なので、インスタンス変数にも同じ値を持たせることにしました。

アドオンの解説

簡単にですがWebSocketServerアドオンについて説明します。基本的にFirefoxのアドオンはJavaScriptで書くのですが、私はCoffeeScriptでコードを書いてJavaScriptコンパイルしています。ですので、以下の説明ではCoffeeScriptのコードやファイル名が出てきますが適宜JavaScriptに置き換えて読んでください。
# CoffeeScriptって良いですよね。これに慣れるとJavaScriptを書くのが面倒くさく感じます(^_^;)

WebSocketServerクラスについて

WebSocketSeverクラスはcontent/WebSocketServer.coffeeにて定義しています。このクラスではXPCOMnsIServerSocketインターフェースを用いてサーバ機能を実現しています。クライアントと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に合わせて作りましたが以下の機能については未実装です。

  • バイナリフレームの送受信
  • 分割されたフレームの送受信
  • subprotocolの対応
  • 機能拡張(Sec-WebSocket-Extensions)の対応
  • TLS通信(wss://URI)の対応

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クラスを使えばFlashJavaScript間で連携できるので、以下の図のように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のメモ

はじめに

タイトルの通り、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と大差無いのが素晴らしいですね!
今回は、VimCoffeeScriptを書く上で必要な設定をメモ帳代わりに残そうと思います。

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用のインデントやシンタックスなどが使えるプラグインです。VimCoffeeScriptを書く上で必要不可欠なものだと思います。

 ~ % 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ファイルに書き込まれないので注意して下さい。


これでVimCoffeeScriptを書く準備ができました!
もっと探せば便利なプラグインやvimrcの設定があると思いますが、上記の方法で整えた環境だけでも楽にプログラムが書けるようになったと思います。

WebSocket(hybi-07)でechoサーバを作ってみた

はじめに

以前、WebSocket Draft 76でechoサーバーを作ってみましたが、今回はhybi-07に対応したバージョンを作ってみました。しばらく見ない間に大きく仕様が変わっていて驚きました(*_*;

今回の実行環境は以下の通りです。

  • サーバ
  • クライアント

最新のWebSocketプロトコルを調べるために作成したプログラムなんですが、以下の機能については未実装です。

  • バイナリデータと断片化されたフレームの送受信
    • クライアントからこれらを送信する方法がわからなかったので
  • 拡張機能(Sec-WebSocket-Extensions)

実行の手順

まず、以下からプログラムをダウンロードします。

ダウンロードしたフォルダの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参照)。

おわりに

今回、実装できなかった機能は、今後新たに情報が入り次第対応していこうかなーっと思ってます。