Block Rockin’ Codes

back with another one of those block rockin' codes

pywebsocketの導入とDraft75フォールバック対応

[追記]
id:Ehren さんが最近のWebSocket事情のまとめ記事を公開されました。
ブラウザの対応関係等わかりやすくて有益です!
最近のWebSocket事情についてまとめとく - Keep on moving



HTML5-hack-a-thonに参加しました。
自分の参加しグループは「ニコニコ動画的なもの」を作ろうと言うことで、

  • コメントが右から流れてくる
  • 映像にエフェクトがつく
  • コメントやエフェクトがリアルタイムに共有出来る

といったような内容でCanvasやWebSocketを使ったアプリを作成しました(未完成でしたが、チームの皆さんお疲れさまでした!)

自分はWebSocket周りを担当したので、その時の備忘録を残します。

WebSocketとは

簡単にいうと、サーバがクライアントに対してpushが出来る技術です。
もっと正確に言えば、これまでのAjax等の通信は基本的に、サーバに対してリクエストを投げて、その結果を受け取る、データを引っ張ってくる(pull)が中心でした。
しかし、WebSocketではサーバ側がデータをクライアントに送ることが出来るので、これによって双方向通信が可能になり、これまで出来なかったリアルタイムな処理が実現出来ます。
最初はHTML5の仕様の一つとして提案されていましたが、今はHTML5から外れて別の仕様として策定されているようです。(つまり正確にはHTML5じゃないということか)


気にはなっていたのですが結局まだ手をつけていなかったので、思い切ってやってみましたが、ハマりどころを上手く回避出来れば思った以上に簡単に出来ました。
双方向通信はGoogle Channel APIが来る前に一度やっておきたかったので、良い経験になりました。

WebSocketの注意点

WebSocketは、接続の確立までがHTTPで、その後は別のプロトコルで通信という仕様になっています。"http:"ではなく"ws:"。
なので、HTML5系の技術はブラウザのサポートだけ気にすれば良い物も多いのですが、WebSocketはサーバ側の対応が必要です。

今では、サーバ側の実装も色々出ているようです。

  • pywebsocket
  • em-websocket
  • jetty
  • node.js


ただし、WebSocketにはDraft75と76という二つの仕様があり、現行では2種類の実装が混在しているようです。
一番やっかいなのは、二つの仕様に互換性が無い(同時に二つは使えない?)ということです。

そして、クライアント(ブラウザ)の実装しているDraftとサーバの実装するDraftが一致していないと通信が確立出来ないということです。
(やり取りするデータのヘッダ等が違う。)


とりあえずpywebsocket

まず試しに、websocketの実装としてるpywebsocketのスタンドアローン版を導入してみました。


pywebsocket - WebSocket server and extension for Apache HTTP Server for testing - Google Project Hosting


pywebsocketはApacheのモジュール(mod-pywebsocket)として起動するものですが、Apacheと連携しないでも起動出来るStandalone用のスクリプトが同梱されています。
現在、WebSocketをちょっと試してみたいという程度であれば、このStandalone版が一番手軽なようです。



手順は以下の通り

ソースを入手

ダウンロードかsvn-checkoutでソースを入手します。
今回は現行のver0.5.2

スクリプトを書く

今回は、ただ単にクライアントが送って来たデータをそのまま、接続して来ているクライアント全員に送り返すだけの簡単な処理です。

# -*- coding:utf-8 -*-
from mod_pywebsocket import msgutil

#接続して来た人を入れとく配列
connections = []

def web_socket_do_extra_handshake(request):
    #全てのリクエストを受け付ける。
    pass 

def web_socket_transfer_data(request):
    #受け付けたコネクションを保存する。
    connections.append(request)

    #ひたすら繰り返す
    while True:
        #メッセージを受け付ける
        try:
            message = msgutil.receive_message(request)
        except Exception:
            return
        #受け付けたメッセージをそのまま全員に返す
        for con in connections:
            try:
                msgutil.send_message(con, message)
            except Exception:
                connections.remove(con)

クライアント側はこんな感じです。

$(function(){
	//WebSocketオブジェクトの取得
	var ws = new WebSocket("ws://localhost/myapp");
	console.log(ws);
	ws.onmessage = function(event){
		console.log('受信');
		console.log(event.data); //ここに受け取ったデータ
	};
	
	ws.onopen = function(event){
		console.log('通信開始');
		console.log(event);
	};

	ws.onclose = function(event){
		console.log('通信終了');
		console.log(event);
	};

	$('#submitbutton').click(function(){
		console.log('送信');
		ws.send(name); //これがデータの送信
	});
});
起動

スタンドアローンとして起動する場合は、pywebsocket-read-only/src/mod_pywebsocketにある、standalone.pyを起動します。
起動オプションには注意が必要です。

  • 起動するにはpython2.5を使う。
  • -dでドキュメントルートの場所を指定
  • -wでサーバ側スクリプトの場所を指定
  • -pでポートの場所を指定


python2.6で起動すると

DeprecationWarning: the md5 module is deprecated; use hashlib instead

が出ました、これはまあ使うモジュールを変えろという警告なんですが、2.5なら出ませんので、2.5でやれば良いと思います。


dは先ほどの.jsを読み込んだhtmlの場所を指定します。このディレクトリを起点にクライアントに送信するファイルを配置します。


wはサーバ側の.pyスクリプトを置いた場所、ここは-dので指定した場所からの相対パスになります。同じディレクトリであれば指定は不要です。


pはポートの指定です。WebSocketはデフォルトで80を使いますが、80が塞がっている場合は多いでしょう、その場合は適当に空いているポートを指定します。その場合は、jsの中のWebSocketサーバの指定も変わります。


自分は80番が埋まっており、その場合

[CRITICAL] root: (13, 'Permission denied')

というようなエラーが出たので、-p 1234と別のポートで上げたら動きました、なのでjsも

- var ws = new WebSocket("ws://localhost/myapp");
+ var ws = new WebSocket("ws://localhost:1234/myapp");

あと、/myappの部分のしてはpywebsocketの仕様で、この指定で-wで指定したディレクトリにあるmyapp_wsh.pyにハンドシェイク(接続)を要求するようです。
ここもディレクトリ構成が変われば変わります。


結果、

python2.5 standalone.py -d path/to/app.html -w path/to/app_wsh.py -p 1234

これで起動すればサーバ側の準備は完了。

接続

これでブラウザからhttp://localhost:1234/app.htmlを叩けば、ソケット通信が出来るはずなのですが、ブラウザでlogを見ると、

Unexpected response code:404

なぜか上手くハンドシェイクできていませんでした。


どうやら原因は、先ほどのDraftの問題のようです。
pywebsocketは現行Draft76で実装されているようですが、自分が試したChrome5はDraft75にしか対応していないというのが問題のようでした。
選択肢は二つあります。

  • Draft76に対応したChrome6にする
  • pywebsocketでDraft75を対応させる。


前者は簡単ですね、後者を説明します。

pywebsocketをDraft75フォールバック対応

結果から言うと、pywebsocketは現行は76で実装されていますが、standalone.pyの404行目あたりの--allow-draft75をdefault=Trueにすればフォールバックが効いて、75でも動くようになります。

    parser.add_option('--allow-draft75', dest='allow_draft75',
#                      action='store_true', default=False,
                      action='store_true', default=True,
                      help='Allow draft 75 handshake')

これで起動し直すと、Draft76の接続を試して、ダメだったらDraft75での通信に切り替えてくれるようです。
しかし、どうやら76を試す最初の通信一回だけはどうも通信ミスになって、二回目から普通に通信出来るという挙動のようです。
なので、念のため最初の一回は通信確立用に捨てる様なクライアントの実装をしとくと良さそうです。


これで無事動くはずです。
(この辺さえハマらなければ手軽です)

Draftの実装状況

こうなってくると、どのブラウザがどのDraftで実装されているのかを調べる必要が出てきますが、今一つまとまったリソースがみつからずにいました。
しかし、そんなことをつぶやいたところ、@のにいくつか情報を頂きました。ありがとうございます!!


@さんはWebSocket周り色々詳しく、今回もサーバの実装状況等多くの助言を頂きました。
Draftの対応状況も詳細はそのうちblogにまとめるとのことなので、楽しみに待ちたいと思います!
(このへんの対応状況とかまとめた記事はぱっとは出てこないので、かなり重宝しそうです)

もう少し先へ

Draftの問題もそうですが、そもそもWebSocketに対応したブラウザ自体もまだまだ多いとは言えないでしょう。
そこで、今回HTML5-hack-a-thonでは結局このpywebsocketではなく、node.jsの実装であるsocket-io-nodeを使うことにしました。
長くなったので、その記事については次回に分けます。