404 Not Found

404 Not Found


nginx

WebSocket — الاتصال ثنائي الاتجاه في الوقت الفعلي

HTTP مثل إرسال رسالة—يذهب في اتجاه واحد ويعود؛ WebSocket مثل إجراء مكالمة هاتفية—كلا الطرفين يمكنهما التحدث في أي وقت دون الحاجة للتعليق وإعادة الاتصال.

1. ما ستتعلمه


2. القصة الحقيقية لـ Alice

(1) نقطة الألم: تغييرات الأسعار يمكن استرجاعها فقط من خلال الاستقصاء

تستقصي واجهة Bob الأمامية واجهة PriceTracker API كل 5 ثوانٍ للتحقق من تغييرات الأسعار، لكن 99% من أسعار ملايين المنتجات تبقى بدون تغيير خلال أي فترة 5 ثوانٍ، مما يعني أن 99% من الطلبات مهملة. لأسوأ من ذلك، قد يستغرق عرض تغييرات الأسعار ما يصل إلى 5 ثوانٍ، مما يؤدي إلى شكاوى العملاء بأن الأسعار ليست في الوقت الفعلي.

(2) حل WebSocket

ينشئ WebSocket اتصالًا ثنائي الاتجاه دائمًا ويدفع تغييرات الأسعار بنشاط من الخادم إلى الواجهة الأمامية عند حدوثها، مما يلغي الحاجة للاستقصاء—صفر هدر، صفر تأخير.

PYTHON
@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) حالات دورة الحياة

100%
stateDiagram-v2
    [*] --> CONNECTING: العميل يبدأ
    CONNECTING --> CONNECTED: accept()
    CONNECTED --> RECEIVING: receive()
    RECEIVING --> CONNECTED: send()
    CONNECTED --> CLOSING: close() / قطع اتصال
    CLOSING --> CLOSED: تم إغلاق الاتصال
    CLOSED --> [*]

(1) ▶ مثال: نقطة نهاية WebSocket بسيطة

PYTHON
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()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: WebSocket مع معلمات المسار

PYTHON
@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()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

4. نمط مدير الاتصال

(1) تصميم ConnectionManager

(1) ▶ مثال: تنفيذ ConnectionManager

PYTHON
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()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) بنية دفع WebSocket

100%
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

PYTHON
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)

الناتج:

TEXT
# تم تعريف الدالة بنجاح

5. مصادقة WebSocket

(1) التحقق من JWT خلال مرحلة المصافحة

لا يمتلك WebSocket آلية رأس قياسية؛ تُمرر الرموز عبر معلمات الاستعلام.

(1) ▶ مثال: مصادقة WebSocket JWT

PYTHON
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

الناتج:

TEXT
# تم تعريف الدالة بنجاح
طريقة المصادقة التنفيذ المزايا العيوب
معلمة الاستعلام ?token=xxx بسيط الرمز يظهر في سجلات URL
الرسالة الأولى إرسال الرمز بعد الاتصال لا يُعرض في URL جولة إضافية واحدة
Sec-WebSocket-Protocol نقل الرمز عبر البروتوكول الفرعي لا يُعرض في URL استخدام غير قياسي

6. تعاون HTTP و WebSocket

(1) تغييرات الأسعار تؤدي إلى إشعارات دفع

(1) ▶ مثال: نقطة نهاية HTTP تؤدي إلى بث WebSocket

PYTHON
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

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: تغذية أسعار عامة (الاشتراك في جميع التغييرات)

PYTHON
@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)

الناتج:

TEXT
# تم تعريف الدالة بنجاح

❓ أسئلة شائعة

س كيف أختار بين WebSocket و SSE (Server-Sent Events)؟
ج استخدم WebSocket للاتصال ثنائي الاتجاه (مثل الدردشة والتعاون في الوقت الفعلي)، واستخدم SSE عند الحاجة للدفع من جانب الخادم فقط (مثل تدفقات السجلات والإشعارات). SSE أبسط ويعيد الاتصال تلقائيًا.
س هل يوجد حد لعدد اتصالات WebSocket؟
ج على جهاز واحد، العدد محدود بواصفات الملفات (عادةً 65,535). في بيئات الإنتاج، يُستخدم Nginx لموازنة الحمل، وتتشارك مثيلات متعددة حالة الاتصال (عبر Redis Pub/Sub).
س كيف يجب التعامل مع انقطاع اتصال WebSocket؟
ج ينفذ العميل إعادة اتصال تلقائية (تراجع أسي). يستخدم الخادم نبضات القلب ping/pong لاكتشاف الاتصالات الميتة، وينظفها ConnectionManager تلقائيًا.
س هل يمكن لـ WebSocket استخدام Pydantic للتحقق من الرسائل؟
ج نعم. بعد استقبال رسالة، تحقق منها باستخدام MessageModel.model_validate(data)؛ إذا فشل التحقق، أرسل رسالة خطأ إلى العميل.
س كيف تبث عمليات العامل المتعددة البيانات؟
ج مدير اتصال الذاكرة أحادي العملية غير كافٍ. في بيئات الإنتاج، استخدم Redis Pub/Sub: تُنشر تغييرات الأسعار إلى قناة Redis، ويشترك كل عامل فيها ويدفع البيانات إلى اتصالاته المحلية.
س كيف تتعامل مع CORS لـ WebSocket؟
ج WebSocket في المتصفحات يخضع أيضًا لسياسة نفس الأصل. تعمل CORSMiddleware لـ FastAPI تلقائيًا على معالجة المصافحة عبر النطاقات لـ WebSocket.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): أنشئ نقطة نهاية WebSocket صدى حيث يرسل العميل نصًا ويعيد الخادم كما هو تمامًا. اختبرها باستخدام أدوات مطور المتصفح أو wscat. تلميح: @app.websocket("/ws/echo") + accept() + receive_text()
  2. تمرين متقدم (الصعوبة ⭐⭐): نفذ ConnectionManager يدعم اشتراك عملاء متعددين في تغييرات أسعار نفس المنتج. عند إنشاء سعر جديد، بثه لجميع عملاء WebSocket المشتركين في ذلك المنتج. تلميح: Dict[int, List[WebSocket]] + broadcast_to_product()
  3. تحدي (الصعوبة ⭐⭐⭐): أضف مصادقة JWT إلى WebSocket (عن طريق تمرير الرمز في معلمات الاستعلام)، وقم بتشغيل بث WebSocket في نقطة نهاية إنشاء السعر HTTP لتنفيذ سير عمل "كتابة HTTP → دفع WebSocket" كامل. تلميح: token: str = Query(...) + verify_ws_token() + استدعاء نقطة نهاية HTTP لـ manager.broadcast_to_product()

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%