konchangakita

KPSを一番楽しんでいたブログ 会社の看板を背負いません 転載はご自由にどうぞ

FastAPI で SocketIO


Socket.IOは、リアルタイム双方向通信を実現するJavaScriptライブラリなのですが、FlaskとFastAPIでは実装がまた別でここも作り直しになります

時間のない人向けの最終形態は github
github.com

最小の実装

バージョン情報

バックエンドの requirement.txt
Pythonバージョン: 3.11

fastapi==0.104.1
uvicorn[standard]==0.24.0
python-socketio==4.3.1
python-engineio==3.9.0
python-multipart==0.0.6

フロントエンドの package.json
Node.jsバージョン: 18

{
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "socket.io-client": "^4.7.2"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@types/socket.io-client": "^1.4.36",
    "typescript": "^5.0.0",
    "eslint": "^8.0.0",
    "eslint-config-next": "^14.0.0"
  }
}


FastAPIでの実装のとっかかり

インスタンスの作り方からして違うのね

from fastapi import FastAPI
import socketio

sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
sio_app = socketio.ASGIApp(sio)

app = FastAPI()
app.mount("/socket.io", sio_app)  # クライアントは /socket.io に接続

@sio.event
async def connect(sid, environ):
    await sio.emit("welcome", {"msg": "hello"}, to=sid)

if __name__ == "__main__":
    uvicorn.run(socket_app, host="0.0.0.0", port=8000)


SocketIO接続テスト環境の実装

最小限のテスト環境として、フロントエンドとバックエンドでSocketIOで接続と切断だけを行う簡単なテスト

バックエンド側の実装

main.py

'use client'

import React, { useState } from 'react'
import io from 'socket.io-client'

type Socket = ReturnType<typeof io>

export default function Home() {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [connected, setConnected] = useState(false)

  const connectSocket = () => {
    if (socket && connected) {
      console.log('既に接続されています')
      return
    }

    console.log('SocketIO接続を開始します...')
    const newSocket = io(`${window.location.protocol}//${window.location.hostname}:8000`)
    
    newSocket.on('connect', () => {
      console.log('サーバーに接続しました')
      setConnected(true)
    })

    newSocket.on('disconnect', () => {
      console.log('サーバーから切断されました')
      setConnected(false)
    })

    newSocket.on('connect_error', (error: any) => {
      console.error('接続エラー:', error)
      setConnected(false)
    })

    setSocket(newSocket)
    console.log('SocketIOオブジェクトを設定しました')
  }

  const disconnectSocket = () => {
    if (socket) {
      socket.close()
      setSocket(null)
      setConnected(false)
      console.log('手動で切断しました')
    }
  }

  return (
    <div className="container">
      <h1>SocketIO テスト環境 (Next.js App Router)</h1>
      
      <div className="card">
        <h2>接続状態</h2>
        <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? '接続中' : '切断中'}
        </div>
        <div style={{ marginTop: '10px' }}>
          <button 
            onClick={connectSocket} 
            className="button connect" 
            disabled={connected}
          >
            接続
          </button>
          <button 
            onClick={disconnectSocket} 
            className="button disconnect" 
            disabled={!connected}
          >
            切断
          </button>
        </div>
      </div>
    </div>
  )
}
フロントエンド

page.tsx

'use client'

import React, { useState } from 'react'
import io from 'socket.io-client'

type Socket = ReturnType<typeof io>

export default function Home() {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [connected, setConnected] = useState(false)

  const connectSocket = () => {
    if (socket && connected) {
      console.log('既に接続されています')
      return
    }

    console.log('SocketIO接続を開始します...')
    const newSocket = io(`${window.location.protocol}//${window.location.hostname}:8000`)
    
    // 接続成功時の処理
    newSocket.on('connect', () => {
      console.log('サーバーに接続しました')
      setConnected(true)
    })

    // 切断時の処理
    newSocket.on('disconnect', () => {
      console.log('サーバーから切断されました')
      setConnected(false)
    })

    // エラー時の処理
    newSocket.on('connect_error', (error: any) => {
      console.error('接続エラー:', error)
    })

    setSocket(newSocket)
    console.log('SocketIOオブジェクトを設定しました')
  }

  const disconnectSocket = () => {
    if (socket) {
      socket.close()
      setSocket(null)
      setConnected(false)
      console.log('手動で切断しました')
    }
  }

  return (
    <div className="container">
      <h1>SocketIO チャットルーム (Next.js App Router)</h1>
      
      <div className="card">
        <h2>接続状態</h2>
        <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? '接続中' : '切断中'}
        </div>
        <div style={{ marginTop: '10px' }}>
          <button 
            onClick={connectSocket} 
            className="button connect" 
            disabled={connected}
          >
            接続
          </button>
          <button 
            onClick={disconnectSocket} 
            className="button disconnect" 
            disabled={!connected}
          >
            切断
          </button>
        </div>
      </div>

    </div>
  )
}


接続テスト

ブラウザでhttp://localhost:3000へ接続

「接続」ボタンをクリックし、状態が「接続中」に変わることを確認

「切断」ボタンをクリックし、状態が「切断中」に変わることを確認

socketio バージョンによって、テストがうまくいかないことがあるので
うまくいかない時は、フロントエンドとバックエンドのバージョンを変えてテストを色々試してみるとよいです
今回の構成では、python-socketio 5.13.0(Engine.IO 4.xプロトコル)とsocket.io-client 4.7.2(Engine.IO 4.xプロトコル)を使用しており、互換性がある


チャットルーム実装

SocketIOでは双方向通信できるので、クライアント同士を「部屋(room)」に参加させて、同じ部屋にいる人だけにメッセージを届けるちょっとしたチャットルームを作ることができます

チャットルーム基本の流れ

1.クライアントが接続する
接続時にユーザーを特定の「room」に参加させる

2.メッセージを受信する
受け取ったメッセージを「同じroom」にいる人だけにブロードキャストする

3.退出処理する
切断時や退出時にsocketIO切断

実装は

実装のステップバイステップはこちらにまとめてあるので参照ください
github.com


おわりに

まだFastAPIによりパフォーマンス向上の実感はないですが、バックエンドをリファクタリングしつつ
テスト用プログラムも実装していきたい。。。
Kubernetes化への道のりは、まだもう少し時間がかかりそう