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アプリケーションを作ることがまたマイナーで、CherryPyPythonのWAFの中でも更にマイナーな立ち位置なので、CherryPyに関する日本語の情報は限りなく少ないです。なので、使い方を知るには、基本的に英語の情報、公式ドキュメントやソースコードを見ることになるかと思います。

インストール

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を使うことにしました。

インストール

こちらも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.onmessagee.dataは文字列型になりますが、バイナリで送るとBlob型になる点には注意が必要かもしれません*5

まとめ

今回、CherryPyとws4pyを使えばPythonでも簡単にWebSocketが使えますよ、ということについて書きました。他にも、OpenCVでウェブカメラから画像持ってきて、適当な画像処理してリアルタイムでブラウザ側に送る、ライブ・ストリーミング的なプログラムも書いたりしているので、AdventCalendarが相変わらず過疎っていたらどこかで書かせていただくかもしれません。

それにしても、Pythonは、この手のWeb関係も然ることながら、数値計算機械学習、ビッグデータ、画像処理など科学技術計算系の充実度は目覚ましいですね。この先、国内でも間違いなく需用が高まってくるかと思われます、、、とか最後にそれっぽいことを言ってみましたが、要するに何が言いたいかというと、みんなPython使おう。Python使おう。Python使おう。

*1:Adventarの方(http://www.adventar.org/calendars/166)は余裕の満席なのに…なんで!?

*2:Pythonic

*3:普通の関数、クラスのメソッド、関数オブジェクト(__call__)

*4:CherryPyのこの辺の仕組みは、ちょっと分かりづらいですね

*5:これはJSのWebSocketの仕様の問題です。ws.binaryTypeに 'arraybuffer' をセットすると、e.dataはArrayBuffer型になります。