pythonによるWebSocketサーバー

 前回からの続きです。なにせpythonによる初めてのコードだったので、最初に色々調べました。といっても全てネット上で得られる知識で何とかなりました。そもそもpythonのネットワークプログラミングやプロセス間通信についての書籍は極端に少ないです。

 調査に1か月、コーディングに半月といった感じの仕事でした。プログラム自体はブラウザを相手にするWebSocketサーバー機能とDBを監視する子プロセスとの通信機能の両方を備えたいわゆる「ゲートウェイdaemon」です。perlで作ったDEN将棋EXよりはずいぶん単純なものです。今回は、WebSocketサーバー機能の実装についての備忘禄です。

ウェブソケットサーバー

DBのあるテンポラリーテーブルの監視を子プロセスにやらせるので、ゲートウェイは上位のブラウザと下位の子プロセス双方からのデータを非同期受信(イベントドリブン)できる仕組みが必要です。

まずは、クライアントであるブラウザとの通信を行うウェブソケットサーバー機能の実装についてです。簡単に実現できそうなモジュールに「websocket-server」というのがあります。pipで簡単にインストールできます。やり方は適当にググってください。下記はこれを使ったサンプルコードです。クライアントから受信したメッセージにHello!!!を付加して送り返すだけのコードです。(****は伏せ字。待ち受けポート番号)

「websocket-server」モジュールを使った例

import logging , asyncio, sys
from websocket_server import WebsocketServer
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(' %(module)s -  %(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
# Callback functions
def new_client(client, server):
  logger.info('New client {}:{} has joined.'.format(client['address'][0], client['address'][1]))
def client_left(client, server):
  logger.info('Client {}:{} has left.'.format(client['address'][0], client['address'][1]))
def message_received(client, server, message):
  logger.info('Message "{}" has been received from {}:{}'.format(message, client['address'][0], client['address'][1]))
  reply_message = 'Hello!!! : ' + message
  server.send_message(client, reply_message)
  logger.info('Message "{}" has been sent to {}:{}'.format(reply_message, client['address'][0], client['address'][1]))
# Main
if __name__ == "__main__":
  server = WebsocketServer(port=****, host='127.0.0.1', loglevel=logging.INFO)
  server.set_fn_new_client(new_client)
  server.set_fn_client_left(client_left)
  server.set_fn_message_received(message_received)
  server.run_forever()

このコードでは、serverオブジェクトを作ったら、set_fn_message_receivedメソッドに受信イベントハンドラ(コールバック)を渡します。そして、run_forever()。これで受信イベントループを回しているわけです。あとはこのループの中で、受信データを捉えると、渡してあるコールバック関数(受信イベントハンドラ)が非同期実行されます。

perlのAny::Eventで、同様のゲートウェイdaemonを実現している私は、ここで当然ある疑問にたどり着きます。そうです。上位(ブラウザ)からの受信はこれで良いが、下位(子プロセス)からの非同期受信どうすればいいの?

私の答えは、「このモジュールを使いながら、下位からの非同期受信も実現するには大変な工夫と苦労が必要となるはずだからあきらめよう。」というものでした。

そこでさらに調べていくと、紛らわしいのですが「websockets」というモジュールが有る事がわかりました。下記はこのモジュールの公式ドキュメントです。

websockets

このモジュールの特徴は、ayncio由来のイベントループを使用するという事。asyncioというのはperlのAnyEventとCoroをドッキングしたようなモジュールだと思います。このasyncioは、coroutineを定義できるので、下位からの受信イベントハンドラと上位からの受信イベントハンドラを両方とも同じイベントループに組み入れることが出来そうです。

これって、perlでもAny::Event由来のwebsocketサーバーモジュールとそうでないwebsocketサーバーモジュールが有ったのと良く似ています。

AnyEventなウェブソケットサーバーのバイナリデータ送信

下記はwebsocketsモジュールによるサーバーの起動例です。(****は伏せ字。待ち受けポート番号)

「websockets」モジュールによるサーバーの起動例

import websockets, asyncio
・・・・・・・・・・・・・・
start_server = websockets.serve(message_received, "localhost", ****)

message_receivedは受信イベントハンドラ(コールバック)です。この関数は、async def によって、coroutineとして定義してあります。そして、下位からの受信イベントハンドラも同様にcouroutineとして定義し、両方のイベントハンドラを、asyncioが提供するイベントループに組み入れることができるのです。

WebSocketクライアントからの非同期受信と子プロセスからの非同期受信を共存させるイベントドリブンなコードは、asyncioモジュールによります。次回はこの説明をしたいと思います。