ブルームテクノロジー

テクノロジー

Python×FlaskでSocket通信をする

Python×FlaskでSocket通信をする

Python×FlaskでSocket通信をする

WEBアプリケーションを開発していると、リアルタイムでの情報のやり取りができる仕様で機能実装したくなることがあります。ビデオ通話やチャットやゲームなど、ユーザ間でのデータのやりとりを双方向間でできる魅力は大きいと感じます。この双方向間の通信を実現するために、今回はSocket通信の紹介とコード解説をします。

今回は、比較的環境構築がしやすく、ライブラリが充実しているPythonとそのフレームワークのFlaskを利用して実装します。また、フロントエンドにJavaScriptの外部ライブラリsocketio.jsを利用しています。

Socket通信とは

Socket通信の定義

インターネットではTCP/IP通信プロトコルを利用します。このTCP/IPをプログラムから利用するための出入り口のことをSocketと呼びます。このSocketを利用した通信のことを「Socket通信」とよびます。また、Socket通信のことを単にTCP/IP通信とも呼びます。

Socket通信の用途

PC間の通信をインターネット経由で行いたい場合によく利用されます。以下はSocket通信を拡張した通信プロトコルで、一般的なものを紹介いたします。

ポート番号プロトコル用途
21ftpファイル転送
25smtp電子メール送信
80httpブラウザによるホームページアクセス

環境構築

動作環境

 Windows10,64bit
 Python 3.8.10
 Flask 2.2.3
 Flask-SocketIO 5.3.3
 soketio.js 4系

①Pythonのインストール

python3.8.10のインストーラを下記サイトからダウンロードしてインストールします。
https://www.python.org/downloads/release/python-3810/

任意のフォルダ(例はsample)を作成し、そこにコマンドプロンプトで移動します。

cd C:\xxx\sample

続いて、pythonの仮想環境を作成して起動します。

②仮想環境の初期化

py -m venv –-clear .venv

③仮想環境の起動

.venv\Scripts\activate

④インストールコマンドをアップグレード

py -m pip install --upgrade pip

⑤Flaskのインストール

PythonのWEBアプリケーション開発用フレームワークをインストールします。

pip install Flask==2.2.3

⑥Flask-SocketIOのインストール

pip install Flask-SocketIO==5.3.3

Socket通信を利用したチャットアプリケーション例

簡単なチャットアプリケーションを実装しました。

ユーザ名とルームIDを入力して、「参加」ボタンを押下します。これで、チャットルームに参加したことになります。画面下部にあるチャットフォームで文字列を入力して、「送信」ボタンを押下すると、同じチャットルームに参加しているユーザにメッセージが送信されます。送信されたメッセージは画面中央に追加される仕様です。

以降は、例のようなSocket通信をするための基本的な解説となります。

Flaskについて

Flaskの基本的な機能紹介

Flaskは簡易サーバを手軽に起動できます。Flaskインスタンスを生成し、run()メソッドを実行すると、サーバが起動します。

#Flaskインスタンス生成
app = Flask(__name__)
app.config["SECRET_KEY"] = "f4Pjp3UgJa51"

※Socket通信を行わない場合、以下のようにrun()メソッドで起動します。しかし、今回はこの方法では起動しません。

app.run(host="0.0.0.0", port="8088", threaded=True)

Flaskはリクエストのルーティング処理をデコレータ(@)で実装します。

#例:URL(/)に対しリクエスト種別(GET,POST)の受付を関数room_html()に付与する
#チャットページを表示
@app.route('/', methods=['GET', 'POST'])
def room_html():
   return render_template('room.html')

上記コードのrender_template()はFlaskが提供する動的HTML生成用の関数です。room.html内にJinja2記法で記述することで、サーバからのデータをフロントエンドに反映できます。

Jinja2の書き方

FlaskはデフォルトでJinja2をテンプレートエンジンとしています。Jinja2の基本的な文法は以下のようになります。

if,forなどの制御文:{% 制御ブロック %}

変数の値や関数を実行する場合:{{ 参照する変数 もしくは 処理 }}

例:配列(arr)から値(a)を取り出して、全て列挙する

{% for a in arr %}
    {{ a }}
{% endfor %}

例:別HTML(base.html)を継承する

{% extends "base.html" %}

例:継承したHTML内のscriptと命名されたブロックを上書きする

{% block script %}
    <script src="{{url_for('static',filename='js/x.js')}}"></script>
{% endblock %}

上記の例で{{}}内で参照しているurl_for()メソッドはFlaskサーバのドキュメントルートなどを自動で保管してURLを生成するものです。適宜room.htmlを編集しておきます。

相互間の通信準備

Socket通信の基本は、クライアントとサーバ間でコネクションを確立し、互いに相手のコールバック関数を呼び出すことです。今回はクライアント(socketio.js)とサーバ(Flask-SocketIO)間での通信を確立するところからはじめます。

Flask-SocketIOの基本的な機能紹介

Flask-SocketIOはFlaskの拡張ライブラリです。Flaskライブラリは慣例として、Flaskインスタンス(今回はapp=Flask(__name__))を基に、拡張ライブラリの(今回はFlask-SocketIO)インスタンスを生成する仕様となっています。

#Flask-SocketIOインスタンス生成(init_app(app)の場合もある)
socketio = SocketIO(app)

サーバ起動は以下のようにrun()を呼び出し、コマンドプロンプトから実行します。

if __name__ == "__main__":
    #Flask-SocketIOのサーバ起動
    socketio.run(app,host="0.0.0.0", port="8088")
py pythonファイル名.py

Flask-SocketIOはコールバック関数の管理をデコレータで管理します。デコレータでは、クライアントサイドから要求されるコールバック関数名を付与します。Flask-SocketIOとsocketio.js間ではクライアントとのコネクション確立にコールバック関数名「connect」を使います。

#クライアントとのコネクション確立
@socketio.on('connect')
def handle_connect():
    emit('client_echo',{'msg': 'server connected!'})


#クライアントからのメッセージを出力する関数
@socketio.on('server_echo')
def handle_server_echo(msg):
    print('echo: ' + str(msg))

上記のコードはまず、クライアントとのコネクションが確立されるとhandle_connect()が実行され、その内部でemit()が実行されます。emit()はFlask-SocketIOが提供するメソッドで、クライアントのコールバック関数を指定し、そのコールバック関数に第二引数のデータを渡すことができます。今回は「clinet_echo」というコールバック関数を呼び出し、その関数に{‘msg’:’server connected’}という辞書型データを渡しています。

socketio.jsの書き方

socketio.jsはio()関数が返すオブジェクトに対し、on関数でコールバック関数の管理をします。Flask-SocketIOと異なり、デコレータではなく関数による登録となります。

socketio.jsはコードが実行された段階で、サーバとのコネクションを確立し、「connect」というイベント名に登録された関数を実行します。また、接続先のIPを指定しない場合、socketio.jsは表示中ページのサーバに対して接続します。つまり、Flaskサーバと自動接続します。

※socketio.jsをHTML内で取得するようにします。

<!-- socketio.js -->
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
#socketオブジェクト生成
let socket = io();
#サーバとのコネクション確立
socket.on('connect', function() {
    socket.emit('server_echo', {data: 'client connected!'});
});


#サーバからのメッセージを出力する関数
socket.on('client_echo', function(data) {
    console.log("echo" + ': ' + data.msg);
});

上記のコードでは、まず、クライアント側でconnectイベントが発火した際に、socketオブジェクトのemit()を実行しています。このemit()は、サーバ側のコールバック関数を指定し、第二引数のデータをそのコールバック関数に渡すことができます。今回はサーバ側の「server_echo」に「{data:’client connected!’}」というデータを渡しています。

また、クライアント側に「client_echo」と紐づけたコールバック関数を定義しています。これにより、サーバがコールバック関数として「client_echo」を指定した場合に発火し、送られたdataからmsgを取り出して、コンソールログに出力するという処理が実行されます。

以上が、基本的なクライアントとサーバの通信となります。

チャット機能について

前項で簡単なテキストベースのデータのやり取りについて、解説しました。これを踏まえて、クライアント間でのチャット機能を解説します。

簡単なチャット機能(リアルタイム更新)実装

チャット機能の基本シーケンスはクライアント→サーバ→クライアントとなります。また、サーバからクライアントにデータを送信する際に、限られたクライアントにのみ送信したい場合がほとんどです。Flask-SocketIOは「room」という概念を採用しているため、非常に簡単にデータ送信制御が実現できます。

#ルーム作成
@socketio.on('join')
def join(msg):
    join_room(str(msg["room"]))

#ルームに参加しているクライアントへ送信
@socketio.on('send_room')
def send_room(data):
    print(data)
    #ここはクライアントから送られてくるデータを踏まえて、調整するべき部分
    emit("from_room",{"msg":data["chat_info"]["user_name"] + ":" + data["chat_info"]["msg"]},to=str(data["to"]))

上記コードでは、「join」というコールバック関数内で、join_room()が実行されます。join_room()はFlask-SocketIOが提供する関数で、クライアントを個別のルームごとに振り分ける関数です。join_room()に渡している値はルームIDです。これにより、ルームごとにデータの送信制御が可能になります。また、「send_room」というコールバック関数では、emit()でルームを指定して、送信をしています。emit()の引数toにはルームIDを渡しています。

クライアントはある特定のルームに参加し、ルームに参加しているクライアントに対し、「send_room」コールバック関数でデータを送信することで、テキストチャットをすることとなります。このとき、クライアント側のコールバック関数は「from_room」としています。

続いて、クライアント側にサーバ側のコールバック関数を呼ぶ処理を追加します。先ほど用意したコールバック関数(「join」「send_room」)を呼び出す関数2つと「from_room」に紐づけたコールバック関数の1つを実装します。

//コールバック関数(join)を呼ぶための関数
const join  = (room) => {socket.emit("join" ,{room:room})};
//コールバック関数(send_room)を呼ぶための関数
const send_room = (room,chat_info) => {
    socket.emit("send_room",{chat_info:chat_info,to:room})
};


//サーバからルームへデータが送られてきたときのコールバック関数
//ここをアプリケーション仕様に合わせて改修すれば、リッチなアプリになる
socket.on('from_room', function(data) {
    console.log("from_room" + ': ' + data.msg);
    //例1:取得したメッセージをHTMLに表示する。
    //例2:Audioクラスで通知音を出す。アニメーションをつける。など
});

join関数でクライアントはルームに参加し、send_room関数でルーム全体にチャットメッセージを送っています。send_room関数でサーバの「send_room」コールバック関数が呼び出されたあと、サーバからクライアントの「from_room」コールバック関数が呼び出され、コンソールログにメッセージが出力されます。join関数とsend_room関数を呼び出しさえすれば、ルームチャットの準備完了です。

あとは、このやりとりを、チャットアプリケーションのようなUIに落とし込んでいくことで実装完了です。この部分は今回は省略します。

まとめ

特徴

  • Socket通信は、双方向でのリアルタイム更新処理が実装できる
  • HTTP通信と異なり、クライアントとサーバの双方向の通信用受け口が必要となる

メリット、デメリット

メリット:

  • チャットアプリなどのテキストのやり取りに向いている
  • HTTP通信よりも、自由度の高い実装が可能(映像配信やビデオ通話など)
  • リアルタイムでの更新が必須のアプリケーションに有効(コミュニケーションツール、ゲーム、株価変動、サーバの死活監視など)

デメリット:

  • 安定した通信を確保しにくい(端末によってSocketが異なるため)
  • 常時接続を行う場合は、通信量が多くなりがち
  • クライアントとサーバで互いの仕様に依存し合うため、複雑な仕様になりがち

以上が、Socket通信の簡単な実装となります。今回はユーザからサーバ、サーバからユーザの通信を構築しました。このほかにもWebRTCというユーザからユーザの通信に特化したものもあります。日本語の記事は少ないですが、興味がある方は、そちらも調べてみると楽しめるかと思います。

>>エンジニア採用情報<<