2022-11-17

flask

python

Posted by

applemango

今回は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))

このドキュメントどう?

emoji
emoji
emoji
emoji