WebSocket چیست؟ آموزش ساخت چت Real-Time با Python، FastAPI و API درواره

WebSocket پروتکلی برای ارتباط دائمی و دوطرفه میان Client و Server است. در این آموزش، یک چت Real-Time و Streaming با JavaScript، Python، FastAPI و API هوش مصنوعی درواره می‌سازید.

Share
 آموزش WebSocket و ساخت چت Real-Time با Python، FastAPI و API درواره

بیشتر ارتباطات میان مرورگر و Backend با HTTP انجام می‌شوند. Client درخواستی می‌فرستد، Server آن را پردازش می‌کند و پاسخی برمی‌گرداند. پس از پایان پاسخ، ارتباط منطقی آن Request تمام می‌شود.

این مدل برای REST API، دریافت اطلاعات، ثبت فرم و بسیاری از عملیات معمول کاملاً مناسب است. اما بعضی برنامه‌ها به ارتباط زنده و دوطرفه نیاز دارند:

  • چت آنلاین
  • Notification لحظه‌ای
  • داشبورد Real-Time
  • بازی آنلاین
  • نمایش وضعیت Job
  • ویرایش هم‌زمان سند
  • پشتیبانی آنلاین
  • Tracking لحظه‌ای
  • گفت‌وگو با مدل هوش مصنوعی
  • نمایش Streaming پاسخ مدل

در این برنامه‌ها، Poll کردن مداوم Server با HTTP می‌تواند باعث Requestهای اضافی، تأخیر و پیچیدگی بیشتر شود. WebSocket ارتباطی پایدار میان Client و Server ایجاد می‌کند که هر دو طرف می‌توانند در طول عمر Connection برای یکدیگر Message ارسال کنند.

در این مقاله ابتدا WebSocket را از پایه بررسی می‌کنیم، تفاوت آن را با HTTP، Polling و SSE توضیح می‌دهیم و سپس یک چت Real-Time عملی با JavaScript، Python، FastAPI و API هوش مصنوعی درواره می‌سازیم.

WebSocket چیست؟

WebSocket یک Protocol ارتباطی مبتنی بر TCP است که امکان ارتباط Full-Duplex یا دوطرفه را میان Client و Server روی یک Connection پایدار فراهم می‌کند.

Full-Duplex یعنی هر دو طرف می‌توانند مستقل از یکدیگر داده ارسال کنند:

Client → Server
Server → Client

پس از برقراری Connection، Server لازم نیست برای ارسال اطلاعات جدید منتظر HTTP Request بعدی Client بماند.

براساس RFC 6455، WebSocket با یک Opening Handshake آغاز می‌شود و پس از آن داده‌ها در قالب Frameهای متنی، Binary و Control روی Connection منتقل می‌شوند.

WebSocket چگونه کار می‌کند؟

برقراری WebSocket با یک HTTP Request ویژه آغاز می‌شود. Client از Server می‌خواهد Protocol ارتباط را از HTTP به WebSocket ارتقا دهد.

نمونه مفهومی Request:

GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: RANDOM_VALUE
Sec-WebSocket-Version: 13

اگر Server درخواست را بپذیرد، پاسخ زیر را برمی‌گرداند:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: GENERATED_VALUE

کد وضعیت 101 Switching Protocols یعنی ارتباط از HTTP به WebSocket تغییر کرده است.

پس از موفقیت Handshake، همان Connection باز می‌ماند و طرفین می‌توانند Message ارسال کنند.

WebSocket URI چیست؟

دو Scheme اصلی WebSocket عبارت‌اند از:

ws://
wss://

ws ارتباط معمولی و wss ارتباط WebSocket روی TLS است.

نمونه محلی:

ws://127.0.0.1:8000/ws/chat

نمونه Production:

wss://example.com/ws/chat

اگر صفحه وب با HTTPS باز شده باشد، مرورگر معمولاً اجازه اتصال ناامن ws:// را نمی‌دهد. در Production باید از wss:// استفاده شود.

تفاوت HTTP و WebSocket

ویژگیHTTPWebSocket
مدل ارتباطRequest/Responseدوطرفه و پایدار
آغاز ارسال توسط Serverمعمولاً پس از Requestدر هر زمان
طول Connectionمعمولاً محدود به Requestطولانی‌مدت
مناسب REST APIبسیار مناسبمعمولاً نه
مناسب چت زندهنیازمند Polling یا روش دیگربسیار مناسب
Streaming یک‌طرفهامکان‌پذیرامکان‌پذیر
ارتباط دوطرفهبا Requestهای جداروی یک Connection
پیچیدگی Scaleکمتربیشتر
Cache استاندارد HTTPداردندارد
Debug با ابزارهای RESTساده‌ترمتفاوت

WebSocket جایگزین کامل HTTP نیست. یک برنامه می‌تواند هم‌زمان از REST API برای عملیات معمول و WebSocket برای قابلیت‌های Real-Time استفاده کند.

Polling چیست؟

در Polling، Client در فاصله‌های زمانی مشخص از Server سؤال می‌کند آیا داده جدیدی وجود دارد یا خیر:

Client → چیزی جدید هست؟
Server → خیر

Client → چیزی جدید هست؟
Server → خیر

Client → چیزی جدید هست؟
Server → بله

نمونه JavaScript:

setInterval(async () => {
  const response = await fetch("/notifications");
  const data = await response.json();

  console.log(data);
}, 5000);

مزیت Polling سادگی آن است. اگر داده هر چند دقیقه تغییر می‌کند، Polling می‌تواند کافی باشد.

معایب:

  • ایجاد Requestهای تکراری
  • دریافت پاسخ‌های خالی
  • تأخیر وابسته به Interval
  • افزایش بار Server
  • نامناسب برای تعامل سریع

Long Polling چیست؟

در Long Polling، Client درخواست می‌فرستد و Server تا زمان آماده‌شدن داده یا رسیدن Timeout پاسخ را باز نگه می‌دارد.

بعد از دریافت پاسخ، Client دوباره Request جدید ایجاد می‌کند.

این روش نسبت به Polling معمولی تأخیر کمتری دارد، اما همچنان به ایجاد Requestهای متوالی نیاز دارد.

SSE چیست؟

Server-Sent Events یا SSE روشی برای Streaming یک‌طرفه از Server به Client روی HTTP است:

Client → Server
Server → Event
Server → Event
Server → Event

SSE برای این کاربردها مناسب است:

  • Streaming پاسخ مدل هوش مصنوعی
  • نمایش Log زنده
  • Notification یک‌طرفه
  • وضعیت پردازش
  • Feed زنده

در مرورگر می‌توان از EventSource استفاده کرد:

const source = new EventSource("/events");

source.onmessage = (event) => {
  console.log(event.data);
};

تفاوت WebSocket و SSE

معیارWebSocketSSE
جهت ارتباطدوطرفهServer به Client
Protocolمستقل پس از UpgradeHTTP
داده متنیدارددارد
داده Binaryداردبه‌صورت مستقیم نه
Reconnect مرورگرباید طراحی شودEventSource پشتیبانی می‌کند
مناسب چتبلههمراه REST قابل استفاده
مناسب Streaming مدلبلهبسیار مناسب
مناسب بازی آنلاینبلهمعمولاً نه
پیاده‌سازی Proxyنیازمند تنظیم Upgradeمعمولاً ساده‌تر
Debugابزار WebSocketابزار HTTP/SSE

اگر فقط می‌خواهید پاسخ مدل را از Server به Client Stream کنید، SSE معمولاً ساده‌تر است. اگر Client و Server باید در طول Connection چند نوع Message، فرمان، وضعیت، لغو عملیات و Event برای هم ارسال کنند، WebSocket انعطاف بیشتری دارد.

چه زمانی از WebSocket استفاده کنیم؟

WebSocket در این سناریوها مناسب است:

  • ارتباط دوطرفه و مداوم لازم است.
  • Latency پایین اهمیت دارد.
  • Server باید بدون Request جدید داده ارسال کند.
  • تعداد زیادی Event کوتاه ردوبدل می‌شود.
  • Client باید فرمان‌هایی مانند Cancel، Pause یا Resume بفرستد.
  • وضعیت Connection اهمیت دارد.
  • Presence یا Online Status لازم است.
  • چند کاربر در Room مشترک تعامل می‌کنند.

چه زمانی WebSocket انتخاب مناسبی نیست؟

WebSocket ممکن است برای این شرایط پیچیدگی غیرضروری ایجاد کند:

  • عملیات ساده CRUD است.
  • داده به‌ندرت تغییر می‌کند.
  • فقط یک پاسخ HTTP معمولی لازم است.
  • Streaming کاملاً یک‌طرفه است و SSE کافی است.
  • Client فقط هر چند دقیقه وضعیت را بررسی می‌کند.
  • زیرساخت Load Balancing برای Connectionهای طولانی آماده نیست.
  • تیم Monitoring و مدیریت Connection ندارد.
  • قابلیت Cache استاندارد HTTP اهمیت زیادی دارد.

Message و Frame در WebSocket

برنامه معمولاً با Message کار می‌کند، اما Protocol داده را در قالب Frame منتقل می‌کند.

انواع کلی Frame:

  • Text Frame
  • Binary Frame
  • Ping
  • Pong
  • Close
  • Continuation Frame

Text Message

برای JSON و متن UTF-8 مناسب است:

{
  "type": "message",
  "content": "سلام"
}

Binary Message

برای داده Binary مانند بخشی از Audio، تصویر یا Protocol سفارشی مناسب است.

ارسال فایل بزرگ کامل از طریق WebSocket همیشه انتخاب مناسبی نیست. در بسیاری از معماری‌ها فایل با HTTP به Object Storage آپلود می‌شود و فقط شناسه یا وضعیت آن از طریق WebSocket منتقل می‌شود.

Ping و Pong چیست؟

Connection ممکن است بدون Close مناسب قطع شود؛ برای مثال:

  • اینترنت Client قطع شود.
  • Mobile Network تغییر کند.
  • Proxy ارتباط Idle را ببندد.
  • Process ناگهان متوقف شود.

Ping و Pong برای تشخیص زنده‌بودن Connection استفاده می‌شوند.

در سطح Protocol، کتابخانه WebSocket می‌تواند Control Frameهای Ping و Pong را مدیریت کند. علاوه بر آن، برنامه نیز می‌تواند Heartbeat سطح Application داشته باشد:

Client:

{
  "type": "ping"
}

Server:

{
  "type": "pong"
}

اگر Client چند Heartbeat متوالی دریافت نکند، می‌تواند Connection را بسته و دوباره متصل شود.

Close Codeهای مهم WebSocket

هنگام بستن Connection می‌توان Close Code ارسال کرد.

کدمعنی
1000بسته‌شدن عادی
1001Client یا Server در حال خروج است
1002خطای Protocol
1003نوع داده پشتیبانی نمی‌شود
1008نقض Policy برنامه
1009Message بیش‌ازحد بزرگ است
1011خطای داخلی Server
1006قطع غیرعادی؛ فقط به‌صورت وضعیت مشاهده می‌شود

کد 1006 نباید به‌عنوان Close Frame ارسال شود. این کد معمولاً در Client نشان می‌دهد Connection بدون Closing Handshake مناسب قطع شده است.

چرا برای WebSocket به Message Protocol نیاز داریم؟

WebSocket فقط کانال انتقال را فراهم می‌کند و نمی‌گوید JSON برنامه شما چه ساختاری داشته باشد.

بهتر است یک Protocol مشخص برای Messageهای برنامه تعریف کنید.

پیام Client:

{
  "type": "message",
  "message_id": "msg-123",
  "content": "WebSocket را توضیح بده"
}

تأیید دریافت Server:

{
  "type": "ack",
  "message_id": "msg-123"
}

بخش Streaming:

{
  "type": "token",
  "message_id": "msg-123",
  "content": "WebSocket"
}

پایان پاسخ:

{
  "type": "done",
  "message_id": "msg-123"
}

خطا:

{
  "type": "error",
  "message_id": "msg-123",
  "code": "generation_failed",
  "message": "پاسخ تولید نشد."
}

این طراحی بهتر از ارسال رشته‌های بدون ساختار است؛ زیرا Client می‌تواند انواع Event را تشخیص دهد.

Versioning پروتکل WebSocket

ساختار Message ممکن است در آینده تغییر کند. بهتر است Version داشته باشد:

{
  "version": 1,
  "type": "message",
  "message_id": "msg-123",
  "content": "سلام"
}

برای تغییرات ناسازگار می‌توان:

  • Version داخل Message قرار داد.
  • Version را داخل URL گذاشت.
  • از WebSocket Subprotocol استفاده کرد.

نمونه URL:

wss://example.com/ws/v1/chat

پروژه عملی: ساخت چت Real-Time با FastAPI و درواره

در این پروژه یک برنامه کامل می‌سازیم که:

  • مرورگر با WebSocket به FastAPI متصل می‌شود.
  • کاربر Message ارسال می‌کند.
  • FastAPI Message را اعتبارسنجی می‌کند.
  • Backend به API درواره متصل می‌شود.
  • پاسخ مدل به‌صورت Streaming دریافت می‌شود.
  • هر بخش پاسخ فوراً از طریق WebSocket به مرورگر می‌رسد.
  • تاریخچه کوتاه گفتگو در همان Connection نگهداری می‌شود.
  • Heartbeat سطح Application داریم.
  • Client در صورت قطع ارتباط دوباره متصل می‌شود.
  • کلید API فقط در Backend نگهداری می‌شود.

معماری:

Browser
⇄ WebSocket
FastAPI
⇄ HTTPS Streaming
API درواره

درواره در این معماری WebSocket عمومی به Browser ارائه نمی‌کند. FastAPI به Endpoint استاندارد Chat Completions درواره متصل می‌شود و Stream دریافتی را به WebSocket Client منتقل می‌کند.

ساختار پروژه

websocket-ai-chat/
├── .env
├── .gitignore
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── app.py

نصب کتابخانه‌ها

فایل requirements.txt:

fastapi>=0.115,<1
uvicorn[standard]>=0.34,<1
openai>=1.100,<2
python-dotenv>=1.0,<2

ساخت Virtual Environment:

python -m venv .venv

فعال‌سازی در Linux و macOS:

source .venv/bin/activate

فعال‌سازی در Windows PowerShell:

.venv\Scripts\Activate.ps1

نصب وابستگی‌ها:

pip install -r requirements.txt

تنظیم متغیرهای محیطی

فایل .env:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

فایل .gitignore:

.env
.venv/
__pycache__/
*.pyc

کلید API را داخل JavaScript مرورگر قرار ندهید. در غیر این صورت هر کاربر می‌تواند آن را از Source، Developer Tools یا Network Traffic استخراج کند.

برای دریافت کلید API می‌توانید در درواره ثبت‌نام کنید. شناسه مدل‌ها و قیمت به‌روز آن‌ها در صفحه مدل‌های درواره قرار دارد.

کد کامل FastAPI و WebSocket

فایل app.py:

import json
import os
from contextlib import asynccontextmanager
from typing import Any
from uuid import uuid4

from dotenv import load_dotenv
from fastapi import (
    FastAPI,
    WebSocket,
    WebSocketDisconnect,
)
from fastapi.responses import HTMLResponse
from openai import AsyncOpenAI

load_dotenv()

DARVAREH_API_KEY = os.getenv(
    "DARVAREH_API_KEY",
    "",
)

DARVAREH_MODEL_ID = os.getenv(
    "DARVAREH_MODEL_ID",
    "YOUR_MODEL_ID",
)

DARVAREH_BASE_URL = (
    "https://api.darvareh.ir/v1"
)

MAX_MESSAGE_LENGTH = 4_000
MAX_HISTORY_MESSAGES = 12


@asynccontextmanager
async def lifespan(app: FastAPI):
    if not DARVAREH_API_KEY:
        raise RuntimeError(
            "DARVAREH_API_KEY is missing"
        )

    client = AsyncOpenAI(
        api_key=DARVAREH_API_KEY,
        base_url=DARVAREH_BASE_URL,
        timeout=60,
        max_retries=2,
    )

    app.state.ai_client = client

    try:
        yield
    finally:
        await client.close()


app = FastAPI(
    title="WebSocket AI Chat",
    version="1.0.0",
    lifespan=lifespan,
)


HTML_PAGE = """
<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1"
  >
  <title>چت WebSocket با درواره</title>

  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      min-height: 100vh;
      font-family: Tahoma, Arial, sans-serif;
      background: #f5f7fb;
      color: #172033;
    }

    .app {
      width: min(880px, calc(100% - 32px));
      margin: 32px auto;
      background: #ffffff;
      border: 1px solid #e4e8f0;
      border-radius: 18px;
      overflow: hidden;
      box-shadow: 0 16px 40px rgba(25, 35, 58, 0.08);
    }

    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      padding: 20px 24px;
      border-bottom: 1px solid #e8ebf2;
    }

    .header h1 {
      margin: 0;
      font-size: 20px;
    }

    .status {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 13px;
      color: #647086;
    }

    .status-dot {
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background: #e0a400;
    }

    .status.connected .status-dot {
      background: #16a36a;
    }

    .status.disconnected .status-dot {
      background: #d64545;
    }

    .messages {
      min-height: 480px;
      max-height: 65vh;
      overflow-y: auto;
      padding: 24px;
    }

    .message {
      width: fit-content;
      max-width: min(78%, 650px);
      margin-bottom: 14px;
      padding: 12px 15px;
      border-radius: 15px;
      line-height: 1.9;
      white-space: pre-wrap;
      overflow-wrap: anywhere;
    }

    .message.user {
      margin-right: auto;
      color: #ffffff;
      background: #6554e8;
      border-bottom-left-radius: 5px;
    }

    .message.assistant {
      margin-left: auto;
      background: #eef1f7;
      border-bottom-right-radius: 5px;
    }

    .message.system {
      max-width: 100%;
      margin-inline: auto;
      padding: 7px 12px;
      color: #768197;
      background: transparent;
      font-size: 12px;
      text-align: center;
    }

    .composer {
      display: flex;
      gap: 12px;
      padding: 18px;
      border-top: 1px solid #e8ebf2;
      background: #fbfcfe;
    }

    textarea {
      flex: 1;
      min-height: 54px;
      max-height: 160px;
      padding: 14px;
      resize: vertical;
      border: 1px solid #d9dfeb;
      border-radius: 12px;
      font: inherit;
      line-height: 1.7;
      outline: none;
    }

    textarea:focus {
      border-color: #6554e8;
    }

    button {
      min-width: 110px;
      padding: 0 18px;
      border: 0;
      border-radius: 12px;
      color: white;
      background: #6554e8;
      font: inherit;
      cursor: pointer;
    }

    button:disabled {
      cursor: not-allowed;
      opacity: 0.55;
    }

    .hint {
      padding: 0 20px 16px;
      color: #7b8497;
      background: #fbfcfe;
      font-size: 12px;
    }
  </style>
</head>

<body>
  <main class="app">
    <header class="header">
      <h1>چت Real-Time با WebSocket</h1>

      <div
        id="status"
        class="status disconnected"
      >
        <span class="status-dot"></span>
        <span id="statusText">قطع</span>
      </div>
    </header>

    <section
      id="messages"
      class="messages"
      aria-live="polite"
    ></section>

    <form id="form" class="composer">
      <textarea
        id="input"
        maxlength="4000"
        placeholder="پیام خود را بنویسید..."
        required
      ></textarea>

      <button id="sendButton" type="submit">
        ارسال
      </button>
    </form>

    <div class="hint">
      برای ارسال، Enter و برای خط جدید Shift + Enter را بزنید.
    </div>
  </main>

  <script>
    const messagesElement =
      document.getElementById("messages");

    const form =
      document.getElementById("form");

    const input =
      document.getElementById("input");

    const sendButton =
      document.getElementById("sendButton");

    const statusElement =
      document.getElementById("status");

    const statusText =
      document.getElementById("statusText");

    let socket = null;
    let heartbeatTimer = null;
    let reconnectTimer = null;
    let reconnectAttempt = 0;
    let activeAssistantElement = null;
    let activeMessageId = null;

    function getWebSocketUrl() {
      const protocol =
        window.location.protocol === "https:"
          ? "wss:"
          : "ws:";

      return (
        protocol +
        "//" +
        window.location.host +
        "/ws/chat"
      );
    }

    function setStatus(state, text) {
      statusElement.className =
        "status " + state;

      statusText.textContent = text;
    }

    function scrollToBottom() {
      messagesElement.scrollTop =
        messagesElement.scrollHeight;
    }

    function createMessage(role, text = "") {
      const element =
        document.createElement("div");

      element.className =
        "message " + role;

      element.textContent = text;

      messagesElement.appendChild(element);
      scrollToBottom();

      return element;
    }

    function createId() {
      if (
        window.crypto &&
        window.crypto.randomUUID
      ) {
        return window.crypto.randomUUID();
      }

      return (
        Date.now().toString(36) +
        "-" +
        Math.random().toString(36).slice(2)
      );
    }

    function startHeartbeat() {
      stopHeartbeat();

      heartbeatTimer = setInterval(() => {
        if (
          socket &&
          socket.readyState === WebSocket.OPEN
        ) {
          socket.send(
            JSON.stringify({
              version: 1,
              type: "ping",
              sent_at: new Date().toISOString()
            })
          );
        }
      }, 25000);
    }

    function stopHeartbeat() {
      if (heartbeatTimer) {
        clearInterval(heartbeatTimer);
        heartbeatTimer = null;
      }
    }

    function scheduleReconnect() {
      if (reconnectTimer) {
        return;
      }

      const delay = Math.min(
        1000 * Math.pow(2, reconnectAttempt),
        15000
      );

      reconnectAttempt += 1;

      createMessage(
        "system",
        "اتصال قطع شد؛ تلاش مجدد..."
      );

      reconnectTimer = setTimeout(() => {
        reconnectTimer = null;
        connect();
      }, delay);
    }

    function connect() {
      if (
        socket &&
        (
          socket.readyState === WebSocket.OPEN ||
          socket.readyState === WebSocket.CONNECTING
        )
      ) {
        return;
      }

      setStatus(
        "connecting",
        "در حال اتصال"
      );

      socket = new WebSocket(
        getWebSocketUrl()
      );

      socket.addEventListener("open", () => {
        reconnectAttempt = 0;

        setStatus(
          "connected",
          "متصل"
        );

        sendButton.disabled = false;
        startHeartbeat();
      });

      socket.addEventListener(
        "message",
        (event) => {
          let data;

          try {
            data = JSON.parse(event.data);
          } catch {
            createMessage(
              "system",
              "پاسخ نامعتبر از Server دریافت شد."
            );

            return;
          }

          if (data.type === "ready") {
            createMessage(
              "system",
              "ارتباط Real-Time برقرار شد."
            );

            return;
          }

          if (data.type === "ack") {
            return;
          }

          if (
            data.type === "token" &&
            data.message_id === activeMessageId
          ) {
            if (!activeAssistantElement) {
              activeAssistantElement =
                createMessage("assistant");
            }

            activeAssistantElement.textContent +=
              data.content;

            scrollToBottom();
            return;
          }

          if (
            data.type === "done" &&
            data.message_id === activeMessageId
          ) {
            if (!activeAssistantElement) {
              activeAssistantElement =
                createMessage(
                  "assistant",
                  "پاسخی دریافت نشد."
                );
            }

            activeAssistantElement = null;
            activeMessageId = null;
            sendButton.disabled = false;
            input.disabled = false;
            input.focus();
            return;
          }

          if (data.type === "error") {
            createMessage(
              "system",
              data.message ||
              "خطایی در پردازش پیام رخ داد."
            );

            activeAssistantElement = null;
            activeMessageId = null;
            sendButton.disabled = false;
            input.disabled = false;
          }
        }
      );

      socket.addEventListener(
        "close",
        (event) => {
          stopHeartbeat();

          setStatus(
            "disconnected",
            "قطع"
          );

          sendButton.disabled = true;
          input.disabled = false;

          activeAssistantElement = null;
          activeMessageId = null;

          if (event.code !== 1000) {
            scheduleReconnect();
          }
        }
      );

      socket.addEventListener("error", () => {
        setStatus(
          "disconnected",
          "خطای اتصال"
        );
      });
    }

    form.addEventListener(
      "submit",
      (event) => {
        event.preventDefault();

        const content = input.value.trim();

        if (!content) {
          return;
        }

        if (
          !socket ||
          socket.readyState !== WebSocket.OPEN
        ) {
          createMessage(
            "system",
            "ارتباط با Server برقرار نیست."
          );

          return;
        }

        activeMessageId = createId();
        activeAssistantElement = null;

        createMessage(
          "user",
          content
        );

        socket.send(
          JSON.stringify({
            version: 1,
            type: "message",
            message_id: activeMessageId,
            content: content
          })
        );

        input.value = "";
        input.disabled = true;
        sendButton.disabled = true;
      }
    );

    input.addEventListener(
      "keydown",
      (event) => {
        if (
          event.key === "Enter" &&
          !event.shiftKey
        ) {
          event.preventDefault();
          form.requestSubmit();
        }
      }
    );

    window.addEventListener(
      "beforeunload",
      () => {
        stopHeartbeat();

        if (
          socket &&
          socket.readyState === WebSocket.OPEN
        ) {
          socket.close(
            1000,
            "Page closed"
          );
        }
      }
    );

    sendButton.disabled = true;
    connect();
  </script>
</body>
</html>
"""


@app.get("/")
async def index():
    return HTMLResponse(HTML_PAGE)


@app.get("/health")
async def health():
    return {
        "status": "ready",
        "service": "websocket-ai-chat",
    }


def build_system_message() -> dict[str, str]:
    return {
        "role": "system",
        "content": (
            "شما یک دستیار فارسی دقیق و کاربردی هستید. "
            "پاسخ را براساس درخواست کاربر تولید کنید. "
            "اگر درباره موضوعی مطمئن نیستید، "
            "عدم قطعیت را صریح بیان کنید."
        ),
    }


def validate_client_message(
    data: Any,
) -> tuple[str, str]:
    if not isinstance(data, dict):
        raise ValueError(
            "Message must be a JSON object"
        )

    if data.get("version") != 1:
        raise ValueError(
            "Unsupported message version"
        )

    if data.get("type") != "message":
        raise ValueError(
            "Unsupported message type"
        )

    message_id = data.get("message_id")
    content = data.get("content")

    if not isinstance(message_id, str):
        raise ValueError(
            "message_id is required"
        )

    if not isinstance(content, str):
        raise ValueError(
            "content must be a string"
        )

    content = content.strip()

    if not content:
        raise ValueError(
            "content cannot be empty"
        )

    if len(content) > MAX_MESSAGE_LENGTH:
        raise ValueError(
            "content is too long"
        )

    return message_id, content


async def send_event(
    websocket: WebSocket,
    event_type: str,
    **data: Any,
) -> None:
    await websocket.send_json(
        {
            "version": 1,
            "type": event_type,
            **data,
        }
    )


@app.websocket("/ws/chat")
async def websocket_chat(
    websocket: WebSocket,
):
    connection_id = str(uuid4())
    history: list[dict[str, str]] = []

    await websocket.accept()

    await send_event(
        websocket,
        "ready",
        connection_id=connection_id,
    )

    try:
        while True:
            try:
                data = await websocket.receive_json()

            except json.JSONDecodeError:
                await send_event(
                    websocket,
                    "error",
                    code="invalid_json",
                    message=(
                        "پیام باید JSON معتبر باشد."
                    ),
                )

                continue

            if (
                isinstance(data, dict)
                and data.get("type") == "ping"
            ):
                await send_event(
                    websocket,
                    "pong",
                )

                continue

            try:
                message_id, content = (
                    validate_client_message(data)
                )

            except ValueError as exc:
                await send_event(
                    websocket,
                    "error",
                    message_id=(
                        data.get("message_id")
                        if isinstance(data, dict)
                        else None
                    ),
                    code="invalid_message",
                    message=str(exc),
                )

                continue

            await send_event(
                websocket,
                "ack",
                message_id=message_id,
            )

            history.append(
                {
                    "role": "user",
                    "content": content,
                }
            )

            history = history[
                -MAX_HISTORY_MESSAGES:
            ]

            generated_parts: list[str] = []

            try:
                stream = await (
                    websocket.app.state.ai_client
                    .chat.completions.create(
                        model=DARVAREH_MODEL_ID,
                        temperature=0.3,
                        max_tokens=1200,
                        stream=True,
                        messages=[
                            build_system_message(),
                            *history,
                        ],
                    )
                )

                async for chunk in stream:
                    if not chunk.choices:
                        continue

                    token = (
                        chunk.choices[
                            0
                        ].delta.content
                    )

                    if not token:
                        continue

                    generated_parts.append(token)

                    await send_event(
                        websocket,
                        "token",
                        message_id=message_id,
                        content=token,
                    )

                assistant_message = "".join(
                    generated_parts
                ).strip()

                if assistant_message:
                    history.append(
                        {
                            "role": "assistant",
                            "content": (
                                assistant_message
                            ),
                        }
                    )

                    history = history[
                        -MAX_HISTORY_MESSAGES:
                    ]

                await send_event(
                    websocket,
                    "done",
                    message_id=message_id,
                )

            except WebSocketDisconnect:
                raise

            except Exception:
                if (
                    history
                    and history[-1]["role"] == "user"
                    and history[-1]["content"] == content
                ):
                    history.pop()

                await send_event(
                    websocket,
                    "error",
                    message_id=message_id,
                    code="generation_failed",
                    message=(
                        "تولید پاسخ در این لحظه "
                        "انجام نشد. دوباره تلاش کنید."
                    ),
                )

    except WebSocketDisconnect:
        print(
            "WebSocket disconnected:",
            connection_id,
        )

اجرای پروژه

برنامه را اجرا کنید:

uvicorn app:app --reload

سپس این آدرس را باز کنید:

http://127.0.0.1:8000

مرورگر به‌صورت خودکار به این WebSocket متصل می‌شود:

ws://127.0.0.1:8000/ws/chat

هنگام ارسال Message، پاسخ مدل به‌صورت تدریجی نمایش داده می‌شود.

تست WebSocket در Developer Tools

در مرورگر Chrome یا Edge:

  1. Developer Tools را باز کنید.
  2. وارد بخش Network شوید.
  3. فیلتر WS را انتخاب کنید.
  4. Connection مربوط به /ws/chat را باز کنید.
  5. بخش Messages را مشاهده کنید.

Messageهای ارسالی و دریافتی مانند این‌ها قابل مشاهده‌اند:

{
  "version": 1,
  "type": "message",
  "message_id": "msg-123",
  "content": "سلام"
}
{
  "version": 1,
  "type": "token",
  "message_id": "msg-123",
  "content": "سلام"
}

ساخت Dockerfile

فایل Dockerfile:

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

COPY app.py .

CMD [
    "uvicorn",
    "app:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

ساخت Docker Compose

فایل docker-compose.yml:

services:
  websocket-chat:
    build:
      context: .
    container_name: websocket-ai-chat
    environment:
      DARVAREH_API_KEY: ${DARVAREH_API_KEY}
      DARVAREH_MODEL_ID: ${DARVAREH_MODEL_ID}
    ports:
      - "127.0.0.1:8000:8000"
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"
        ]
      interval: 15s
      timeout: 5s
      retries: 10
    restart: unless-stopped

اجرا:

docker compose up \
  --build \
  -d

مشاهده Log:

docker compose logs \
  -f \
  websocket-chat

تنظیم Nginx برای WebSocket

اگر FastAPI پشت Nginx قرار دارد، Nginx باید Headerهای Upgrade را عبور دهد.

نمونه Configuration:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream websocket_backend {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;

    location /ws/ {
        proxy_pass http://websocket_backend;

        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 300s;
        proxy_send_timeout 300s;

        proxy_buffering off;
    }

    location / {
        proxy_pass http://websocket_backend;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

در Production باید HTTPS و در نتیجه wss:// فعال باشد. این نمونه فقط بخش مرتبط با Proxy کردن WebSocket را نشان می‌دهد.

چرا پاسخ مدل را از HTTP Stream به WebSocket تبدیل کردیم؟

Endpoint Chat Completions درواره از Streaming پشتیبانی می‌کند. Backend هر Chunk را از اتصال HTTP دریافت و بلافاصله به Client WebSocket ارسال می‌کند:

API درواره
→ Chunk 1
→ FastAPI
→ WebSocket Token 1
→ Browser

API درواره
→ Chunk 2
→ FastAPI
→ WebSocket Token 2
→ Browser

این روش باعث می‌شود کاربر برای مشاهده پاسخ کامل منتظر پایان Generation نماند.

مدیریت تاریخچه گفتگو

در پروژه آموزشی، تاریخچه داخل Memory همان Connection ذخیره می‌شود:

history: list[dict[str, str]] = []

مزایا:

  • ساده است.
  • به Database نیاز ندارد.
  • برای Demo مناسب است.

محدودیت‌ها:

  • با بسته‌شدن Connection حذف می‌شود.
  • میان چند Instance مشترک نیست.
  • در Restart از بین می‌رود.
  • برای چند دستگاه کاربر قابل بازیابی نیست.
  • کنترل Token دقیق ندارد.

در Production تاریخچه را می‌توان در PostgreSQL ذخیره کرد و چند Message اخیر یا Summary گفتگو را هنگام اتصال بازیابی کرد.

محدودکردن Context

ارسال کل تاریخچه گفتگو در هر Request هزینه و Latency را افزایش می‌دهد.

در نمونه آموزشی فقط تعداد محدودی Message نگه داشته می‌شود:

MAX_HISTORY_MESSAGES = 12

راهکار حرفه‌ای‌تر:

  • نگهداری چند Message اخیر
  • خلاصه‌سازی بخش‌های قدیمی
  • شمارش Token
  • حذف Messageهای غیرضروری
  • ذخیره Summary
  • بازیابی اطلاعات مرتبط
  • تعیین سقف هزینه هر گفتگو

لغو Generation

در نسخه فعلی، Client هنگام Generation ورودی را غیرفعال می‌کند و Server تا پایان Stream همان Message را پردازش می‌کند.

برای اضافه‌کردن Cancel واقعی، دریافت Message و Generation باید در Taskهای جدا اجرا شوند:

Receive Task
├── message
├── cancel
└── ping

Generation Task
├── token
├── done
└── cancelled

Client می‌تواند ارسال کند:

{
  "version": 1,
  "type": "cancel",
  "message_id": "msg-123"
}

Server Task مربوط به همان message_id را Cancel می‌کند. این طراحی نیازمند مدیریت دقیق Task، State و بسته‌شدن Stream بالادستی است.

مدیریت چند Connection

برای Chat Room یا Notification باید Connectionهای فعال را نگهداری کنید.

نمونه ساده:

from fastapi import WebSocket


class ConnectionManager:
    def __init__(self):
        self.active_connections: set[
            WebSocket
        ] = set()

    async def connect(
        self,
        websocket: WebSocket,
    ):
        await websocket.accept()

        self.active_connections.add(
            websocket
        )

    def disconnect(
        self,
        websocket: WebSocket,
    ):
        self.active_connections.discard(
            websocket
        )

    async def broadcast(
        self,
        message: dict,
    ):
        disconnected = []

        for connection in (
            self.active_connections
        ):
            try:
                await connection.send_json(
                    message
                )

            except Exception:
                disconnected.append(
                    connection
                )

        for connection in disconnected:
            self.disconnect(connection)

این روش فقط برای یک Process کار می‌کند. اگر چند Worker یا Server داشته باشید، Connectionهای Processهای دیگر در این Set دیده نمی‌شوند.

Scale کردن WebSocket

WebSocket به دلیل Connection طولانی‌مدت، Scale متفاوتی از REST API دارد.

فرض کنید سه Instance دارید:

Load Balancer
├── Instance 1
├── Instance 2
└── Instance 3

هر WebSocket تا زمان قطع معمولاً به یک Instance متصل می‌ماند.

برای Broadcast میان همه کاربران، یک Backplane مشترک لازم است:

Instance 1
┐
Instance 2
├── Redis Pub/Sub یا Message Broker
Instance 3
┘

هر Instance Messageهای مشترک را دریافت و به Connectionهای محلی خودش ارسال می‌کند.

Sticky Session

Sticky Session باعث می‌شود Connectionهای بعدی یک کاربر به Instance قبلی هدایت شوند.

این قابلیت ممکن است مفید باشد، اما جای Storage مشترک را نمی‌گیرد:

  • Instance ممکن است Restart شود.
  • Load Balancer ممکن است Mapping را از دست بدهد.
  • کاربر ممکن است از دستگاه دیگری متصل شود.
  • Scale Down ممکن است Connection را جابه‌جا کند.

State مهم باید خارج از Process ذخیره شود.

Redis در معماری WebSocket

Redis می‌تواند برای این موارد استفاده شود:

  • Presence
  • Pub/Sub میان Instanceها
  • Session کوتاه‌مدت
  • Mapping کاربر به Connection
  • Rate Limit
  • Cache
  • Room Metadata

اما خود Connection WebSocket داخل Redis ذخیره نمی‌شود. Connection متعلق به Process و Socket همان Server است. Redis فقط Metadata و Messageهای میان Instanceها را منتقل می‌کند.

Backpressure در WebSocket

اگر Server سریع‌تر از توان Client Message ارسال کند، Buffer رشد می‌کند.

مثال:

  • Server هزاران Event در ثانیه می‌فرستد.
  • اینترنت Client کند است.
  • Browser نمی‌تواند UI را سریع Render کند.
  • Memory مصرف می‌شود.
  • Latency افزایش می‌یابد.

راهکارها:

  • محدودکردن نرخ ارسال
  • Batch کردن Eventها
  • حذف Eventهای قدیمی و غیرضروری
  • نگهداری فقط آخرین State
  • محدودکردن Queue خروجی هر Connection
  • قطع Client بسیار کند
  • مانیتور کردن Buffer
  • کاهش دفعات Render در Frontend

در Browser می‌توان bufferedAmount را بررسی کرد:

if (socket.bufferedAmount > 1_000_000) {
  console.warn("WebSocket buffer is growing");
}

Reconnect استاندارد

قطع WebSocket همیشه خطای برنامه نیست. Mobile Network، Sleep دستگاه، Deploy و Load Balancer می‌توانند Connection را قطع کنند.

Reconnect بهتر است Exponential Backoff داشته باشد:

1 second
2 seconds
4 seconds
8 seconds
15 seconds

Reconnect فوری و نامحدود هزاران Client می‌تواند پس از Restart Server باعث Reconnect Storm شود. اضافه‌کردن Jitter تصادفی این فشار را توزیع می‌کند.

Resume بعد از Reconnect

Reconnect به‌تنهایی تضمین نمی‌کند Eventهای زمان قطعی دریافت شوند.

برای Resume می‌توان هر Event را Sequence Number داد:

{
  "type": "notification",
  "sequence": 1248,
  "data": {}
}

Client آخرین Sequence دریافت‌شده را نگه می‌دارد و هنگام Reconnect ارسال می‌کند:

{
  "type": "resume",
  "last_sequence": 1248
}

Server Eventهای ازدست‌رفته را از Storage پایدار بازیابی می‌کند.

برای Chat نیز می‌توان از conversation_id و last_message_id استفاده کرد.

احراز هویت WebSocket

مرورگر API عمومی برای اضافه‌کردن Header دلخواه Authorization به Constructor استاندارد WebSocket ندارد.

روش‌های رایج:

  • Cookie دارای Session
  • Ticket کوتاه‌عمر دریافت‌شده از REST API
  • Subprotocol کنترل‌شده
  • Message احراز هویت بلافاصله پس از اتصال

قرار دادن Token بلندمدت داخل Query String مناسب نیست؛ زیرا URL ممکن است در Logهای Proxy ثبت شود.

الگوی Ticket کوتاه‌عمر:

  1. Client با REST و Session معتبر Ticket می‌گیرد.
  2. Server Ticket یک‌بارمصرف و کوتاه‌عمر می‌سازد.
  3. Client با Ticket به WebSocket متصل می‌شود.
  4. Server Ticket را اعتبارسنجی و مصرف می‌کند.
  5. Connection به User ID متصل می‌شود.

کلید API درواره فقط باید در Backend باقی بماند و نباید به WebSocket Client ارسال شود.

کنترل Origin

Browser هنگام Handshake معمولاً Header مربوط به Origin را ارسال می‌کند. Server می‌تواند فقط Originهای مجاز را بپذیرد.

نمونه مفهومی:

ALLOWED_ORIGINS = {
    "https://example.com",
    "https://app.example.com",
}

origin = websocket.headers.get(
    "origin"
)

if origin not in ALLOWED_ORIGINS:
    await websocket.close(
        code=1008
    )

    return

در محیط توسعه باید Origin محلی خود را نیز با دقت اضافه کنید.

محدودکردن Message

برای هر Message محدودیت تعریف کنید:

  • حداکثر طول متن
  • حداکثر اندازه Binary
  • تعداد Message در دقیقه
  • تعداد Connection هر کاربر
  • تعداد Generation هم‌زمان
  • Timeout عملیات
  • فهرست Typeهای مجاز

در پروژه آموزشی طول Message به ۴۰۰۰ کاراکتر محدود شده است:

MAX_MESSAGE_LENGTH = 4_000

این محدودیت باید با مدل، هزینه، کاربرد و نوع داده تنظیم شود.

مدیریت Rate Limit

در چت هوش مصنوعی، محدودیت فقط تعداد Connection نیست.

Metricهای مهم:

  • Connection هم‌زمان هر کاربر
  • Message در دقیقه
  • Generation هم‌زمان
  • Token در دقیقه
  • هزینه روزانه
  • تعداد Reconnect
  • تعداد خطا
  • مدت Connection

برای مثال، بازبودن پنج Tab می‌تواند پنج WebSocket جدا ایجاد کند.

ذخیره Messageها

در Production معمولاً این اطلاعات در Database ذخیره می‌شوند:

conversations
messages
connections
generation_runs
usage_records

مدل داده نمونه:

CREATE TABLE conversations (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE messages (
    id UUID PRIMARY KEY,
    conversation_id UUID NOT NULL,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    status TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

وضعیت Message می‌تواند یکی از این مقادیر باشد:

received
generating
completed
failed
cancelled

Observability در WebSocket

یک HTTP Request سریع آغاز و تمام می‌شود، اما WebSocket ممکن است ساعت‌ها باز بماند. بنابراین Monitoring باید Connection‌محور باشد.

Metricهای مهم:

  • تعداد Connection فعال
  • نرخ Connection جدید
  • نرخ Disconnect
  • مدت متوسط Connection
  • Message ورودی در ثانیه
  • Message خروجی در ثانیه
  • تعداد Connection هر Instance
  • حجم داده ورودی و خروجی
  • Generation موفق و ناموفق
  • زمان اولین Token
  • زمان کامل پاسخ
  • تعداد Reconnect
  • Close Codeها
  • Buffer و Backpressure
  • تعداد Connection ردشده

Log ساختاریافته

برای هر Connection:

{
  "event": "websocket_connected",
  "connection_id": "connection-123",
  "user_id": "user-456",
  "origin": "https://example.com",
  "instance": "ws-server-2"
}

برای هر Generation:

{
  "event": "ai_generation_completed",
  "connection_id": "connection-123",
  "message_id": "message-789",
  "model": "YOUR_MODEL_ID",
  "time_to_first_token_ms": 620,
  "duration_ms": 4100,
  "status": "completed"
}

متن کامل کاربران، کلید API و داده‌های حساس غیرضروری را در Log قرار ندهید.

تست WebSocket

تست اتصال

بررسی کنید:

  • Handshake موفق است.
  • Event نوع ready دریافت می‌شود.
  • Connection باز می‌ماند.
  • Close عادی با کد ۱۰۰۰ انجام می‌شود.

تست Message

بررسی کنید:

  • JSON معتبر پردازش می‌شود.
  • JSON نامعتبر خطای کنترل‌شده دارد.
  • Message خالی رد می‌شود.
  • Message بزرگ رد می‌شود.
  • Type ناشناخته رد می‌شود.
  • Version ناسازگار رد می‌شود.

تست Streaming

بررسی کنید:

  • Tokenها به ترتیب می‌رسند.
  • done پس از آخرین Token ارسال می‌شود.
  • Error باعث فعال‌شدن دوباره UI می‌شود.
  • قطع Client باعث توقف مناسب پردازش می‌شود.
  • پاسخ خالی مدیریت می‌شود.

تست قطعی

این سناریوها را آزمایش کنید:

  • Restart FastAPI
  • قطع اینترنت Client
  • بسته‌شدن Tab
  • Timeout API مدل
  • Restart Reverse Proxy
  • تغییر شبکه Mobile
  • خاموش‌شدن یک Instance
  • Reconnect هم‌زمان تعداد زیاد Client

Load Test WebSocket

Load Test WebSocket با REST متفاوت است. باید طول عمر Connection و تعداد Message را هم‌زمان شبیه‌سازی کنید.

موارد مهم:

  • Connection هم‌زمان
  • Connection جدید در ثانیه
  • Message در ثانیه
  • مدت Connection
  • نرخ Ping/Pong
  • حجم Message
  • Latency ارسال
  • زمان اولین Token
  • CPU و Memory هر Connection
  • File Descriptor
  • رفتار هنگام Disconnect گروهی
  • Reconnect Storm

فقط بازکردن تعداد زیادی Connection بدون ارسال Message، رفتار واقعی برنامه را شبیه‌سازی نمی‌کند.

خطاهای رایج WebSocket

خطای 403 هنگام اتصال

دلایل احتمالی:

  • Server پیش از accept اتصال را رد کرده است.
  • Origin مجاز نیست.
  • Session یا Ticket معتبر نیست.
  • Route اشتباه است.
  • Proxy درخواست Upgrade را درست عبور نمی‌دهد.

Connection بلافاصله بسته می‌شود

موارد زیر را بررسی کنید:

  • Exception داخل Endpoint
  • نبود متغیر محیطی
  • خطای مدل
  • تنظیم نبودن Proxy
  • Timeout کوتاه Load Balancer
  • فرمت نامعتبر Message
  • بستن Connection توسط Client

خطای 1006

کد ۱۰۰۶ معمولاً نشان‌دهنده قطع غیرعادی Connection است.

علت‌های رایج:

  • قطع شبکه
  • Restart Server
  • Timeout Proxy
  • Crash Process
  • بسته‌شدن TCP بدون Close Handshake
  • مشکل TLS
  • Load Balancer ناسازگار

Log Server و Proxy را همراه زمان قطع بررسی کنید.

WebSocket پشت Nginx کار نمی‌کند

معمولاً Headerهای زیر تنظیم نشده‌اند:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

همچنین باید از HTTP/1.1 برای Proxy استفاده شود:

proxy_http_version 1.1;

اتصال در Production بعد از مدتی قطع می‌شود

ممکن است Proxy یا Load Balancer برای Connection Idle، Timeout داشته باشد.

راهکارها:

  • Heartbeat
  • تنظیم Timeout مناسب
  • Ping/Pong
  • Reconnect Client
  • بررسی محدودیت زیرساخت
  • مانیتور Close Code و زمان Connection

Messageها در چند Worker گم می‌شوند

اگر Connection Manager داخل Memory Process باشد، هر Worker فقط Connectionهای خودش را می‌بیند.

برای Broadcast میان Workerها از Redis Pub/Sub یا Message Broker استفاده کنید.

پاسخ Stream به‌صورت یکجا نمایش داده می‌شود

احتمال‌های رایج:

  • مدل با stream=True فراخوانی نشده است.
  • Backend Chunkها را Buffer می‌کند.
  • Proxy Buffering فعال است.
  • Frontend تا done صبر می‌کند.
  • Tokenها فوراً Render نمی‌شوند.

در Nginx برای مسیر WebSocket:

proxy_buffering off;

UI هنگام Streaming کند می‌شود

به‌روزرسانی DOM برای هر Token کوچک می‌تواند در پاسخ‌های طولانی پرهزینه باشد.

می‌توان Tokenها را برای چند میلی‌ثانیه Buffer و سپس گروهی Render کرد:

Tokenها
→ Buffer کوتاه
→ Render هر 30 تا 50 میلی‌ثانیه

هزینه API افزایش پیدا می‌کند

علت‌های احتمالی:

  • تاریخچه کامل در هر Request ارسال می‌شود.
  • چند Tab هم‌زمان فعال است.
  • Reconnect باعث ارسال دوباره Message می‌شود.
  • Generation تکراری است.
  • محدودیت Message وجود ندارد.
  • Model Routing انجام نشده است.
  • پاسخ بیش‌ازحد طولانی است.

برای هر message_id وضعیت پردازش و Usage را ثبت کنید.

چک‌لیست Production

پیش از انتشار WebSocket در Production بررسی کنید:

  • از wss:// استفاده می‌شود.
  • Proxy از Upgrade پشتیبانی می‌کند.
  • Timeout Proxy تنظیم شده است.
  • Heartbeat وجود دارد.
  • Client دارای Reconnect با Backoff است.
  • Reconnect دارای Jitter است.
  • Message Protocol نسخه‌بندی شده است.
  • Messageها شناسه یکتا دارند.
  • محدودیت اندازه Message وجود دارد.
  • محدودیت Connection هر کاربر وجود دارد.
  • Rate Limit پیام و Generation وجود دارد.
  • کلید API فقط در Backend است.
  • Originهای مجاز بررسی می‌شوند.
  • احراز هویت Connection طراحی شده است.
  • State مهم خارج از Process ذخیره می‌شود.
  • تاریخچه گفتگو پایدار است.
  • چند Instance از Backplane مشترک استفاده می‌کنند.
  • رفتار Duplicate Message مشخص است.
  • Idempotency بررسی شده است.
  • Backpressure مدیریت می‌شود.
  • Close Codeها ثبت می‌شوند.
  • Connection فعال مانیتور می‌شود.
  • Time to First Token اندازه‌گیری می‌شود.
  • هزینه هر Generation ثبت می‌شود.
  • Graceful Shutdown وجود دارد.
  • تست Restart و Reconnect انجام شده است.
  • Client کند آزمایش شده است.
  • Failure API بالادستی مدیریت شده است.

پرسش‌های متداول

WebSocket چیست؟

WebSocket پروتکلی برای ایجاد ارتباط دائمی و دوطرفه میان Client و Server روی یک Connection است.

وب سوکت چه تفاوتی با HTTP دارد؟

HTTP معمولاً براساس Request و Response کار می‌کند. در WebSocket هر دو طرف می‌توانند پس از اتصال در هر زمان Message ارسال کنند.

تفاوت WebSocket و SSE چیست؟

WebSocket دوطرفه است، اما SSE برای Streaming یک‌طرفه از Server به Client طراحی شده است. برای Streaming ساده پاسخ مدل، SSE می‌تواند انتخاب ساده‌تری باشد.

آیا WebSocket از HTTP استفاده می‌کند؟

Handshake اولیه WebSocket به‌شکل HTTP Upgrade انجام می‌شود. پس از موفقیت Handshake، ارتباط با Protocol WebSocket ادامه پیدا می‌کند.

تفاوت ws و wss چیست؟

ws ارتباط معمولی و wss ارتباط WebSocket روی TLS است. در Production باید از wss استفاده شود.

آیا WebSocket برای REST API مناسب است؟

معمولاً REST API برای CRUD و عملیات مستقل مناسب‌تر است. WebSocket بیشتر برای ارتباط زنده و دوطرفه استفاده می‌شود.

آیا WebSocket همیشه سریع‌تر از HTTP است؟

خیر. برای Requestهای کم و مستقل، HTTP می‌تواند ساده‌تر و کاملاً مناسب باشد. مزیت WebSocket در Connection پایدار و انتقال سریع Messageهای متوالی است.

آیا WebSocket برای چت‌بات هوش مصنوعی مناسب است؟

بله، مخصوصاً اگر به Streaming، Cancel، Presence یا Eventهای دوطرفه نیاز دارید. اگر فقط پاسخ مدل از Server به Client Stream می‌شود، SSE نیز گزینه مناسبی است.

آیا می‌توان WebSocket را با FastAPI ساخت؟

بله. FastAPI از WebSocket Endpoint، دریافت Text یا JSON و ارسال Message پشتیبانی می‌کند.

آیا می‌توان کلید API درواره را در JavaScript قرار داد؟

خیر. کلید API باید فقط در Backend نگهداری شود. Browser از طریق WebSocket به Backend شما متصل می‌شود و Backend API درواره را فراخوانی می‌کند.

آیا WebSocket بعد از Refresh صفحه باقی می‌ماند؟

خیر. Refresh باعث بسته‌شدن Connection فعلی می‌شود و صفحه جدید باید Connection تازه ایجاد کند.

آیا WebSocket به‌صورت خودکار Reconnect می‌شود؟

WebSocket استاندارد Browser به‌طور خودکار Reconnect نمی‌کند. برنامه Client باید منطق Reconnect را پیاده‌سازی کند.

چگونه WebSocket را روی چند Server اجرا کنیم؟

Load Balancer باید WebSocket را پشتیبانی کند. State مشترک در Database یا Redis نگهداری می‌شود و برای Broadcast میان Instanceها می‌توان از Redis Pub/Sub یا Message Broker استفاده کرد.

قیمت استفاده از مدل هوش مصنوعی چقدر است؟

هزینه به مدل، تعداد Token ورودی و خروجی و تعداد Generationها بستگی دارد. قیمت به‌روز و شناسه مدل‌ها در صفحه مدل‌های درواره قرار دارد.

جمع‌بندی

WebSocket یک راهکار قدرتمند برای ارتباط دائمی، دوطرفه و Real-Time میان Client و Server است. این Protocol برای چت، Notification، بازی آنلاین، داشبورد زنده و Streaming تعاملی مناسب است؛ اما جایگزین کامل HTTP و REST API محسوب نمی‌شود.

در پروژه عملی این مقاله، یک چت Real-Time با JavaScript و FastAPI ساختیم. مرورگر از طریق WebSocket به Backend متصل می‌شود، FastAPI درخواست را به API درواره ارسال می‌کند و پاسخ مدل را به‌صورت Streaming به Client برمی‌گرداند.

همچنین ساختار Message، Ack، Token، Done، Error، Heartbeat، Reconnect، محدودیت ورودی، تاریخچه گفتگو و اجرای Docker را پیاده‌سازی کردیم.

برای تبدیل این نمونه به سیستم Production باید موارد زیر تکمیل شوند:

  • احراز هویت
  • ذخیره پایدار گفتگو
  • Rate Limit
  • مدیریت چند Instance
  • Redis Pub/Sub
  • Cancellation
  • Idempotency
  • Observability
  • Backpressure
  • Graceful Shutdown
  • تست Load

اگر می‌خواهید یک چت‌بات یا قابلیت Real-Time مبتنی بر مدل‌های هوش مصنوعی بسازید، در درواره ثبت‌نام کنید، کلید API بگیرید و مدل مناسب پروژه را از صفحه مدل‌ها انتخاب کنید.

منابع پیشنهادی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.