Mosquitto Publish と Server-Sent Events で Python チャットサンプルを改造
前回の「Python WebSocket のチャットサンプルに、MongoDB 保存を付け加えました」の Python WebSocket チャットプログラムをベースに、ブラウザから POST メソッドで Mosquitto へ Publish し、また、Server-Sent Events でブラウザに表示する大改造を、今回は行います。
前回は、フレームワークに Flask を利用しましたが、今回は Bottle を利用します。WSGI Server に gevent を利用します。
次のプログラムは、mqtt-chat.py でセーブしました。
import sys,os import gevent from gevent import monkey; monkey.patch_all() from gevent.pywsgi import WSGIServer from bottle import get, post, route, view, request, response, redirect, Bottle import time from datetime import datetime sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../model') from mchat import (ChatLog, ConnLog, connecter) from mqttc import (MQTTpub) app = Bottle(__name__) def getlist(lastid): wlist = [] wid = lastid if lastid == '0': posts = ChatLog.objects.order_by("-created_at")[:5] else: posts = ChatLog.objects(created_at__gt = int(lastid)).order_by("-created_at") for post in posts: wt = '<p>' + post.text + '</p>' vt = datetime.fromtimestamp(post.created_at) wt = wt + '<p>- ' + vt.strftime('%Y/%m/%d %H:%M:%S') + ' -</p>' wlist.append(wt) print 'text=%s' % post.text if wid == lastid: wid = str(post.created_at) return wid, wlist @app.post('/post') def post(): message = request.forms.get('message') print "post: %s" % message MQTTpub(message) @app.get('/stream') def stream(): lastid = '0' connid = request.query.id conns = ConnLog.objects(connid = int(connid)).order_by("-lastid") for conn in conns: if lastid == '0': lastid = str(conn.lastid) break response.content_type = 'text/event-stream' response.cache_control = 'no-cache' wlist = [] wid = lastid wid, wlist = getlist(lastid) if wid != lastid: conns.update(set__lastid=int(wid)) yield 'event: messages\n' for value in wlist: yield 'data: %s\n' % value yield 'id: %s\n' % wid yield 'retry: 10000\n' yield '\n' @app.route('/') @view('mqtt-chat') def index(): lastid = '0' wlist = [] wid = '0' wid, wlist = getlist(lastid) name = 'MQTTC' now = int(time.mktime(datetime.now().timetuple())) ConnLog(connid=now, lastid=int(wid), user=name).save() return dict(data=wlist, id=str(now), lastid=str(wid)) if __name__ == '__main__': connecter() server = WSGIServer(('0.0.0.0', 8080), app) server.serve_forever()
Server-Sent Events を利用するにあたっては、第2回の「MongoDB に保存した Mosquitto の payload を Server-Sent Events で表示してみる」の方式を前提としています。
次はテンプレートです。
mqtt-chat.py をセーブしたディレクトリの直下に views ディレクトリを作成して mqtt-chat.html でセーブしました。
<!doctype html> <title>chat</title> <head> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> </head> <body> <h3>Chat</h3> <p>Message: <input id="in" /></p> <div id="out"> % for item in data: {{!item}} % end </div> <script> $('#in').keyup(function(e){ if (e.keyCode == 13) { $.post('/post', {'message': $(this).val()}); $(this).val(''); } }); </script> <script> var lastid = {{lastid}}; function sse(){ var source; if (typeof (EventSource) !== 'undefined') { source = new EventSource('/stream?id={{id}}'); source.addEventListener('messages', function(event){ if (event.lastEventId > lastid) { document.getElementById('out').innerHTML = event.data + document.getElementById('out').innerHTML ; lastid = event.lastEventId; } },false); source.addEventListener('end', function(event){ if (event.lastEventId > lastid) { document.getElementById('out').innerHTML = event.data + document.getElementById('out').innerHTML ; lastid = event.lastEventId; } source.close(); },false); source.onerror = function (event) { if (source.readyState === EventSource.CLOSED) { document.getElementById('out').innerHTML = '<p>終了しています。</p>' + document.getElementById('out').innerHTML ; } else if (source.readyState === EventSource.OPEN) { document.getElementById('out').innerHTML = '<p>終了します。</p>' + document.getElementById('out').innerHTML ; source.close(); } else if (source.readyState === EventSource.CONNECTING) { } } } else { document.getElementById('out').innerHTML = '<p>Server-Sent Events はサポートされていません。</p>' ; } } window.onload = sse; </script> </body> </html>
MVC のモデルにあたる mchat.py は、次のようにしました。
from mongoengine import * class ChatLog(Document): text = StringField(required=True) user = StringField(required=True) created_at = LongField(required=True) class ConnLog(Document): connid = LongField(required=True) lastid = LongField(required=True) user = StringField(required=True) def connecter(): con = connect('chattest') print con
さらに、Mosquitto への Publish の部分も別モジュールにして、mqttc.py でセーブしました。
import paho.mqtt.client as paho def MQTTpub(msg): mqttc = paho.Client(client_id="gwclient", clean_session=True, protocol=paho.MQTTv311) mqttc.username_pw_set("mqtt", "mqttpasswd") mqttc.connect("127.0.0.1", 1883, 60) mqttc.publish("test/chat", msg, 0) mqttc.disconnect()
サーバー環境は、ConoHa VPS で、CentOS 6.5 です。
Android の VNC クライアントアプリ bVNC Pro から確認を行いました。
これまでの改造サンプルプログラムそのままで、サービス公開するようなことは、セキュリティなどからみても、ありえないことですが、Server-Sent Events の部分を公開ページに組み入れて、ひとりタイムライン公開のようなことはできそうです。
今回の Server-Sent Events では、約 10 秒間隔で retry 再接続するように設定しています。ブラウザからメッセージを POST してから、そのメッセージがブラウザに表示されるまで、そのぐらいの時間がかかります。
第1回の「Mosquitto の payload を MongoDB に保存してみる」では、Mosquitto の payload を MongoDB に保存しました。
第2回の「MongoDB に保存した Mosquitto の payload を Server-Sent Events で表示してみる」では、HTML5 の Server-Sent Events と PHP でブラウザに表示しました。
第3回の「Python WebSocket のチャットサンプルに、MongoDB 保存を付け加えました」では、Python で WebSocket を利用した簡単なチャットプログラムを参考に MongoDB アクセスを書き加えてみました。
今回の第4回は、第3回の Python の WebSocket チャットプログラムを参考にしつつ、ブラウザから POST メソッドで Mosquitto へ Publish し、また、Server-Sent Events でブラウザに表示する方式を Python で試してみました。
また、「Mosquitto へ SSL で接続してみる」では、SSL/TLS での接続にトライします。