はじめに
ふとしたきっかけでチャットアプリの実装が必要になったため
Flaskを使ってWebSocketアプリケーションをどうやって実装できるか調べました。
サンプルを動かしてみるところまで書きますが
これだけで、チャットの主な機能は十分使えてしまいます。
エクステンションの選定
Flaskの肩に乗っかって実装するつもりで
選択肢に挙がったのが以下の2つのエクステンションです。
- Flask-SocketIO: miguelgrinberg氏作
- Flask-Sockets: kennethreitz氏作
2014年2月と少し古いのですが、miguelgrinberg氏による解説記事に
両者の比較を説明している箇所があります。
++++++++++++++++++++++++++++++++++++++++++++++++++
The main difference between Flask-Sockets and Flask-SocketIO is that the former wraps the native WebSocket protocol (through the use of the gevent-websocket project), so it can only be used by the most modern browsers that have native support. Flask-SocketIO transparently downgrades itself for older browsers.
Flask-SocketsはネイティブのWebSocketプロトコルを
gevent-websocketを通してラップしている。
ネイティブでWebSocketをサポートしているモダンブラウザのみで使用される。
一方で、Flask-SocketIOならもう少し古いブラウザでも使用可能。
Another difference is that Flask-SocketIO implements the message passing protocol exposed by the SocketIO Javascript library. Flask-Sockets just implements the communication channel, what is sent on it is entirely up to the application.
Flask-SocketIOはJavaScriptのSocketIOライブラリと
メッセージをやりとりするためのプロトコルを実装している。
Flask-Socketsはコミュニケーションのチャネルを実装しているのみであり
何を送るかはアプリケーションに委ねられている。
Flask-SocketIO also creates an environment for event handlers that is close to that of regular view functions, including the creation of application and request contexts. There are some important exceptions to this explained in the documentation, however.
Flask-SocketIOはビュー関数に近接するイベントハンドラのための環境を生成する。
これはアプリケーションコンテキストやリクエストコンテキストの生成を含む。
ドキュメントの中で説明されているように、例外はあるが。
++++++++++++++++++++++++++++++++++++++++++++++++++
3点目の翻訳がうまくできてないものの
要は既存のFlaskアプリにシームレスに実装できるということだと思います。
作者の信頼度もGitHubのスターも同程度で悩みましたが
- アップデートの頻度が高い
- すぐに動くサンプルが付属している
という点から、Flask-SocketIOを選択しました。
WebSocketプロトコルにがっつり触れたい場合や、クライアント(JavaScript)側に
自由度を求める場合はFlask-Socketsを使うべきでしょうか。
とはいえ、どちらでもやりたいことは実現できそうです。
準備
前回の記事と同様にローカルのDockerを使用します。
私の実行環境はDocker for MacのPublic Betaです。
$ docker --version
Docker version 1.12.0, build 8eab29e, experimental
手早く環境を作るするためにDockerを使っているだけなので
ある程度新しいPythonが動く環境があれば、環境構築部分は読み飛ばしてください。
ソース
flask_socket/
├ Dockerfile
└ requirements.txt
Dockerfile
# ベースイメージの指定
FROM python:3.5.2-alpine
# ソースを置くディレクトリを変数として格納
ARG project_dir=/web/socket/
# apkでインストールできるパッケージの更新後にgitをインストール
RUN apk update
RUN apk add git
# requirements.txtに記載されたパッケージをインストール
WORKDIR $project_dir
ADD requirements.txt .
RUN pip install -r requirements.txt
# GitHubのリポジトリからFlask-SocketIOのソースコードを取得
RUN git clone https://github.com/miguelgrinberg/Flask-SocketIO.git Flask-SocketIO
WORKDIR $project_dir/Flask-SocketIO/example
requirements.txt
Flask
とFlask-SocketIO
をpipでインストールしたあとに
$ pip freeze > requirements.txt
したもの。
click==6.6
Flask==0.11.1
Flask-SocketIO==2.6.2
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
python-engineio==0.9.2
python-socketio==1.4.4
six==1.10.0
Werkzeug==0.11.10
Flask
の本体が依存しているパッケージ以外では
Flask-SocketIO
、python-engineio
、python-socketio
、six
が追加されています。
手順
イメージ作成
$ cd /path/to/flask_socket/
$ docker build -t flask_socket .
-
-t flask_socket
: 作成するイメージの名前を'flask_socket'にする -
.
: カレントディレクトリにあるDockerfileを使用
コンテナ起動
$ docker run -p 5000:5000 -it flask_socket /bin/sh
-
-p 5000:5000
: hostの5000番ポートをコンテナの5000番ポートに向ける -
-it
: 現在の入力デバイスでコンテナを操作 -
flask_socket
: イメージ名の指定 -
/bin/sh
: 起動したコンテナでshコマンドを実行
コードの追加
アプリケーション実行のコードにhost
の設定を追記します。
# socketio.run(app, debug=True)
socketio.run(app, host='0.0.0.0', debug=True)
サンプルアプリケーションの実行
$ python app.py
ブラウザからlocalhost:5000
にアクセスすると、以下のような画面が表示されます。
'Send:'フォームの一部を解説
-
Echo
: メッセージを入力したユーザにオウム返し -
Broadcast
: すべてのクライアント(同じページを開いているすべてのユーザ)に送信 -
Disconnect
: サーバとの接続を切断
Echo
とBroadcast
の違いはタブを並べてみるとわかりやすいと思います。
他の項目はチャットルーム関連の操作と解釈して割愛。
ちなみに部屋の名前は指定できるものの、自分の名前は入力できません。
コードの解釈
アプリケーション実行時の処理
# 必要なモジュールの読み込み
from flask import Flask, render_template, session, request
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect
# 非同期処理に使用するライブラリの指定
# `threading`, `eventlet`, `gevent`から選択可能
async_mode = None
# Flaskオブジェクトを生成し、セッション情報暗号化のキーを指定
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
# Flaskオブジェクト、async_modeを指定して、SocketIOサーバオブジェクトを生成
socketio = SocketIO(app, async_mode=async_mode)
# スレッドを格納するためのグローバル変数
thread = None
# SocketIOサーバをデバッグモードで起動
socketio.run(app, debug=True)
ページを開いた際の処理
html内のscript内で、SocketIOライブラリ(JavaScript)が読み込まれます。
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
その後、ネームスペース(WebSocket通信のエンドポイント)の指定と
SocketIOサーバとの接続が行われます。
サーバ接続時に発行されるイベントのハンドラ指定や
my response
イベントのハンドラ指定もここで行われています。
レスポンスは#log
セレクタで指定されたタグに追加されていくようです。
namespace = '/test';
var socket = io.connect('http://' + document.domain + ':' + location.port + namespace);
socket.on('connect', function() {
socket.emit('my event', {data: 'I\'m connected!'});
});
socket.on('my response', function(msg) {
$('#log').append('<br>' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());
});
サーバ側では以下のコードでクライアントからの接続要求を受け
my response
イベントを発火させることでレスポンスを返しています。
socketio.start_background_task(target=background_thread)
の部分は後述します。
@socketio.on('connect', namespace='/test')
def test_connect():
global thread
if thread is None:
thread = socketio.start_background_task(target=background_thread)
emit('my response', {'data': 'Connected', 'count': 0})
以上のコードにより、Responseログの初期動作部分が表示されます。
Received #0: Connected
Received #1: I'm connected!
PythonとJavaScriptの両方で、イベントに対するハンドラを記載することにより
双方向のやりとりを定義できることがわかりました。
Echo/Broadcastの際の処理
Echo/BroadcastはFormタグが以下のように記載されており
<form id="emit" method="POST" action='#'>
<input type="text" name="emit_data" id="emit_data" placeholder="Message">
<input type="submit" value="Echo">
</form>
<form id="broadcast" method="POST" action='#'>
<input type="text" name="broadcast_data" id="broadcast_data" placeholder="Message">
<input type="submit" value="Broadcast">
</form>
フォームのsubmitにより以下のコードで定義されたハンドラが実行されます。
それぞれmy event
イベントとmy broadcast event
を
フォームに入力されたデータを持たせて発火させています。
$('form#emit').submit(function(event) {
socket.emit('my event', {data: $('#emit_data').val()});
return false;
});
$('form#broadcast').submit(function(event) {
socket.emit('my broadcast event', {data: $('#broadcast_data').val()});
return false;
});
結果としてPython側の以下のコードが実行されて
my response
イベントを発火させることにより、結果を返しています。
ほとんど同じコードが並んでいますが、my broadcast event
の場合は
broadcast=True
をemit
のキーワード引数として置くことで
メッセージをすべてのクライアントに送るように指定しています。
@socketio.on('my event', namespace='/test')
def test_message(message):
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my response',
{'data': message['data'], 'count': session['receive_count']})
@socketio.on('my broadcast event', namespace='/test')
def test_broadcast_message(message):
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my response',
{'data': message['data'], 'count': session['receive_count']},
broadcast=True)
ping/pongの処理
'Average ping/pong latency'の部分には
サーバとの通信のやりとりにどれだけレイテンシがあるかが表示されています。
以下のコードでは1秒ごとに通信の開始時間を記録し
my ping
イベントを発火させています。
ping_pong_times
変数は過去の通信を格納するための配列です。
var ping_pong_times = [];
var start_time;
window.setInterval(function() {
start_time = (new Date).getTime();
socket.emit('my ping');
}, 1000);
Python側はmy ping
イベントの発火を受けて、my pong
を発火させるのみです。
@socketio.on('my ping', namespace='/test')
def ping_pong():
emit('my pong')
JavaScript側ではmy pong
が発火した時間と開始時間の差を取り
過去の通信記録を平均したものを'Average ping/pong latency'に表示しています。
socket.on('my pong', function() {
var latency = (new Date).getTime() - start_time;
ping_pong_times.push(latency);
ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples
var sum = 0;
for (var i = 0; i < ping_pong_times.length; i++)
sum += ping_pong_times[i];
$('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10);
});
サーバ側でのイベント生成処理
ここまでの処理はすべてクライアント側(JavaScript)が起点になっていましたが
アプリケーションにおいて、サーバ側から情報をプッシュしたい場合もあります。
サンプルコードでは、connect
イベントのハンドラに以下の記述がありました。
thread = socketio.start_background_task(target=background_thread)
target
となっているbackground_thread
は以下のように定義されています。
def background_thread():
"""Example of how to send server generated events to clients."""
count = 0
while True:
socketio.sleep(10)
count += 1
socketio.emit('my response',
{'data': 'Server generated event', 'count': count},
namespace='/test')
10秒ごとにmy response
イベントを発火させていることがわかります。
タイムラインの自動更新や、美人時計のようなアプリを実装する際に有用そうです。
むすび
サンプルコードを落として読んだだけですが、ためになりました。
これを少し改変するだけでも色々作れそうですね。
これからソースコード全体の読み込みは必要になるものの
エクステンションを含めても小さめで、とっつきやすいのはFlaskの魅力だと思います。