CherryPyとws4pyによるWebSocketチュートリアル
この記事は QiitaのPython Advent Calendar 2013 の7日目です。
はじめに
簡単に自己紹介をすると、筆者はOSやHCIを研究しているふりをしている、しがない院生です。現在、修士論文に向けPython+OpenCVでARシステムを構築しています。
来年からWeb系エンジニアとして労働に勤しむことなりそうなので、そろそろAdvent Calendarなどに参加して、技術力を高めなければと思っていた矢先、QiitaのPython Advent Calendarのあまりの過疎っぷりに絶望したので*1、折角なので、ちょうどいじり始めたCherryPyとws4pyについて書いてみたいと思います。
CherryPyとws4pyについてそれぞれ簡単に説明した後、実際にWebSocket通信を行うプログラムを紹介する三部構成となっています。コード多めです。動作はPython2.7で確認しています。
What is CherryPy?
CherryPy is a pythonic, object-oriented HTTP framework.
CherryPy — A Minimalist Python Web Framework
ということで、CherryPyは、いかにWebアプリケーションをPythonらしく*2書けるようにするかを追求したフレームワークです。 Pythonicでobject-orientedというのはHelloWorldプログラムに既に現れています。
import cherrypy class HelloWorld(object): def index(self): return "Hello World!" index.exposed = True cherrypy.quickstart(HelloWorld())
このプログラムを走らせて http://localhost:8080/ にアクセスすると ”Hello World!” が表示されるわけですが、ぱっと見、巷のモダンなフレームワークにあるようなルーティング系のコードが見当たりません。というのも、index
という名前のメソッドがあるので想像できるかもしれませんが、オブジェクトのプロパティ名(メソッド名)がURLに対応しているからです。これが、object-orientedの所以でしょう。
ちなみに、CherryPyが登場したのは2001年のようなので、Railsよりも古い、結構枯れたフレームワークだったりします。Railsのようなコッテコテなフレームワークが出たかと思えば、Sinatraのようにlightweightに行こうぜ的なフレームワークも現れ、一周回ってしてモダンっぽいフレームワークになった感があります。最近でもHuluやNetflixのような企業で使われたりしているらしいです。
ただし、日本では、Python自体がマイナーで、PythonでWebアプリケーションを作ることがまたマイナーで、CherryPyがPythonのWAFの中でも更にマイナーな立ち位置なので、CherryPyに関する日本語の情報は限りなく少ないです。なので、使い方を知るには、基本的に英語の情報、公式ドキュメントやソースコードを見ることになるかと思います。
- ドキュメント:CherryPy 3.2.4 Documentation — CherryPy 3.2.4 documentation
- ソースコード:cherrypy / CherryPy / source / — Bitbucket
インストール
pipで一発です。
$ pip install cherrypy
サンプルプログラム
使い方を全く説明しないのもアレですので、私自身がチュートリアルを読みながら作成したサンプルプログラムを載せておきます。このコードでは触れていませんが、RESTfulなAPIも作成できます(参考 )。Lenaさん画像はこちらから。
import os import cherrypy class Lena(object): exposed = True def _load_lena_png(self): path = os.path.join(os.path.dirname(__file__), "lena.png") with open(path, "rb") as f: data = f.read() return data def __call__(self): cherrypy.response.headers["Content-Type"] = "image/png" return self._load_lena_png() class Image(object): def __init__(self): self.lena = Lena() @cherrypy.expose def add(self, a=0, b=0): result = int(a) + int(b) return "a + b = {0} + {1} = {2}".format(a, b, result) class Root(object): image = Image() add = add @cherrypy.expose def index(self): return cherrypy.request.headers["Host"] @cherrypy.expose def calc(self, method, a, b): if method == "add": op = "+" result = int(a) + int(b) elif method == "sub": op = "-" result = int(a) - int(b) elif method == "mul": op = "*" result = int(a) * int(b) elif method == "div": op = "/" result = int(a) / int(b) else: return "invalid method ({0}).".format(method) return "a {0} b = {1} {0} {2} = {3}".format(op, a, b, result) if __name__ == "__main__": cherrypy.quickstart(Root())
基本的には、exposedプロパティがTrueな呼び出し可能なオブジェクト*3がURLとして扱われます。@cherrypy.expose
はexposedプロパティにTrueをセットしてくれるデコレータで、これを使えばよりPythonicな感じになります。
URLのパラメータは、直接、関数の引数に渡ります。”/action/p1/p2”は固定引数(positional arguments)としてdef action(self, p1, p2):
のように、”/action?p1=&p2=“はキーワード引数(keyword arguments)としてdef action(self, p1=None, p2=None):
のように受け取ることが出来ます。
What is ws4py?
ws4pyはその名の通り、PythonのためのWebSocketライブラリです。PythonでWebSocketを扱えるライブラリ・フレームワークは他にも色々あるのですが(参考)、今回はCherryPyをサポートしているws4pyを使うことにしました。
- ドキュメント:ws4py - A WebSocket package for Python — ws4py 0.3.3 documentation
- サンプルコード:WebSocket-for-Python/example at master · Lawouach/WebSocket-for-Python · GitHub
インストール
こちらもpipで一発です。
$ pip install ws4py
CherryPyとws4pyによるWebSocketプログラミング
ようやく本題となります。百聞は一見にしかずということで、サンプルコードです。
import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket class EchoWebSocketHandler(WebSocket): def _log(self, msg): cherrypy.log(msg, "WEBSOCKET") def opened(self): self._log("Opened!") def closed(self, code, reason): self._log("Closed!") def received_message(self, message): self._log("Echo '%s'." % str(message)) self.send(message.data, message.is_binary) class Root(object): @cherrypy.expose def index(self): return """\ <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>WebSocket</title> </head> <body> <div id="result"></div> <script> (function() { var result = document.getElementById("result"); var ws = new WebSocket("ws://localhost:3000/ws"); ws.onopen = function() { result.innerHTML += "Opened!" + "<br>"; ws.send("Hello!") }; ws.onclose = function() { result.innerHTML += "Closed!"; }; ws.onmessage = function(e) { result.innerHTML += e.data + "<br>"; ws.close() }; })(); </script> </body> </html> """ @cherrypy.expose def ws(self): pass if __name__ == "__main__": cherrypy.config.update({ "server.socket_host": "127.0.0.1", "server.socket_port": 3000, }) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() cherrypy.quickstart(Root(), "", config={ "/ws": { "tools.websocket.on": True, "tools.websocket.handler_cls": EchoWebSocketHandler, } })
プログラムを実行して http://localhost:3000 にアクセスすればWebSocketの動作確認ができます。
個人的に引っかかったのは、”server.socket_host” に ”127.0.0.1” を入れなければならなかった部分です。ここを “localhost” にすると、IPv4なのかIPv6なのか区別がつかないYO、と怒られてしまいます。
また、main部分の、CherryPyにws4pyのプラグインとツールをセットアップする部分なのですが、これはオマジナイだと考えてしまって良いかと思います。というか、CherryPyのプラグイン、ツールの仕組みを私がまだ理解できてないだけです…*4
これだけでは面白くないので、もう一つ、WebSocketで画像を送るプログラムも作ったので、そちらも掲載します。
import os import base64 import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket class LenaHandler(WebSocket): def _load_lena(self): path = os.path.join(os.path.dirname(__file__), "lena.png") with open(path, "rb") as f: data = "data:image/png;base64," + base64.b64encode(f.read()) return data def received_message(self, message): if str(message) == "Give me lena!": self.send(self._load_lena()) else: self.send("") class Root(object): @cherrypy.expose def ws(self): pass @cherrypy.expose def index(self): return """\ <!DOCTYPE html> <title>Get Image via WebSocket</title> <body> <div id="result"></div> <script> (function() { var result = document.getElementById("result"); var ws = new WebSocket("ws://localhost:3000/ws"); ws.onopen = function() { ws.send("Give me lena!") }; ws.onmessage = function(e) { if (e.data.length > 0) { var img = document.createElement("img"); img.src = e.data; result.appendChild(img); } else { result.innerHTML = "no data"; } ws.close(); }; })(); </script> </body> """ if __name__ == "__main__": cherrypy.config.update({ "server.socket_host": "127.0.0.1", "server.socket_port": 3000, }) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() cherrypy.quickstart(Root(), "", config={ "/ws": { "tools.websocket.on": True, "tools.websocket.handler_cls": LenaHandler, } })
ここでは、base64なData URIで送信することにしました。文字列を送る場合はws.onmessage
のe.data
は文字列型になりますが、バイナリで送るとBlob型になる点には注意が必要かもしれません*5。
まとめ
今回、CherryPyとws4pyを使えばPythonでも簡単にWebSocketが使えますよ、ということについて書きました。他にも、OpenCVでウェブカメラから画像持ってきて、適当な画像処理してリアルタイムでブラウザ側に送る、ライブ・ストリーミング的なプログラムも書いたりしているので、AdventCalendarが相変わらず過疎っていたらどこかで書かせていただくかもしれません。
それにしても、Pythonは、この手のWeb関係も然ることながら、数値計算、機械学習、ビッグデータ、画像処理など科学技術計算系の充実度は目覚ましいですね。この先、国内でも間違いなく需用が高まってくるかと思われます、、、とか最後にそれっぽいことを言ってみましたが、要するに何が言いたいかというと、みんなPython使おう。Python使おう。Python使おう。