📝 مقاله

WebSocket – معماری، مقیاس‌پذیری و پیاده‌سازی عملی (راهنمای فوق‌تخصصی)

۱۶ اوت ۲۰۲۶ شبکه | برنامه‌نویسی | پروتکل‌ها | معماری سیستم ۶۰ دقیقه مطالعه

WebSocket – معماری، مقیاس‌پذیری و پیاده‌سازی عملی

در دنیای مدرن وب، کاربران دیگر پذیرای صفحه‌هایی نیستند که برای دریافت داده‌های جدید باید دکمه رفرش را بزنند. از بازارهای مالی و پلتفرم‌های معاملاتی گرفته تا بازی‌های آنلاین، چت‌های گروهی و داشبوردهای لحظه‌ای، همگی به یک پروتکل ارتباطی نیاز دارند که دوطرفه (Full-Duplex)، کم‌تأخیر (Low-Latency) و سبک (Lightweight) باشد. این پروتکل چیزی نیست جز WebSocket.

در این مقاله، قصد داریم فراتر از یک آموزش مقدماتی برویم و WebSocket را از زاویه یک معمار سیستم بررسی کنیم. با ما همراه باشید تا از نحوه دست دادن (Handshake) تا مدیریت هزاران اتصال همزمان در یک کلاستر توزیع‌شده را گام به گام پیش برویم.

۱. چرا WebSocket؟ ناکارآمدی HTTP در ارتباطات بلادرنگ

پروتکل HTTP بر اساس درخواست-پاسخ (Request-Response) طراحی شده است. کلاینت درخواست می‌دهد و سرور پاسخ می‌دهد. برای دریافت داده‌های جدید، کلاینت مجبور است مدام درخواست ارسال کند که به روش‌های زیر انجام می‌شد:

  • Polling (درخواست دوره‌ای): کلاینت هر چند ثانیه یکبار درخواست GET می‌فرستد. این روش باعث هدررفت پهنای باند و تأخیر بالا می‌شود.
  • Long-Polling: کلاینت درخواست می‌دهد و سرور تا زمانی که داده جدیدی وجود داشته باشد، پاسخ را معطل (Hold) می‌کند. اگرچه بهتر از Polling است، اما همچنان سربار هدرهای HTTP را دارد و مدیریت همزمانی (Concurrency) روی سرور را دشوار می‌کند.
  • Server-Sent Events (SSE): ارتباط یک‌طرفه از سرور به کلاینت است و کلاینت نمی‌تواند داده‌ای به سمت سرور بفرستد (مگر با درخواست جداگانه).

پروتکل WebSocket در سال ۲۰۱۱ با استاندارد RFC 6455 معرفی شد و این مشکل را به صورت بنیادین حل کرد. WebSocket پس از یک Handshake اولیه (که روی پورت ۸۰ یا ۴۴۳ انجام می‌شود)، یک کانال ارتباطی دائمی و دوطرفه ایجاد می‌کند که تا زمانی که یکی از دو طرف آن را نبندد، باز می‌ماند.

ویژگی HTTP (Polling) HTTP Long-Polling WebSocket
جهت ارتباط یک‌طرفه (کلاینت -> سرور) عمدتاً یک‌طرفه با مکث دوطرفه (Full-Duplex)
سربار هدر زیاد (هر بار ~۸۰۰ بایت) زیاد بسیار کم (~۲ بایت پس از Handshake)
تأخیر (Latency) بالا متوسط بسیار پایین (میلی‌ثانیه)
پشتیبانی از Proxy عالی عالی نیازمند پشتیبانی از Upgrade (اکثر Proxyها)

۲. عملیات Handshake (دست دادن)؛ چگونه یک اتصال HTTP به WebSocket تبدیل می‌شود؟

همه چیز با یک درخواست HTTP GET آغاز می‌شود که هدرهای خاصی دارد. این درخواست به سرور می‌گوید: "من می‌خواهم این ارتباط را به پروتکل WebSocket ارتقا دهم."

درخواست (Request) از سمت کلاینت:

GET /chat HTTP/1.1
Host: server.mahmoudi.me
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://mahmoudi.me
  • Upgrade و Connection: قلب درخواست که به سرور می‌گوید پروتکل را عوض کن.
  • Sec-WebSocket-Key: یک کلید ۱۶ بایتی رمزگذاری‌شده با Base64. این کلید برای جلوگیری از کش شدن درخواست توسط Proxyها استفاده می‌شود.
  • Sec-WebSocket-Version: نسخه پروتکل (عدد ۱۳ معیار اصلی است).

پاسخ (Response) از سمت سرور (در صورت موفقیت):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

کد وضعیت ۱۰۱ (Switching Protocols) به معنای موفقیت‌آمیز بودن تغییر پروتکل است. مقدار Sec-WebSocket-Accept با استفاده از الگوریتم خاصی از روی کلید درخواست تولید می‌شود تا امنیت ارتباط تضمین شود. از این لحظه به بعد، پروتکل از HTTP به WebSocket (باینری) تغییر می‌کند و دیگر خبری از هدرهای متنی نیست.

۳. قلب پروتکل: ساختار Frame (قاب) در WebSocket

داده‌ها در WebSocket به صورت Frame (قاب) ارسال می‌شوند. درک این ساختار برای عیب‌یابی و بهینه‌سازی بسیار حیاتی است. هر Frame از یک هدر و یک Payload تشکیل شده است.

ساختار یک Frame به صورت زیر است (به بیت‌ها توجه کنید):

  • FIN (۱ بیت): نشان‌دهنده آخرین قطعه از یک پیام است (چون پیام‌ها می‌توانند قطعه‌قطعه ارسال شوند).
  • RSV1, RSV2, RSV3 (هرکدام ۱ بیت): برای توسعه‌های آینده یا Extensions (مثلاً فشرده‌سازی) استفاده می‌شوند.
  • Opcode (۴ بیت): نوع داده را مشخص می‌کند. مقادیر مهم: 0x1 (Text)، 0x2 (Binary)، 0x8 (Close)، 0x9 (Ping) و 0xA (Pong).
  • Mask (۱ بیت): مشخص می‌کند که آیا Payload ماسک (XOR) شده است یا خیر. طبق استاندارد، همیشه از سمت کلاینت به سرور ماسک می‌خورد (Mask=1) و از سرور به کلاینت بدون ماسک است (Mask=0).
  • Payload Length (۷ بیت، یا ۱۶ بیت یا ۶۴ بیت): طول داده را مشخص می‌کند. اگر مقدار بین ۰ تا ۱۲۵ باشد، همان طول است. اگر ۱۲۶ باشد، ۲ بایت بعدی طول را مشخص می‌کنند. اگر ۱۲۷ باشد، ۸ بایت بعدی طول را مشخص می‌کنند.
  • Masking-key (۰ یا ۴ بایت): اگر بیت Mask=1 باشد، یک کلید ۴ بایتی وجود دارد که برای unmask کردن داده استفاده می‌شود.
  • Payload Data: خود داده اصلی.

این طراحی باعث می‌شود WebSocket فوق‌العاده سبک باشد و بتواند داده‌های باینری (عکس، صدا، پروتوباف) را بدون نیاز به رمزگذاری Base64 (که حجم را افزایش می‌دهد) به طور خام ارسال کند.

۴. چالش مقیاس‌پذیری (Scaling)؛ مدیریت هزاران اتصال همزمان

بزرگترین چالش در WebSocket، برخلاف HTTP که Stateless (بی‌حالت) است، Stateful (حالت‌دار) بودن آن است. هر کلاینت یک اتصال طولانی به یک نمونه (Instance) مشخص از سرور دارد. حال اگر سیستم شما پشت یک Load Balancer اجرا شود، با مشکل زیر روبرو می‌شوید:

مشکل نشست‌های چسبنده (Sticky Sessions):

اگر کلاینت به سرور شماره ۱ متصل شود و سپس درخواست بعدی به سرور شماره ۲ برود، سرور شماره ۲ هیچ اطلاعی از این اتصال ندارد و ارتباط قطع می‌شود. برای حل این مشکل، سه راهکار اصلی وجود دارد:

راهکار اول: Sticky Session در لودبالانسر (روش سنتی)

در این روش، لودبالانسر (مثل Nginx یا HAProxy) بر اساس آدرس IP کلاینت یا یک کوکی، تمام درخواست‌های آن کلاینت را به یک سرور مشخص هدایت می‌کند.

مثال تنظیمات در Nginx:

upstream websocket_cluster {
    ip_hash;  # این دستور باعث چسبندگی بر اساس IP می‌شود
    server backend1.mahmoudi.me:8080;
    server backend2.mahmoudi.me:8080;
}

server {
    location /ws/ {
        proxy_pass http://websocket_cluster;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 60s; # برای جلوگیری از بسته شدن اتصال توسط Proxy
    }
}

معایب: اگر یک سرور از کار بیفتد (Failover)، تمام اتصالات آن قطع می‌شوند و کاربر مجبور به reconnect می‌شود.

راهکار دوم: معماری Shared-Nothing با Redis Pub/Sub (روش مدرن)

در این روش پیشرفته، لودبالانسر به صورت Round-Robin (چرخشی) درخواست‌ها را توزیع می‌کند. اما همه سرورها به یک مرکز داده مشترک (مانند Redis) متصل هستند. وقتی سرور شماره ۱ پیامی دریافت می‌کند، آن را در یک کانال Redis منتشر (Publish) می‌کند. تمام سرورهای دیگر (شماره ۲، ۳، ...) به آن کانال گوش می‌دهند (Subscribe) و اگر کلاینت مورد نظر روی خودشان متصل باشد، پیام را برای او ارسال می‌کنند.

معماری توزیع‌شده با Redis (روش استاندارد ۲۰۲۶):

  1. اتصال WebSocket روی هر سرور برقرار می‌شود و یک شناسه یکتا (مثلاً client_id) به آن اختصاص می‌یابد.
  2. موقعیت (Instance ID) هر کلاینت در Redis ذخیره می‌شود (یا در یک جدول دیتابیس).
  3. هنگام ارسال پیام، ابتدا بررسی می‌شود که کلاینت روی کدام سرور است. اگر روی سرور فعلی است، مستقیم ارسال می‌شود؛ در غیر این صورت، پیام با استفاده از Redis Pub/Sub به سرور مقصد ارسال می‌شود.

۵. امنیت در WebSocket (WSS، احراز هویت و جلوگیری از حملات)

امنیت در WebSocket اغلب نادیده گرفته می‌شود. نکات حیاتی امنیتی عبارتند از:

  • استفاده از WSS به جای WS: درست مانند HTTPS، از TLS (لایه امنیتی) استفاده کنید. WSS روی پورت ۴۴۳ کار می‌کند و ترافیک را رمزنگاری می‌کند. هرگز در محیط تولید از WS (پورت ۸۰) استفاده نکنید.
  • بررسی Origin (دامنه مبدأ): سرور شما باید هدر Origin را در زمان Handshake بررسی کند و فقط دامنه‌های مجاز را بپذیرد. این کار از حملات CSWSH (Cross-Site WebSocket Hijacking) جلوگیری می‌کند.
  • احراز هویت (Authentication)؛ چالش بزرگ: کوکی‌های HTTP به‌طور پیش‌فرض با درخواست WebSocket ارسال نمی‌شوند (مگر اینکه با `withCredentials` تنظیم شوند). بهترین روش‌ها:
    - **توکن در هدر `Sec-WebSocket-Protocol`:** سمت کلاینت می‌تواند یک زیرپروتکل سفارشی مانند `myapp.auth` تعریف کند و توکن JWT را در آن قرار دهد. این روش امن‌ترین روش است.
    - **توکن در Query String (مثل `?token=xxx`) :** این روش ریسک بالایی دارد زیرا توکن در لاگ‌های سرور و Proxy ذخیره می‌شود. فقط در صورتی مجاز است که از WSS استفاده کنید و لاگ‌ها را به‌دقت مدیریت کنید.
  • محدودیت نرخ (Rate Limiting) و تعداد اتصالات: مهاجم می‌تواند با باز کردن هزاران اتصال، سرور شما را با مشکل منابع (File Descriptor) مواجه کند. حتماً برای هر IP حداکثر تعداد اتصال همزمان (مثلاً ۵۰ عدد) تعیین کنید.

۶. پیاده‌سازی عملی با Node.js (کتابخانه ws)

محبوب‌ترین و سریع‌ترین کتابخانه برای WebSocket در اکوسیستم جاوااسکریپت، کتابخانه ws است. در زیر یک سرور قدرتمند با قابلیت Publish/Subscribe ساده شبیه‌سازی شده است.

import { WebSocketServer } from 'ws';
import http from 'http';
import url from 'url';

const server = http.createServer();
const wss = new WebSocketServer({ server });

// نگهداری لیست کلاینت‌ها با شناسه‌های منحصربه‌فرد
const clients = new Map();

wss.on('connection', (ws, req) => {
    const parameters = url.parse(req.url, true).query;
    const token = parameters.token; // دریافت توکن (توصیه نمی‌شود، فقط برای نمونه)
    
    // اعتبارسنجی توکن (ساده‌شده)
    if (!token || token !== 'secret') {
        ws.close(1008, 'Unauthorized');
        return;
    }

    const clientId = Math.random().toString(36).substring(7);
    clients.set(clientId, ws);
    console.log(`Client ${clientId} connected. Total: ${clients.size}`);

    ws.on('message', (data) => {
        // داده را به صورت بافر دریافت می‌کنیم
        const message = data.toString();
        console.log(`Received from ${clientId}: ${message}`);

        // اکو (بازگشت به خود فرستنده)
        ws.send(`Echo: ${message}`);

        // ارسال به سایر کلاینت‌ها (Broadcast)
        clients.forEach((client, id) => {
            if (id !== clientId && client.readyState === 1) { // 1 = OPEN
                client.send(`Client ${clientId} said: ${message}`);
            }
        });
    });

    ws.on('close', () => {
        clients.delete(clientId);
        console.log(`Client ${clientId} disconnected. Total: ${clients.size}`);
    });

    // ارسال Ping دوره‌ای برای بررسی سلامت اتصال (Keep-Alive)
    const pingInterval = setInterval(() => {
        if (ws.readyState === 1) {
            ws.ping();
        }
    }, 30000);

    ws.on('close', () => clearInterval(pingInterval));
});

server.listen(8080, () => {
    console.log('WebSocket Server running on port 8080');
});

۷. پیاده‌سازی با PHP و Workerman (مناسب برای اکوسیستم شما)

از آنجایی که سایت شما با PHP (لاراول) نوشته شده است، کتابخانه Workerman یک انتخاب عالی برای اجرای WebSocket به صورت مستقل از PHP-FPM است. این کتابخانه یک سرور غیرهمگام (Asynchronous) نوشته شده با PHP خالص است.

نصب Workerman از طریق Composer:

composer require workerman/workerman

ایجاد فایل سرور (مثلاً ws-server.php):

<?php
require_once __DIR__ . '/vendor/autoload.php';

use WorkermanWorker;
use WorkermanConnectionTcpConnection;

// ایجاد یک Worker روی پورت ۸۰۸۰
$ws_worker = new Worker('websocket://0.0.0.0:8080');

// تعداد فرایندها (برای بهره‌وری از چند هسته CPU)
$ws_worker->count = 4;

// ذخیره اتصالات (برای Broadcast)
$ws_worker->onConnect = function(TcpConnection $connection) {
    echo "New connection from {$connection->getRemoteIp()}
";
};

$ws_worker->onMessage = function(TcpConnection $connection, $data) {
    // echo دریافت داده
    echo "Received: $data
";

    // ارسال پاسخ به کلاینت
    $connection->send('Server replied: ' . $data);

    // ارسال به همه کلاینت‌های متصل (Broadcast)
    foreach ($connection->worker->connections as $client) {
        if ($client !== $connection) {
            $client->send($data);
        }
    }
};

$ws_worker->onClose = function(TcpConnection $connection) {
    echo "Connection closed
";
};

// اجرای سرور
Worker::runAll();

اجرای سرور:

php ws-server.php start

با این کار، یک سرور WebSocket با پشتیبانی از چندین فرایند (Process) راه‌اندازی می‌شود. نکته مهم این است که برای مقیاس‌پذیری، باید لایه Redis را به Workerman متصل کنید که با کتابخانه workerman/redis امکان‌پذیر است.

۸. سمت کلاینت (مرورگر)؛ مدیریت Reconnect و مکانیزم Retry

اتصالات شبکه همیشه پایدار نیستند. کد زیر یک کلاینت مقاوم با قابلیت تلاش مجدد (Exponential Backoff) را نشان می‌دهد:

class WebSocketClient {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 10;
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);

        this.ws.onopen = () => {
            console.log('Connected to WebSocket');
            this.reconnectAttempts = 0; // ریست کردن شمارنده
            this.ws.send(JSON.stringify({ type: 'auth', token: 'your-jwt-token' }));
        };

        this.ws.onmessage = (event) => {
            // پردازش داده دریافتی (می‌تواند JSON یا باینری باشد)
            console.log('Message from server:', event.data);
        };

        this.ws.onclose = () => {
            console.log('Disconnected. Reconnecting...');
            this.reconnect();
        };

        this.ws.onerror = (error) => {
            console.error('WebSocket Error:', error);
        };
    }

    reconnect() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            console.error('Max reconnect attempts reached. Giving up.');
            return;
        }
        // زمان انتظار نمایی: 1s, 2s, 4s, 8s, ...
        const delay = Math.pow(2, this.reconnectAttempts) * 1000;
        setTimeout(() => {
            this.reconnectAttempts++;
            this.connect();
        }, delay);
    }

    sendMessage(data) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        } else {
            console.warn('Connection is not open. Message queued or dropped.');
        }
    }
}

// استفاده
const client = new WebSocketClient('wss://mahmoudi.me:8080');

۹. بنچمارک و عملکرد؛ تست همزمانی (C10k و فراتر)

ابزارهای قدرتمندی مانند autobahn-testsuite و websocket-bench برای تست عملکرد وجود دارند. بر اساس بنچمارک‌های انجام‌شده در سال ۲۰۲۴، یک سرور Node.js با کتابخانه `ws` می‌تواند به راحتی بیش از ۲۰۰ هزار اتصال همزمان را روی یک ماشین با ۴ هسته CPU مدیریت کند.

مقایسه تأخیر (Latency) در شبکه‌های مختلف:

نوع شبکه HTTP Polling (میانگین) WebSocket (میانگین) درصد بهبود
شبکه محلی (LAN) ~۵۰ میلی‌ثانیه ~۲ میلی‌ثانیه ۹۶٪
اینترنت ۴G/۵G ~۲۰۰ میلی‌ثانیه ~۵۰ میلی‌ثانیه ۷۵٪
ارتباطات بین‌المللی ~۴۰۰ میلی‌ثانیه ~۱۸۰ میلی‌ثانیه ۵۵٪

برای افزایش راندمان، همیشه از فشرده‌سازی (Compression) با استفاده از پسوند permessage-deflate استفاده کنید. این قابلیت حجم پیام‌های متنی بزرگ را تا ۷۰٪ کاهش می‌دهد.

۱۰. زیرپروتکل‌ها (Sub-protocols)؛ STOMP، MQTT و معماری مبتنی بر پیام

WebSocket فقط یک لوله (Tunnel) است. برای سازماندهی پیام‌ها، از زیرپروتکل‌ها استفاده می‌شود:

  • STOMP (Simple Text Oriented Messaging Protocol): بسیار محبوب در اکوسیستم Java (Spring) و ActiveMQ. پیام‌ها دارای هدر و بدنه هستند و امکان اشتراک‌گذاری (Subscribe) بر اساس مقصد (Destination) را فراهم می‌کنند.
  • MQTT (Message Queuing Telemetry Transport): پروتکل فوق‌سبک برای اینترنت اشیا (IoT) که روی WebSocket سوار می‌شود و با معماری Publish/Subscribe کار می‌کند.
  • Socket.IO (فریمورک سطح بالا): اگرچه از WebSocket استفاده می‌کند، اما لایه‌های اضافی مانند Fallback به Long-Polling، اتاقک‌ها (Rooms) و Ack خودکار را فراهم می‌کند.

۱۱. آینده WebSocket؛ ظهور WebTransport

در سال ۲۰۲۶، پروتکل جدید WebTransport به عنوان جانشین بالقوه WebSocket در حال محبوب شدن است. WebTransport بر پایه HTTP/3 و QUIC (UDP) ساخته شده است و مزایای زیر را دارد:

  • ارسال داده به صورت نامرتب (Unordered): مناسب برای جریان‌های ویدیویی که ترتیب بسته‌ها اهمیت چندانی ندارد.
  • پشتیبانی از جریان‌های دوطرفه متعدد (Streams): امکان باز کردن چندین کانال مجزا روی یک اتصال واحد.
  • عدم وجود مشکل Head-of-Line Blocking: که در TCP وجود دارد.

با این حال، WebSocket تا سال‌ها و به دلیل پشتیبانی گسترده از Proxyها و CDNها، همچنان استاندارد طلایی باقی خواهد ماند.

۱۲. نتیجه‌گیری و جمع‌بندی نهایی

WebSocket تنها یک ابزار نیست؛ بلکه یک معماری است. انتخاب درست بین WebSocket، SSE، و WebTransport بر اساس نیاز پروژه (نرخ داده، تأخیر مجاز، نوع کلاینت) انجام می‌شود.

در این مقاله، یاد گرفتید که:

  • چگونه یک اتصال HTTP به WebSocket تبدیل می‌شود.
  • ساختار Frame چیست و چرا WebSocket سبک‌تر از HTTP است.
  • چگونه با استفاده از Nginx و Redis یک کلاستر مقاوم در برابر قطعی طراحی کنیم.
  • چگونه با Node.js و PHP (Workerman) سرورهای عملیاتی بسازیم.
  • مهم‌ترین نکات امنیتی برای جلوگیری از هک و قطعی سرویس.

اگر قصد دارید یک پلتفرم معاملاتی، بازی آنلاین یا چت‌روم با بیش از ۱۰۰ هزار کاربر همزمان طراحی کنید، پیشنهاد می‌کنم از ترکیب Node.js (برای I/O سنگین) + Redis (برای وضعیت اشتراکی) + Nginx (برای لودبالانس) استفاده کنید. این سه‌گانه، استاندارد صنعتی سال ۲۰۲۶ است.

فناوری وب در حال حرکت به سمت بلادرنگ محض است و WebSocket پلی محکم به این آینده است.


نویسنده: محمدمهدی محمودی

تاریخ انتشار: ۱۶ اوت ۲۰۲۶

منابع: مستندات RFC 6455 (IETF)، مستندات Workerman و کتابخانه ws، مقاله معماری Discord (واقعی)، گزارش‌های بنچمارگ Autobahn و وبسایت CanIUse.