WebSocket چیست؟ آموزش ساخت چت Real-Time با Python، FastAPI و API درواره
WebSocket پروتکلی برای ارتباط دائمی و دوطرفه میان Client و Server است. در این آموزش، یک چت Real-Time و Streaming با JavaScript، 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
| ویژگی | HTTP | WebSocket |
|---|---|---|
| مدل ارتباط | 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
| معیار | WebSocket | SSE |
|---|---|---|
| جهت ارتباط | دوطرفه | Server به Client |
| Protocol | مستقل پس از Upgrade | HTTP |
| داده متنی | دارد | دارد |
| داده 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 | بستهشدن عادی |
1001 | Client یا Server در حال خروج است |
1002 | خطای Protocol |
1003 | نوع داده پشتیبانی نمیشود |
1008 | نقض Policy برنامه |
1009 | Message بیشازحد بزرگ است |
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:
- Developer Tools را باز کنید.
- وارد بخش Network شوید.
- فیلتر
WSرا انتخاب کنید. - Connection مربوط به
/ws/chatرا باز کنید. - بخش 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 کوتاهعمر:
- Client با REST و Session معتبر Ticket میگیرد.
- Server Ticket یکبارمصرف و کوتاهعمر میسازد.
- Client با Ticket به WebSocket متصل میشود.
- Server Ticket را اعتبارسنجی و مصرف میکند.
- 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 بگیرید و مدل مناسب پروژه را از صفحه مدلها انتخاب کنید.
منابع پیشنهادی
- RFC 6455؛ استاندارد WebSocket
- WebSocket API در MDN
- آموزش WebSocket در FastAPI
- مستندات JavaScript WebSocket
- مستندات Uvicorn
- مستندات Nginx درباره WebSocket Proxy
مقالات مرتبط
- Streaming API چیست؟ آموزش دریافت پاسخ لحظهای از مدل هوش مصنوعی
- ساخت چتبات هوش مصنوعی با API درواره
- ساخت چتبات هوش مصنوعی با Next.js، React و API درواره
- ساخت API هوش مصنوعی آماده Production
- مانیتورینگ و Observability در سیستمهای هوش مصنوعی
- آموزش اضافهکردن ChatGPT به سایت
- آموزش اتصال API هوش مصنوعی به اپلیکیشن
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.