2022-11-17
Posted by
今回はFlask-SocketIOを使い実装していきます
例に出てくるclientはnext.jsでsocket.io-clientを使用していますがその他でもかまいません
準備
いつもの requirements.txt
Flask==2.1.3
Flask-Cors==3.0.10
Flask-JWT-Extended==4.4.2
Flask-SQLAlchemy==2.5.1
Flask-SocketIO==5.3.1
今回は以下のプログラムを土台に作って行きます
import os, json, random
from flask_cors import CORS, cross_origin
from flask import Flask,jsonify,request, session
from flask_socketio import SocketIO, emit, join_room, \
leave_room, close_room, rooms, disconnect, ConnectionRefusedError
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY = "secret"
,JSON_AS_ASCII = False
)
socketIo = SocketIO(app, cors_allowed_origins="*")
cors = CORS(app, responses={r"/*": {"origins": "*"}})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
基本
イベント
基本的にsocketioは普通の@app.route
と同じように@socketIo.event
などのデコレーターを使って行きます
基本的には以下の二つのデコレーターを使って行きますが、以下の二つは同じ動作をします
@socketIo.on("hello_world")
def hoge():
emit('hello_world', "hello, world!", broadcast=True)
@socketIo.event
def hello_world():
emit('hello_world', "hello, world!", broadcast=True)
接続イベント
また接続イベントもあります
接続イベントはクライアントが接続や切断した場合に実行されます
接続イベントではFalseやConnectionRefusedErrorを返す事でクライアントの接続を拒否することも可能です
後述しますがそれを使えば認証なども可能です
@socketIo.on('connect')
def connect():
import random
if random.random() < 0.5:
raise ConnectionRefusedError('unlucky')
print("connect...")
@socketIo.on('disconnect')
def disconnect():
print("disconnect...")
ルーム
ルームはクライアントをルームと言う箱に入れて一括送信などができます
チャットルームみたいなものです
ルームはサーバー側で操作できます
@socketIo.event
def join(message):
join_room("hello_room")
@socketIo.event
def leave(message):
leave_room("hello_room")
また、ルームは個別に送信する一番(多分)簡単な方法です
接続時にクライアントを一人ひとり違うルームに入れる事で簡単に管理出来ます
送信
送信はemitを使って行います
emit("event_name", {"msg":"hello"})
toを指定する事で指定のルームに送信することが可能です
emit("event_name", {"msg": "hello"}, to="hello_room")
ですが残念ながら複数のルームに送信する機能は付いていません、ですが送信する事は出来ます
ただこの方法だと複数のルームに参加しているクライアントには複数回メッセージが送信されてしまいます
回避策としては一人ひとりを違うルームに入れる事です
rooms = ["hello_room", "world_room"]
for room in rooms:
emit("event_name", {"msg": "hello"}, to=room)
まとめ
今まで紹介してきた機能をまとめると次のようなプログラムを書けます
import os, json, random
from flask_cors import CORS, cross_origin
from flask import Flask,jsonify,request, session
from flask_socketio import SocketIO, emit, join_room, \
leave_room, close_room, rooms, disconnect, ConnectionRefusedError
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY = "secret"
,JSON_AS_ASCII = False
)
socketIo = SocketIO(app, cors_allowed_origins="*")
cors = CORS(app, responses={r"/*": {"origins": "*"}})
@socketIo.on('connect')
def connect():
print("connect...")
@socketIo.on('disconnect')
def disconnect():
print("disconnect...")
@socketIo.event
def join(message):
join_room(str(message["room"]))
@socketIo.event
def leave(message):
leave_room(str(message["room"]))
@socketIo.event
def send_all(message):
emit("from_all", {"msg": message["msg"]})
@socketIo.event
def send_room(message):
emit("from_room", {"msg": message["msg"]}, to=str(message["to"]))
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
クライアント
今回はnext.jsでsocket.io-clientを使用していますがその他でもかまいません
今回使うライブラリ
npm i socket.io-client
接続
const socket = io("ws://127.0.0.1:5000")
イベントの受信
useEffect(() => {
socket.on("from_all", (msg: any) => {
console.log(msg)
})
socket.on("from_room", (msg: any) => {
console.log(msg)
})
},[])
イベントの送信
const join = (room: string) => {
socket.emit("join", {room:room})
}
const leave = (room: string) => {
socket.emit("leave", {room:room})
}
const send_room = (room: string, msg: string) => {
socket.emit("send_room", {msg: msg, to: room})
}
const send_all = (msg: string) => {
socket.emit("send_all", {msg: msg})
}
クライアントのまとめ
info
デフォルトのindex.tsxを書き換えています
npx create-next-app@latest --ts で作られるやつ
import { useEffect } from 'react'
import { io } from 'socket.io-client'
import styles from '../styles/Home.module.css'
const socket = io("ws://127.0.0.1:5000")
export default function Home() {
const join = (room: string) => {
socket.emit("join", {room:room})
}
const leave = (room: string) => {
socket.emit("leave", {room:room})
}
const send_room = (room: string, msg: string) => {
socket.emit("send_room", {msg: msg, to: room})
}
const send_all = (msg: string) => {
socket.emit("send_all", {msg: msg})
}
useEffect(() => {
socket.on("from_all", (msg: any) => {
console.log(msg)
})
socket.on("from_room", (msg: any) => {
console.log(msg)
})
},[])
return (
<div className={styles.container}>
<main className={styles.main}>
<div className={styles.grid}>
<div onClick={() => {
join("test")
}} className={styles.card}>
<p>join</p>
</div>
<div onClick={() => {
leave("test")
}} className={styles.card}>
<p>leave</p>
</div>
<div onClick={() => {
send_all(String(Math.random()).slice(2,12))
}} className={styles.card}>
<p>all</p>
</div>
<div onClick={() => {
send_room("test", String(Math.random()).slice(2,12))
}} className={styles.card}>
<p>room</p>
</div>
</div>
</main>
</div>
)
}
どうですか?
もしかするとイベントが二回実行されたかもしれませんがそれはuseEffectの問題です
以下のプログラムは一見すると"hello"と一回出力するプログラムに思えますが2回実行されます
useEffect(() => {
console.log("hello")
},[])
何が言いたいかと言うとuseEffectが複数回実行される事によって、その中に入っているsocket.on
も複数回実行されているのです
それを修正する為にはnext.config.jsを変更する必要があります
具体的には以下のように変更してください
2
3
4
-
5
+
6
7
8
9
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
reactStrictMode: false,
swcMinify: true,
}
module.exports = nextConfig
reactStrictModeをfalseにすることによって修正出来ます
ですがデメリットも多少あります
詳しくは以下のドキュメントを参照してください
認証
socket ioに認証を付けるのも比較的簡単に実装出来ます
今回はjwtを用いて認証します
ライブラリはFlask-JWT-Extendedを使います
まずクライアントを接続時にトークンを送信するようにします
const socket = io("ws://127.0.0.1:5000", {
query : {
token: "jwt token"
}
})
次にサーバー側です
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,JWTManager \
,get_jwt,get_jwt_identity,current_user,create_refresh_token
from flask_jwt_extended import decode_token
def token_auth(jwt):
try:
if not decode_token(jwt):
return False
except:
return False
return True
def get_token_data(jwt):
if not token_auth(jwt):
return False
data = decode_token(jwt)
return data
@socketIo.on('connect')
def connect():
token = request.args.get('token')
if not token_auth(token):
raise ConnectionRefusedError("Token not found")
join_room(str(get_token_data(token)["sub"]))
@socketIo.event
def request_token(message):
if not token_auth(request.args.get('token')):
raise RuntimeError("Token not found")
今回は接続時にユーザーの部屋に入れています
そうする事でユーザーのsub(idやname)を指定する事で簡単に個別に送信出来ます
info
join_roomやemitのtoは型がstringではないとダメなようです
emit("test",{"msg":"hello"}, to=str(user.id))
このドキュメントどう?