حول المحتوى:
بناء تطبيق تفاعلي يستخدم WebSockets في FastAPI: الاتصال ثنائي الاتجاه، إدارة الغرف، التعامل مع اتصالات متعددة، ومشاكل الأداء والحلول.
في الكثير من تطبيقات الويب الحديثة، لم يعد الطلب/الاستجابة التقليدي (Request/Response) كافيًا لتقديم تجربة تفاعلية وسريعة. هنا يأتي دور WebSockets مع FastAPI WebSockets لتوفير اتصال ثنائي الاتجاه بين العميل (المتصفح أو تطبيق الموبايل) والخادم في الوقت الحقيقي.
في هذا الشرح من افهم صح سنبني تطبيقًا عمليًا بسيطًا يعتمد على FastAPI WebSockets، ونتعرف على:
إذا كنت جديدًا على FastAPI، يمكنك مراجعة: بناء أول موقع باستخدام FastAPI و بناء RESTful APIs باستخدام FastAPI لتكوين صورة أوضح عن أساسيات الإطار.
ببساطة، WebSocket هو بروتوكول يسمح بإنشاء اتصال مستمر بين العميل والخادم، بحيث يمكن لأي منهما إرسال بيانات في أي وقت بدون الحاجة لإعادة إنشاء اتصال جديد مع كل رسالة، بعكس HTTP التقليدي.
أمثلة على استخدام WebSockets:
هنا يبرز دور FastAPI WebSockets
نفترض أن بيئة بايثون جاهزة لديك. سنبدأ ببناء مشروع بسيط يدعم WebSockets.
نثبّت FastAPI مع Uvicorn (سيرفر ASGI):
pip install fastapi "uvicorn[standard]"
بعد التثبيت يمكنك إنشاء ملف مثلًا: main.py.
هيكل أولي للتطبيق:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "مرحبًا بك في تطبيق WebSockets باستخدام FastAPI"}
يمكنك تشغيل التطبيق:
uvicorn main:app --reload
الآن ننتقل إلى إضافة دعم FastAPI WebSockets بشكل عملي.
لدعم WebSockets في FastAPI، نستخدم WebSocket و WebSocketDisconnect من الحزمة fastapi.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"استقبلت: {data}")
except WebSocketDisconnect:
print("تم قطع اتصال العميل")
الشرح:
@app.websocket("/ws"): يعرّف مسار WebSocket على /ws.await websocket.accept(): قبول الاتصال من العميل.receive_text: استقبال رسالة نصية من العميل.send_text: إرسال رد نصي للعميل.WebSocketDisconnect: استثناء يتم رفعه عند قطع الاتصال من جهة العميل.لنضيف صفحة HTML صغيرة لاختبار WebSocket:
from fastapi.responses import HTMLResponse
html = """
<!DOCTYPE html>
<html>
<head>
<title>اختبار FastAPI WebSockets</title>
</head>
<body>
<h1>WebSocket Echo</h1>
<input id="messageText" type="text" placeholder="اكتب رسالة" />
<button onclick="sendMessage()">إرسال</button>
<ul id="messages"></ul>
<script>
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = function(event) {
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.innerText = event.data;
messages.appendChild(li);
}
function sendMessage() {
const input = document.getElementById("messageText");
ws.send(input.value);
input.value = "";
}
</script>
</body>
</html>
"""
@app.get("/test", response_class=HTMLResponse)
async def get():
return html
الآن بزيارة http://localhost:8000/test يمكنك كتابة رسالة، وسيقوم الخادم بإرجاعها (Echo) عبر WebSocket.
في التطبيقات الحقيقية، لا نتعامل مع اتصال واحد فقط، بل مع عدد كبير من المستخدمين المتصلين في نفس الوقت. نحتاج إلى آلية:
سنعرّف كلاس بسيط لإدارة الاتصالات:
from typing import List
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
هذا الكلاس يوفر لنا طريقة لإضافة اتصال جديد، إزالته، إرسال رسالة خاصة لمستخدم، وبث رسالة لجميع المتصلين.
@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"مستخدم قال: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast("مستخدم غادر الدردشة")
الآن أي مستخدم يتصل على /ws/chat سيتمكن من إرسال رسائل تصل للجميع عبر broadcast.
في كثير من الأحيان لا نريد أن يتلقى جميع المستخدمين كل الرسائل، بل نريد تقسيمهم إلى غرف مختلفة (مثل غرف الدردشة أو غرف الاجتماعات).
سنعدل ConnectionManager ليدعم غرفًا متعددة، بحيث نربط كل غرفة بقائمة اتصالات خاصة بها.
from typing import Dict, List
class RoomConnectionManager:
def __init__(self):
# room_id -> list of WebSockets
self.rooms: Dict[str, List[WebSocket]] = {}
async def connect(self, room: str, websocket: WebSocket):
await websocket.accept()
if room not in self.rooms:
self.rooms[room] = []
self.rooms[room].append(websocket)
def disconnect(self, room: str, websocket: WebSocket):
if room in self.rooms:
self.rooms[room].remove(websocket)
if not self.rooms[room]:
del self.rooms[room]
async def send_to_room(self, room: str, message: str):
if room in self.rooms:
for connection in self.rooms[room]:
await connection.send_text(message)
room_manager = RoomConnectionManager()
سنستخدم معرف الغرفة في مسار WebSocket:
@app.websocket("/ws/rooms/{room_id}")
async def websocket_room_endpoint(websocket: WebSocket, room_id: str):
await room_manager.connect(room_id, websocket)
await room_manager.send_to_room(room_id, f"مستخدم انضم إلى الغرفة {room_id}")
try:
while True:
data = await websocket.receive_text()
await room_manager.send_to_room(room_id, f"[{room_id}] قال: {data}")
except WebSocketDisconnect:
room_manager.disconnect(room_id, websocket)
await room_manager.send_to_room(room_id, f"مستخدم غادر الغرفة {room_id}")
بهذه الطريقة، عند الاتصال على ws://localhost:8000/ws/rooms/room1 سيتم توجيه الرسائل إلى أعضاء الغرفة room1 فقط.
استخدام WebSockets مع FastAPI يوفر تجربة رائعة، لكن توجد تحديات متعلقة بالأداء، خاصة مع عدد كبير من الاتصالات.
بما أن FastAPI مبني على async/await، يجب تجنّب أي عمليات متزامنة Blocking داخل دوال WebSocket (مثل الوصول لقاعدة بيانات بمكتبة متزامنة أو عمليات حسابية ثقيلة).
للتعمق في البرمجة غير المتزامنة يمكنك مراجعة: البرمجة غير المتزامنة في بايثون: تحسين الأداء باستخدام async و await.
حلول محتملة:
كل اتصال WebSocket يستهلك قدرًا من الذاكرة. عند آلاف الاتصالات قد يحدث ضغط على الخادم.
بعض النصائح:
تنفيذ حلقة for لإرسال الرسالة لكل اتصال في broadcast يمكن أن يصبح مكلفًا عند أعداد كبيرة من المستخدمين.
طرق تحسين:
asyncio.Queue) لمعالجة الرسائل بشكل متسلسل وسلس.عادة لا يُستخدم WebSocket بمفرده، بل يكون مكملًا لـ REST API موجود بالفعل. مثال على سيناريو متكامل:
هذا الدمج يسمح لك بالاستفادة من قوة REST في العمليات التقليدية (CRUD) وقوة FastAPI WebSockets في التحديثات اللحظية.
wss:// بدل ws:// مع HTTPS.type وpayload.{"type": "message", "room": "room1", "text": "مرحبا"}.لنلخّص ما سبق في مثال واحد مبسط يدمج إدارة الغرف مع الرسائل:
from typing import Dict, List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
app = FastAPI()
class RoomConnectionManager:
def __init__(self):
self.rooms: Dict[str, List[WebSocket]] = {}
async def connect(self, room: str, websocket: WebSocket):
await websocket.accept()
if room not in self.rooms:
self.rooms[room] = []
self.rooms[room].append(websocket)
def disconnect(self, room: str, websocket: WebSocket):
if room in self.rooms:
self.rooms[room].remove(websocket)
if not self.rooms[room]:
del self.rooms[room]
async def send_to_room(self, room: str, message: str):
if room in self.rooms:
for connection in self.rooms[room]:
await connection.send_text(message)
room_manager = RoomConnectionManager()
html = """
<!DOCTYPE html>
<html>
<head><title>غرف دردشة FastAPI WebSockets</title></head>
<body>
<h1>غرفة دردشة</h1>
<input id="room" placeholder="اسم الغرفة" />
<button onclick="joinRoom()">انضمام للغرفة</button>
<br/><br/>
<input id="messageText" type="text" placeholder="اكتب رسالة" />
<button onclick="sendMessage()">إرسال</button>
<ul id="messages"></ul>
<script>
let ws;
function joinRoom() {
const room = document.getElementById("room").value;
if (!room) return;
ws = new WebSocket(`ws://localhost:8000/ws/rooms/${room}`);
ws.onmessage = function(event) {
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.innerText = event.data;
messages.appendChild(li);
}
}
function sendMessage() {
if (!ws) return;
const input = document.getElementById("messageText");
ws.send(input.value);
input.value = "";
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def get():
return html
@app.websocket("/ws/rooms/{room_id}")
async def websocket_room_endpoint(websocket: WebSocket, room_id: str):
await room_manager.connect(room_id, websocket)
await room_manager.send_to_room(room_id, f"مستخدم انضم إلى الغرفة {room_id}")
try:
while True:
data = await websocket.receive_text()
await room_manager.send_to_room(room_id, f"[{room_id}] قال: {data}")
except WebSocketDisconnect:
room_manager.disconnect(room_id, websocket)
await room_manager.send_to_room(room_id, f"مستخدم غادر الغرفة {room_id}")
يمكنك تشغيل هذا المثال، فتح أكثر من تبويب في المتصفح، والانضمام إلى نفس الغرفة أو غرف مختلفة لمشاهدة كيفية عمل FastAPI WebSockets في تطبيق حي.
استخدام FastAPI WebSockets يفتح أمامك بابًا لبناء تطبيقات تفاعلية في الوقت الحقيقي، مثل الدردشة، التحديثات اللحظية، الألعاب، ولوحات التحكم. في هذا المقال طبقنا:
بعد فهم أساسيات WebSockets مع FastAPI، يمكنك الانتقال لمواضيع أوسع مثل توزيع الحمل، استخدام Redis Pub/Sub، أو مقارنة هذا النهج مع أطر أخرى مثل Django Channels، كما في شرح: بناء تطبيق دردشة باستخدام Django Channels.
بهذا تكون لديك قاعدة قوية للانطلاق في بناء أي تطبيق يعتمد على الاتصال ثنائي الاتجاه وتحديث البيانات في الوقت الحقيقي باستخدام FastAPI WebSockets.
بناء تطبيق تفاعلي يستخدم WebSockets في FastAPI: الاتصال ثنائي الاتجاه، إدارة الغرف، التعامل مع اتصالات متعددة، ومشاكل الأداء والحلول.
مساحة اعلانية