2015/04/16

ポータブルな Firefox & Python で WebSocket 通信テスト

[Python][Firefox][WebSocket]
psycopg2 でリアルタイムに PostgreSQL のメッセージ取得(2015/04/13)の続きです。時間のかかる複数のクエリを PostgreSQL で行い、その進行状況を RAISE INFO でリアルタイムに Python に返すまで出来ました。次は Web サーバを加え、クライアント(ブラウザ)にも進行状況をリアルタイムで伝える仕組みができたらいいなと。

今日はひとまず PostgreSQL を置いて pywebsocket とブラウザの通信だけテストしました。↓ がその様子で、pywebsocket のスタンドアロンサーバを使用。ブラウザからのリクエストを受け、サーバから一秒おきにデータをプッシュし、受け取ったブラウザが画面に追加します。実行環境は動画の下に、ソース一式はもう少し下の方です。

■ 20150416_demo.mp4


実行環境は Windows 7 32bit + Python 2.7 + pywebsocket 0.7 + Firefox 36 ポータブル版です。Python も 2015/04/07 に書いた QGIS ポータブル版に入っていたものなので、丸ごと持ち運べる WebSocket サーバ & クライアントになります。Wikipedia の記事では WebSocket のブラウザ側での実装が発展途上らしいですが、今回試した Firefox 36 ではひとまず問題ないようです。

pywebsocket の使い方については、下記二つの記事が参考になりました。m(_)m

■ 技術/HTML5/WebSocket(msakamoto-sf さん)
■ WebSocketによるクライアント=サーバー通信 - libro

以下、ソースを含むテストまでの経過です。まず pywebsocket 公式サイトのダウンロードページから、最新の mod_pywebsocket-0.7.9.tar.gz をダウンロード。なお上記ページは今年いっぱいで終わる Google Code にあるので、いずれアドレスが無効になると思います。


上の圧縮ファイルのうち、今回必要だったのは mod_pywebsocket フォルダのみ。ポータブル版 QGIS フォルダの下に pywebsocket というフォルダを作り ↓ その中に解凍し、同じ場所に五つのファイルを作成。これがソース一式です。20150416_demo.zip にまとめました。


以下、作業経過とファイルの概要を書きます。まず pywebsocket のスタンドアロンサーバ起動・再起動用の Python スクリプト ↓ を作成。mod_pywebsocket/standalone.py を実行するユーティリティで、先週 wsgiref で行なった Python バッチファイル一つで Web サーバの起動も再起動もする(2015/04/11)とほぼ同じです。これを実行するたび、サーバのプロセスが未存なら起動、既存ならプロセスを閉じて再起動します。ポート番号など standalone.py に渡す引数をここで設定。ドキュメントルートを特に指定していないので、このスクリプトの場所がルートになります。

■ ctr_pywebsocket.py
import os
import signal
import subprocess

fp_current = os.path.abspath(os.path.dirname(__file__))
fp_pid = fp_current + '/pid.txt'
fp_script = fp_current + '/mod_pywebsocket/standalone.py'
cmd = ['python', fp_script, '-p', '8180']

def start_server(fp_pid):
# check the server is running
if os.path.exists(fp_pid):
stop_server(fp_pid)

# start server
process = subprocess.Popen(cmd)

# create pid file
with open(fp_pid, 'w') as pid_file:
pid_file.write(str(process.pid))
return True

def stop_server(fp_pid):
# check server if is running
if not os.path.exists(fp_pid):
return

# try to terminate/stop server's process
with open(fp_pid) as pid_file:
pid = int(pid_file.read())
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass #ignore errors if process does not exist

# remove pid file
os.unlink(fp_pid)

if __name__ == '__main__':
start_server(fp_pid)


上のスクリプトを繰り返し実行し易くするため、バッチファイル ↓ を作成。昨日書いた Console は、なぜかサーバをすぐ閉じてしまうので cmd.exe を使いました。実行時は一瞬コマンドプロンプトが出て、すぐタスクバーに収納されます(2015/04/11 と同じ方法)。

■ test_pywebsocket.bat
@echo off

set args=cmd /c "%~0"
:: set args=..\..\Console2\Console.exe -r "cmd \"%~0\""
:: Console を使うと、スクリプト実行後に閉じてしまうので使わない

::
:: コマンドプロンプト最小化で起動
:: http://piyopiyocs.blog115.fc2.com/blog-entry-731.html
::
if not "%HOGE%"=="hoge" (
set HOGE=hoge
start /min %args%
exit
)

:start
set PYTHONHOME=../QGIS/apps/Python27
set PYTHONPATH=.
echo.
echo %time% Started pywebsocket...
"../QGIS/bin/python" ctr_pywebsocket.py


以上二つのファイルを作ったのは、サーバ内で動かす Python スクリプトの修正と確認をし易くするためです。サーバ起動中はスクリプトを修正しても反映されず、確認のたび再起動が必要。それをバッチ実行一つで、ウィンドウも邪魔にならない形で行えます。

↓ サーバ起動中のコマンドプロンプト。Python スクリプトのエラーもここに表示されます。


次に、ブラウザから呼び出す簡単な HTML & JavaScript ↓ を作成。pywebsocket のスタンドアロンサーバは、静的なファイルをそのまま返せるので便利です。サーバ起動中に修正すれば、Python スクリプトと違ってすぐ反映されます。

■ index.html
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Portable pywebsocket test</title>
<script type="text/javascript" src="main.js"></script>
<style type="text/css"><!--
body { font-family:monospace; padding:20px; font-size:13px }
#text1 { margin:15px 0 10px; width:40% }
#res { line-height:1.5em; margin:10px }
--></style>
</head>
<body>
<div id="title"></div>
<input type="text" id="text1" value="何か書いて" />
<input type="button" onclick="action();" value="送る" />
<div id="res"></div>
</body>
</html>


■ main.js
// coding: utf-8

function $id(x) { return document.getElementById(x) }

function action(){
var res = $id('res');
var socket = new WebSocket('ws://localhost:8180/test');

socket.onopen = function(e){
socket.send($id('text1').value);
res.innerHTML = '受信開始...<br />';
}
socket.onmessage = function(e){
res.innerHTML += e.data + '<br />';
}
socket.onerror = function(){
res.innerHTML += '<b>エラー発生</b><br />';
}
socket.onclose = function(){
res.innerHTML += '受信終了';
}
}

window.addEventListener('load', function() {
$id('title').textContent = document.title;
});


上の main.js が WebSocket 通信の初歩的なひな形になります。送信ボタンが押されたら、まず ws://localhost:8180/test に接続。onopen イベントを受けて文字列を送り、onmessage でサーバから返るデータを受信。サーバが処理を終えたら onclose イベントでキャッチします。

test というアドレスがリクエストされたら、サーバ側は test_wsh.py というスクリプトを探して実行します。その中身が ↓ で、web_socket_transfer_data 関数がメイン。ws_stream.receive_message でブラウザからのデータを受信し、別の関数で適当に返す文字を作り ws_stream.send_message で一秒おきに送信。スクリプト終了まで接続が維持され、終了時はブラウザに onclose イベントが起きます。なお import で pywebsocket のモジュールを特に指定していませんが、今回はこれで動きました。

■ test_wsh.py
# coding: utf-8

import datetime
import time
import random

def web_socket_do_extra_handshake(request):
pass

def web_socket_transfer_data(request):
while True:
line = request.ws_stream.receive_message()
if isinstance(line, unicode):
response(request, line)
return

def response(request, line):
cnt = random.randint(10, 20)
for i in range(0, cnt):

# 文字列のシャッフル
# http://stackoverflow.com/questions/17649875/why-does-random-shuffle-returns-none
#
str_time = datetime.datetime.now().strftime('%H:%M:%S')
str_line = sorted(list(line), key=lambda k: random.random())
str_line = str_line[0 : random.randint(1, len(line) - 1)]
res = '%s (%d/%d) : %s' % (str_time, i+1, cnt, ''.join(str_line))
request.ws_stream.send_message(res, binary=False)
time.sleep(1)


Python での文字列のシャッフルは、なぜか random.shuffle が動かず Stack Overflow - Why does random.shuffle returns None? の方法にしました。今回 PostgreSQL と接続していないので、ダミーの送信データを Python 内で作った格好。

次は、ダミーでなく PostgreSQL からのデータ & メッセージにしたいですが、その前に一つ課題として、ブラウザからキャンセル指示を送り、サーバ側できちんと中止し、そのメッセージを送れないか検討します。一方的にブラウザ側で中止するだけでは、まずいので。
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。