WebSocket — الاتصال ثنائي الاتجاه في الوقت الفعلي
HTTP مثل إرسال رسالة—يذهب في اتجاه واحد ويعود؛ WebSocket مثل إجراء مكالمة هاتفية—كلا الطرفين يمكنهما التحدث في أي وقت دون الحاجة للتعليق وإعادة الاتصال.
1. ما ستتعلمه
- أساسيات WebSocket: مُزخرفات
@app.websocketودورة الحياة (اتصال/استقبال/قطع اتصال) - نمط مدير الاتصال: تصميم فئة
ConnectionManagerوآلية البث - WebSocket مصادق: التحقق من رمز JWT خلال مرحلة المصافحة
- دمج مع نقاط نهاية HTTP: تغييرات الأسعار تؤدي إلى إشعارات دفع WebSocket
- سيناريو Alice: تغذية أسعار في الوقت الفعلي—عندما يتغير سعر منتج بين ملايين المنتجات، يتلقى المشتركون إشعار دفع فوريًا
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: تغييرات الأسعار يمكن استرجاعها فقط من خلال الاستقصاء
تستقصي واجهة Bob الأمامية واجهة PriceTracker API كل 5 ثوانٍ للتحقق من تغييرات الأسعار، لكن 99% من أسعار ملايين المنتجات تبقى بدون تغيير خلال أي فترة 5 ثوانٍ، مما يعني أن 99% من الطلبات مهملة. لأسوأ من ذلك، قد يستغرق عرض تغييرات الأسعار ما يصل إلى 5 ثوانٍ، مما يؤدي إلى شكاوى العملاء بأن الأسعار ليست في الوقت الفعلي.
(2) حل WebSocket
ينشئ WebSocket اتصالًا ثنائي الاتجاه دائمًا ويدفع تغييرات الأسعار بنشاط من الخادم إلى الواجهة الأمامية عند حدوثها، مما يلغي الحاجة للاستقصاء—صفر هدر، صفر تأخير.
@app.websocket("/ws/prices")
async def price_websocket(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_json({"price_update": data})
(3) العائد
انخفض عدد طلبات الاستقصاء من 200 في الثانية إلى 0، وانخفض تأخير تحديث الأسعار من 5 ثوانٍ إلى 50 مللي ثانية، ولم تعد واجهة Bob الأمامية تهدر حصص API، وتحسنت رضا العملاء بشكل كبير.
3. أساسيات WebSocket
(1) حالات دورة الحياة
stateDiagram-v2
[*] --> CONNECTING: العميل يبدأ
CONNECTING --> CONNECTED: accept()
CONNECTED --> RECEIVING: receive()
RECEIVING --> CONNECTED: send()
CONNECTED --> CLOSING: close() / قطع اتصال
CLOSING --> CLOSED: تم إغلاق الاتصال
CLOSED --> [*]
(1) ▶ مثال: نقطة نهاية WebSocket بسيطة
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws/echo")
async def websocket_echo(websocket: WebSocket):
await websocket.accept() # قبول الاتصال
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
except Exception:
await websocket.close()
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶ مثال: WebSocket مع معلمات المسار
@app.websocket("/ws/products/{product_id}/prices")
async def product_price_stream(
websocket: WebSocket,
product_id: int,
):
await websocket.accept()
try:
while True:
data = await websocket.receive_json()
# إرجاع مع سياق المنتج
await websocket.send_json({
"product_id": product_id,
"price": data.get("price"),
"currency": data.get("currency", "USD"),
})
except Exception:
await websocket.close()
الناتج:
# تم تعريف الدالة بنجاح
4. نمط مدير الاتصال
(1) تصميم ConnectionManager
(1) ▶ مثال: تنفيذ ConnectionManager
from fastapi import FastAPI, WebSocket
from typing import Dict, List
import json
class ConnectionManager:
def __init__(self):
# تعيين: product_id -> قائمة الاتصالات النشطة
self.active_connections: Dict[int, List[WebSocket]] = {}
async def connect(self, websocket: WebSocket, product_id: int):
await websocket.accept()
if product_id not in self.active_connections:
self.active_connections[product_id] = []
self.active_connections[product_id].append(websocket)
def disconnect(self, websocket: WebSocket, product_id: int):
if product_id in self.active_connections:
self.active_connections[product_id].remove(websocket)
if not self.active_connections[product_id]:
del self.active_connections[product_id]
async def broadcast_to_product(self, product_id: int, message: dict):
if product_id in self.active_connections:
dead_connections = []
for connection in self.active_connections[product_id]:
try:
await connection.send_json(message)
except Exception:
dead_connections.append(connection)
# تنظيف الاتصالات الميتة
for conn in dead_connections:
self.disconnect(conn, product_id)
async def broadcast_all(self, message: dict):
for product_id in list(self.active_connections.keys()):
await self.broadcast_to_product(product_id, message)
manager = ConnectionManager()
الناتج:
# تم تعريف الدالة بنجاح
(2) بنية دفع WebSocket
flowchart TD
Update[تحديث سعر عبر HTTP] --> Handler[معالج API]
Handler --> Manager[ConnectionManager]
Manager --> WS1[عميل WebSocket 1]
Manager --> WS2[عميل WebSocket 2]
Manager --> WSN[عميل WebSocket N]
subgraph Subscribers
WS1
WS2
WSN
end
(2) ▶ مثال: استخدام ConnectionManager مع نقطة نهاية WebSocket
app = FastAPI()
@app.websocket("/ws/products/{product_id}/prices")
async def price_websocket(websocket: WebSocket, product_id: int):
await manager.connect(websocket, product_id)
try:
while True:
# إبقاء الاتصال حيًا، استقبال أي رسائل من العميل
data = await websocket.receive_text()
except Exception:
manager.disconnect(websocket, product_id)
الناتج:
# تم تعريف الدالة بنجاح
5. مصادقة WebSocket
(1) التحقق من JWT خلال مرحلة المصافحة
لا يمتلك WebSocket آلية رأس قياسية؛ تُمرر الرموز عبر معلمات الاستعلام.
(1) ▶ مثال: مصادقة WebSocket JWT
from fastapi import WebSocket, Query, HTTPException
from jose import jwt, JWTError
from app.core.config import settings
async def verify_ws_token(token: str) -> dict:
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
return payload
except JWTError:
raise ValueError("Invalid token")
@app.websocket("/ws/prices")
async def authenticated_price_ws(
websocket: WebSocket,
token: str = Query(..., description="رمز الوصول JWT"),
):
# التحقق من الرمز قبل قبول الاتصال
try:
user = await verify_ws_token(token)
except ValueError:
await websocket.close(code=4001, reason="Authentication failed")
return
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_json({"user": user.get("sub"), "data": data})
except Exception:
pass
الناتج:
# تم تعريف الدالة بنجاح
| طريقة المصادقة | التنفيذ | المزايا | العيوب |
|---|---|---|---|
| معلمة الاستعلام | ?token=xxx |
بسيط | الرمز يظهر في سجلات URL |
| الرسالة الأولى | إرسال الرمز بعد الاتصال | لا يُعرض في URL | جولة إضافية واحدة |
| Sec-WebSocket-Protocol | نقل الرمز عبر البروتوكول الفرعي | لا يُعرض في URL | استخدام غير قياسي |
6. تعاون HTTP و WebSocket
(1) تغييرات الأسعار تؤدي إلى إشعارات دفع
(1) ▶ مثال: نقطة نهاية HTTP تؤدي إلى بث WebSocket
from pydantic import BaseModel, Field
from datetime import datetime
class PriceUpdate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0, description="السعر الجديد بالدولار")
currency: str = Field(default="USD")
source: str = Field(max_length=100)
app = FastAPI()
manager = ConnectionManager()
@app.post("/api/v1/prices", response_model=PriceResponse)
async def create_price(
price: PriceCreate,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = PriceService(db)
result = await service.create_price(price)
# تشغيل بث WebSocket بعد إنشاء السعر بنجاح
await manager.broadcast_to_product(
product_id=price.product_id,
message={
"event": "price_update",
"product_id": price.product_id,
"new_price": price.price,
"currency": price.currency,
"source": price.source,
"timestamp": datetime.utcnow().isoformat(),
},
)
return result
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶ مثال: تغذية أسعار عامة (الاشتراك في جميع التغييرات)
@app.websocket("/ws/prices/stream")
async def global_price_stream(websocket: WebSocket):
await websocket.accept()
# إضافة إلى قائمة المشتركين العامة
manager.global_connections.append(websocket)
try:
while True:
# استقبال نبض القلب/.ping من العميل
data = await websocket.receive_text()
if data == "ping":
await websocket.send_text("pong")
except Exception:
manager.global_connections.remove(websocket)
الناتج:
# تم تعريف الدالة بنجاح
❓ أسئلة شائعة
MessageModel.model_validate(data)؛ إذا فشل التحقق، أرسل رسالة خطأ إلى العميل.📖 ملخص
- يوفر WebSocket اتصالًا دائمًا ثنائي الاتجاه، يستبدل الاستقصاء لتمكين إشعارات الدفع الفعلي في الوقت الفعلي
- يدير ConnectionManager تجمع الاتصالات ويدعم البث المجمّع حسب معرف المنتج
- مصادقة JWT تُتحقق خلال مرحلة المصافحة باستخدام معلمات الاستعلام؛ إذا فشلت، يُغلق الاتصال فورًا (كود 4001)
- بعد إنشاء سعر في نقطة نهاية HTTP، استدعِ
manager.broadcast_to_product()لتشغيل إشعار دفع - بيئة الإنتاج تتطلب Redis Pub/Sub لتمكين البث عبر العمليات؛ الذاكرة أحادية العملية غير مناسبة لعمليات متعددة
📝 تمارين
- تمرين أساسي (الصعوبة ⭐): أنشئ نقطة نهاية WebSocket صدى حيث يرسل العميل نصًا ويعيد الخادم كما هو تمامًا. اختبرها باستخدام أدوات مطور المتصفح أو wscat. تلميح:
@app.websocket("/ws/echo")+accept()+receive_text() - تمرين متقدم (الصعوبة ⭐⭐): نفذ ConnectionManager يدعم اشتراك عملاء متعددين في تغييرات أسعار نفس المنتج. عند إنشاء سعر جديد، بثه لجميع عملاء WebSocket المشتركين في ذلك المنتج. تلميح:
Dict[int, List[WebSocket]]+broadcast_to_product() - تحدي (الصعوبة ⭐⭐⭐): أضف مصادقة JWT إلى WebSocket (عن طريق تمرير الرمز في معلمات الاستعلام)، وقم بتشغيل بث WebSocket في نقطة نهاية إنشاء السعر HTTP لتنفيذ سير عمل "كتابة HTTP → دفع WebSocket" كامل. تلميح:
token: str = Query(...)+verify_ws_token()+ استدعاء نقطة نهاية HTTP لـmanager.broadcast_to_product()
---|



