הדגמת פיתוח תקשורת דו-כיוונית בין שרת פייתון ללקוח ריאקט
ספריית socket.io היא עדיין הדרך הכי פשוטה לבנות חיבור תקשורת אסינכרוני בין קוד צד לקוח לקוד צד שרת. בדוגמה היום אני לוקח שרת פייתון ולקוח ריאקט כדי לבנות תיבת טקסט שמראה את אותו הטקסט בין כמה חלונות - כלומר אנחנו משנים את הטקסט בחלון אחד ואוטומטית בחלונות אחרים ובמחשבים אחרים הטקסט בתיבה משתנה בהתאמה.
1. צד השרת: פייתון ו Socket IO
הספריה python-socketio מספקת מימוש נוח לפרוטוקול SocketIO בצד שרת בפייתון. זו ספריית מעטפת שיכולה להשתמש במספר ספריות ניהול אירועים בשביל התקשורת, ואני הלכתי על מימוש ב aiohttp.
החלק המעניין בקוד צד השרת הוא בלוקים מהצורה הזו:
@sio.on('message')
async def print_message(sid, message):
print("Socket ID: " , sid)
print(message)
await sio.emit('message', message, broadcast=True);
כל מתודה שמסומנת עם ה Decorator של SocketIO תהיה מתודה שמאזינה לאירוע. בקוד הדוגמה מאזינים לאירוע בשם message. הפונקציה emit שולחת אירועים החוצה והפרמטר broadcast גורם לפרמטר להישלח לכל הלקוחות. זה נוח לתיבת טקסט שצריכה להיות מסונכרנת בין כמה מכונות כדי שכל שינוי במכונה אחת אוטומטית יישלח לכל המכונות האחרות.
קוד השרת המלא הוא בסך הכל קובץ פייתון אחד עם התוכן הבא:
from aiohttp import web
import socketio
static_files = {
'/static': './frontend/dist',
}
sio = socketio.AsyncServer(cors_allowed_origins='*', aync_mode='aiohttp')
app = web.Application()
app.add_routes([web.static('/static', './public')])
sio.attach(app)
@sio.on('message')
async def print_message(sid, message):
print("Socket ID: " , sid)
print(message)
await sio.emit('message', message, broadcast=True);
if __name__ == '__main__':
web.run_app(app)
אני הוספתי CORS Headers כדי שיהיה נוח במצב פיתוח להפעיל שרת Webpack ולהתחבר ממנו לשרת ה SocketIO (כל אחד מהם רץ על פורט אחר). במצב ייצור אפשר לוותר על זה ולהגיש את הקבצים מאותו דומיין דרך nginx או באמצעות מנגנון ה Static Files של שרת aiohttp.
2. צד הלקוח: ריאקט ו socket.io-react-hook
בצד הלקוח הקוד מחולק לשני קבצים: בקובץ ה index.js או main.js (תלוי בתבנית הפרויקט שלכם) אני צריך להוסיף אלמנט בשם IoProvider מסביב לכל האפליקציה שלי:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { IoProvider } from 'socket.io-react-hook';
ReactDOM.render(
<React.StrictMode>
<IoProvider>
<App />
</IoProvider>
</React.StrictMode>,
document.getElementById('root')
)
והחלק המעניין הוא הקומפוננטה עצמה: שם אני משתמש ב Hooks הבאים:
הפונקציה useSocket מתחברת לשרת Socket IO מרוחק.
הפונקציה useSocketEvent מתחברת לאירוע שהשרת יכול לשלוח, וכל פעם שהשרת שולח את האירוע אוטומטית תגרום ל Render מחדש של הקומפוננטה.
בעזרת useEffect אני מחבר בין האירוע שחוזר מ useSocketEvent לטקסט שמוצג: כל פעם שקיבלנו הודעה מהשרת נבדוק אם היא שונה מהטקסט ששמור אצלנו נציג אותה בעזרת שמירתה למשתנה ה State.
הקוד המלא בקובץ App.jsx נראה כך:
import './App.css'
import { useState, useEffect } from 'react';
import { useSocket, useSocketEvent } from 'socket.io-react-hook';
function App() {
const [text, setText] = useState('');
const { socket, error } = useSocket('http://localhost:8080');
const { lastMessage } = useSocketEvent(socket, 'message');
useEffect(function() {
if (lastMessage && lastMessage !== text) {
setText(lastMessage);
}
}, [lastMessage]);
if (error) {
return <p>{String(error)}</p>
}
function handleChange(e) {
setText(e.target.value);
socket.emit('message', e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>Last message = {lastMessage}</p>
</div>
);
}
export default App
3. איך מפעילים
אם אתם רוצים לשחק עם הפרויקט תשמחו לשמוע ששמתי את כל הקוד בגיטהאב במאגר https://github.com/ynonp/python-socketio-demo.
בשביל להפעיל אותו צריך להריץ קודם כל את ההתקנות בפייתון עם:
$ pip install -r requirements.txt
ואז להפעיל את שרת הפייתון עם:
$ python main.py
ולבסוף להפעיל את שרת הפיתוח לצד לקוח עם:
$ cd frontend
$ yarn dev
נסו להיכנס ל localhost:3000
ממספר חלונות ולכתוב טקסט בתיבה. אם הכל עובד כמו שצריך אתם תראו את הטקסט מועתק לכל החלונות האחרים.