مجله ایرانیکاسرورآموزش سرور، هاست، وردپرس و شبکه
آموزشی

راه‌اندازی صحیح SSE و WebSocket پشت Nginx + رفع قطع اتصال

اگر SSE یا WebSocket پشت Nginx قطع می‌شود، پیام‌ها با تأخیر می‌رسند، اتصال بعد از حدود یک دقیقه بسته می‌شود یا WebSocket با خطای Upgrade مواجه است، معمولاً مشکل از این است که Nginx…

راه‌اندازی صحیح SSE و WebSocket پشت Nginx + رفع قطع اتصال
خلاصه سریع این آموزش

اگر SSE یا WebSocket پشت Nginx قطع می‌شود، پیام‌ها با تأخیر می‌رسند، اتصال بعد از حدود یک دقیقه بسته می‌شود یا WebSocket با خطای Upgrade مواجه است، معمولاً مشکل از…

۱۲ دقیقه مطالعه ۰ دیدگاه ۱۳ مهر ۱۴۰۵ آخرین به‌روزرسانی

اگر SSE یا WebSocket پشت Nginx قطع می‌شود، پیام‌ها با تأخیر می‌رسند، اتصال بعد از حدود یک دقیقه بسته می‌شود یا WebSocket با خطای Upgrade مواجه است، معمولاً مشکل از این است که Nginx این دو نوع ارتباط را مثل یک Request عادی HTTP پردازش می‌کند. برای WebSocket باید Headerهای Upgrade به Upstream منتقل شوند؛ برای SSE نیز باید Buffering غیرفعال شود تا Eventها همان لحظه به Client برسند.

راه‌حل کوتاه: برای WebSocket از proxy_set_header Upgrade $http_upgrade و proxy_set_header Connection $connection_upgrade استفاده کنید و Timeout خواندن را متناسب با اتصال طولانی تنظیم کنید. برای SSE، پاسخ Backend باید Content-Type: text/event-stream داشته باشد، proxy_buffering off باشد و Backend به‌صورت دوره‌ای Heartbeat ارسال کند تا Connection بی‌دلیل Idle نشود.

فهرست مطالب

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

هر دو برای ارتباط بلادرنگ استفاده می‌شوند، اما رفتارشان متفاوت است.

  • SSE: ارتباط یک‌طرفه از Server به Client است و معمولاً با EventSource در مرورگر استفاده می‌شود.
  • WebSocket: ارتباط دوطرفه و Full-duplex است و Client و Server هر دو می‌توانند در هر لحظه پیام بفرستند.

SSE روی HTTP معمولی کار می‌کند و پاسخ آن از نوع زیر است:

Content-Type: text/event-stream

اما WebSocket با یک HTTP Request شروع می‌شود و سپس Connection با مکانیزم Upgrade به WebSocket تغییر می‌کند.

معماری معمول پشت Nginx

Browser / App
     ↓
   Nginx
     ↓
Node.js / FastAPI / Go / Laravel / Docker
     ↓
SSE or WebSocket Backend

اگر Nginx تنظیم مناسب نداشته باشد، ممکن است:

  • WebSocket Upgrade انجام نشود.
  • SSE داخل Buffer بماند و Eventها دیر برسند.
  • Connection بعد از Timeout بسته شود.
  • Backend سالم باشد اما Client مرتب Reconnect کند.

تنظیم صحیح WebSocket پشت Nginx

برای WebSocket، Headerهای Upgrade باید صریحاً به Backend منتقل شوند.

در Block http بهتر است این Map را تعریف کنید:

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

سپس داخل Location مربوط به WebSocket:

location /ws/ {
    proxy_pass http://127.0.0.1:3000;

    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 3600s;
    proxy_send_timeout 3600s;
}

در نسخه‌های قدیمی‌تر Nginx معمولاً این خط نیز ضروری بود:

proxy_http_version 1.1;

در نسخه‌های جدید Nginx، HTTP/1.1 برای Proxying به‌صورت پیش‌فرض استفاده می‌شود، اما اگر سرور قدیمی دارید، همچنان بهتر است آن را صریحاً بررسی کنید.

چرا برای Connection از map استفاده کنیم؟

می‌توان مستقیم نوشت:

proxy_set_header Connection "upgrade";

اما روش Map تمیزتر است؛ چون اگر Client اصلاً Upgrade نفرستاده باشد، Connection به‌شکل معمولی close مدیریت می‌شود.

چرا WebSocket بعد از حدود ۶۰ ثانیه قطع می‌شود؟

Nginx برای خواندن از Upstream Timeout دارد. اگر Backend برای مدت مشخص هیچ داده‌ای نفرستد، Connection می‌تواند بسته شود.

برای WebSocketهای طولانی:

proxy_read_timeout 3600s;

اما افزایش Timeout تنها راه‌حل نیست. بهتر است Backend به‌صورت دوره‌ای Ping/Pong ارسال کند تا Connection زنده بماند و قطع‌شدن واقعی Client هم تشخیص داده شود.

تست WebSocket پشت Nginx

اگر ابزار wscat نصب دارید:

wscat -c wss://example.com/ws/

یا با websocat:

websocat wss://example.com/ws/

اگر Direct Backend کار می‌کند اما پشت Nginx Fail می‌شود، تنظیم Upgrade و Timeout را بررسی کنید.

تست مستقیم Backend WebSocket

مثلاً اگر Backend روی Port 3000 است:

wscat -c ws://127.0.0.1:3000/ws/

اگر Direct Connection هم Fail است، مشکل از Nginx نیست.

تنظیم صحیح SSE پشت Nginx

SSE برخلاف WebSocket نیازی به Upgrade ندارد. مشکل اصلی معمولاً Buffering است.

نمونه Location:

location /events/ {
    proxy_pass http://127.0.0.1:3000;

    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_buffering off;
    proxy_cache off;

    proxy_read_timeout 3600s;
}

Directive مهم:

proxy_buffering off;

اگر Buffering روشن باشد، Nginx ممکن است چند Event را جمع کند و بعد یک‌جا برای Client بفرستد؛ در نتیجه SSE دیگر حالت Real-time واقعی ندارد.

استفاده از X-Accel-Buffering برای SSE

Backend می‌تواند Header زیر را برگرداند:

X-Accel-Buffering: no

Nginx این Header را می‌شناسد و می‌تواند Buffering پاسخ را غیرفعال کند.

برای SSE بهتر است علاوه بر تنظیم Nginx، Backend نیز این Header را در Response ارسال کند.

Headerهای صحیح SSE

Backend SSE معمولاً باید چیزی شبیه این برگرداند:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

هسته اصلی پروتکل SSE همان text/event-stream است.

فرمت صحیح Event در SSE

هر Event باید با یک خط خالی تمام شود.

مثال:

event: message
data: {"status":"ok"}

یا ساده‌تر:

data: hello

اگر newlineهای انتهایی درست نباشند، مرورگر ممکن است Event را Dispatch نکند.

برای SSE Heartbeat بفرستید

اگر Stream برای مدت طولانی هیچ Event واقعی ندارد، Connection ممکن است در Nginx، Load Balancer، CDN یا NAT مسیر Idle تشخیص داده شود.

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

: ping

خطی که با : شروع می‌شود Comment است و EventSource آن را به Application تحویل نمی‌دهد، اما Connection فعال می‌ماند.

مثلاً هر ۱۵ تا ۳۰ ثانیه یک Heartbeat منطقی است.

تست SSE با curl

برای اینکه curl پاسخ را Buffer نکند از -N استفاده کنید:

curl -N https://example.com/events/

اگر Stream درست باشد، Eventها باید همان لحظه ظاهر شوند.

برای دیدن Headerها:

curl -i -N https://example.com/events/

SSE را بدون Nginx تست کنید

مثلاً:

curl -N http://127.0.0.1:3000/events/

اگر Direct Backend Eventها را لحظه‌ای می‌فرستد اما از دامنه Nginx Eventها دسته‌ای می‌رسند، احتمال زیاد Buffering در Proxy فعال است.

نمونه ساده SSE در Node.js

app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('X-Accel-Buffering', 'no');

    res.flushHeaders();

    const timer = setInterval(() => {
        res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
    }, 5000);

    const heartbeat = setInterval(() => {
        res.write(': ping\n\n');
    }, 20000);

    req.on('close', () => {
        clearInterval(timer);
        clearInterval(heartbeat);
    });
});

نکته مهم این است که هنگام بسته‌شدن Connection، Timerها پاک شوند تا Memory Leak ایجاد نشود.

نمونه ساده SSE در PHP

<?php

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');

while (true) {
    echo "data: " . json_encode([
        'time' => time(),
    ]) . "\n\n";

    @ob_flush();
    flush();

    sleep(5);
}

در PHP-FPM باید محدودیت‌های Execution Time و Worker Capacity را نیز در نظر بگیرید؛ چون هر SSE Connection می‌تواند یک Worker را برای مدت طولانی درگیر نگه دارد.

SSE روی PHP-FPM چه مشکلی دارد؟

اگر PHP-FPM برای هر SSE Connection یک Worker نگه دارد و تعداد Connectionها زیاد شود، ممکن است Pool سریعاً پر شود.

پارامتر مهم:

pm.max_children

اگر ۵۰ Connection SSE هم‌زمان داشته باشید و Pool فقط ۲۰ Worker داشته باشد، Requestهای عادی سایت نیز ممکن است پشت صف بمانند.

برای تعداد Connection بالا، Backendهای Event-driven مثل Node.js، Go یا Async Python معمولاً انتخاب مناسب‌تری هستند.

نمونه Nginx برای Node.js WebSocket

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

server {
    listen 443 ssl;
    server_name example.com;

    location /ws/ {
        proxy_pass http://127.0.0.1:3000;

        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 3600s;
        proxy_send_timeout 3600s;
    }
}

اگر هم SSE و هم WebSocket روی یک دامنه دارید

بهتر است Locationهای جدا داشته باشید:

location /events/ {
    proxy_pass http://127.0.0.1:3000;

    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;

    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;
}

location /ws/ {
    proxy_pass http://127.0.0.1:3000;

    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 3600s;
}

proxy_buffering را برای کل سایت خاموش نکنید

اگر فقط SSE نیاز به Streaming دارد، بهتر است proxy_buffering off فقط روی همان Location اعمال شود.

Buffering برای Responseهای عادی می‌تواند مفید باشد؛ غیرفعال کردن سراسری آن ممکن است Performance و رفتار Nginx را تغییر دهد.

اگر جلوی Nginx CDN یا Proxy دیگری دارید

مسیر ممکن است این‌طور باشد:

Client
 ↓
CDN / Cloud Proxy
 ↓
Nginx
 ↓
Backend

در این حالت فقط Nginx تعیین‌کننده نیست. لایه بالادستی نیز ممکن است:

  • Idle Timeout داشته باشد.
  • WebSocket را محدود کند.
  • SSE را Buffer کند.
  • Connection طولانی را قطع کند.

اگر اتصال مستقیم Origin پایدار است ولی از طریق CDN قطع می‌شود، Timeout و Streaming Policy همان CDN را بررسی کنید.

SSE و HTTP/2

SSE با HTTP/2 نیز کار می‌کند. یکی از مزیت‌های مهم این است که محدودیت Connectionهای هم‌زمان مرورگر که در HTTP/1.x پایین است، در HTTP/2 به‌صورت Streamهای Multiplex شده مدیریت می‌شود.

اگر Application چند SSE Connection هم‌زمان باز می‌کند، HTTP/2 معمولاً رفتار بهتری نسبت به چند Connection مستقل HTTP/1.1 دارد.

WebSocket و HTTP/2 را با Proxying Upstream اشتباه نگیرید

ممکن است Client با HTTP/2 به Nginx وصل شود، اما ارتباط Nginx با Backend برای WebSocket همچنان با مکانیزم مخصوص Upgrade مدیریت شود.

برای عیب‌یابی روی Headerهای Upgrade و رفتار Upstream تمرکز کنید، نه فقط Protocol سمت Client.

Timeout مناسب چقدر باشد؟

عدد ثابت برای همه پروژه‌ها وجود ندارد.

برای مثال:

# اتصال‌های طولانی
proxy_read_timeout 3600s;

# همراه با heartbeat سمت backend
# هر 15 تا 30 ثانیه

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

SSE و WebSocket در Docker

اگر Nginx و Backend داخل Docker Compose هستند، بهتر است Nginx از نام Service استفاده کند:

proxy_pass http://app:3000;

نمونه Compose:

services:
  nginx:
    image: nginx:latest
    depends_on:
      - app

  app:
    image: my-app:latest
    expose:
      - "3000"

از داخل شبکه Docker، Nginx می‌تواند Service را با نام app Resolve کند.

تست Backend از داخل Container Nginx

اگر ابزار لازم داخل Image موجود باشد:

docker compose exec nginx \
  curl -N http://app:3000/events/

برای WebSocket نیز از یک Debug Container دارای wscat یا websocat استفاده کنید.

لاگ Nginx را هنگام قطع Connection ببینید

sudo tail -f /var/log/nginx/error.log

برای systemd:

sudo journalctl -u nginx -f

و هم‌زمان لاگ Backend را هم باز کنید.

خطاهای رایج WebSocket

اگر Handshake درست انجام نشود ممکن است ببینید:

400 Bad Request
426 Upgrade Required
502 Bad Gateway
WebSocket connection failed

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

  • Header Upgrade
  • Header Connection
  • مسیر درست /ws/
  • Port واقعی Backend
  • TLS و استفاده صحیح از wss://

مشکلات رایج SSE

اگر SSE متصل است ولی Eventها دیر می‌آیند، معمولاً Buffering مقصر است.

اگر Connection مرتب Reconnect می‌شود:

  • proxy_read_timeout را بررسی کنید.
  • Heartbeat بفرستید.
  • Timeout CDN را بررسی کنید.
  • Crash یا Restart Backend را بررسی کنید.

اگر Browser Event دریافت نمی‌کند:

  • Content-Type: text/event-stream را بررسی کنید.
  • هر Event را با دو newline تمام کنید.
  • Response را Flush کنید.
  • Buffering را خاموش کنید.

بعد از تغییر Configuration

همیشه قبل از Reload:

sudo nginx -t

اگر موفق بود:

sudo systemctl reload nginx

نمونه کامل Nginx برای SSE و WebSocket

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

server {
    listen 443 ssl;
    server_name example.com;

    location /events/ {
        proxy_pass http://127.0.0.1:3000;

        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_buffering off;
        proxy_cache off;

        proxy_read_timeout 3600s;
    }

    location /ws/ {
        proxy_pass http://127.0.0.1:3000;

        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 3600s;
        proxy_send_timeout 3600s;
    }
}

ترتیب پیشنهادی عیب‌یابی

  1. Backend را بدون Nginx تست کنید.
  2. برای SSE از curl -N استفاده کنید.
  3. برای WebSocket از wscat یا websocat استفاده کنید.
  4. اگر Direct Backend سالم است، Nginx را بررسی کنید.
  5. برای SSE، proxy_buffering off و Headerهای Stream را بررسی کنید.
  6. برای WebSocket، Upgrade و Connection Headerها را بررسی کنید.
  7. Timeout و Heartbeat را هماهنگ کنید.
  8. لاگ Nginx و Backend را هم‌زمان بخوانید.
  9. اگر CDN دارید، اتصال مستقیم Origin را با مسیر CDN مقایسه کنید.
  10. بعد از هر تغییر nginx -t و Reload انجام دهید.

اجرای سرویس‌های Real-time روی VPS

اگر برای WebSocket، SSE، API بلادرنگ، Notification Server یا سرویس‌های Docker به زیرساخت لینوکسی نیاز دارید، می‌توانید پلن‌های خرید سرور مجازی ایران ایرانیکاسرور را بررسی کنید. برای Connectionهای طولانی، علاوه بر CPU و RAM، پایداری شبکه و تنظیم درست Reverse Proxy اهمیت زیادی دارد.

تماس با پشتیبانی ایرانیکاسرور: 021-91302467

Rollback تنظیمات

قبل از تغییر فایل Nginx بکاپ بگیرید:

sudo cp /etc/nginx/sites-available/example.com \
  /etc/nginx/sites-available/example.com.backup

برای بازگردانی:

sudo cp /etc/nginx/sites-available/example.com.backup \
  /etc/nginx/sites-available/example.com

sudo nginx -t
sudo systemctl reload nginx

اشتباه‌های رایج

  • استفاده از تنظیم WebSocket برای SSE؛
  • فراموش کردن proxy_buffering off برای SSE؛
  • فراموش کردن Headerهای Upgrade برای WebSocket؛
  • قرار دادن Timeout بسیار بزرگ بدون Heartbeat؛
  • خاموش کردن Buffering برای کل سایت؛
  • نادیده گرفتن Timeout CDN یا Load Balancer؛
  • تست نکردن مستقیم Backend؛
  • عدم Cleanup Connectionهای SSE در Backend؛
  • استفاده از PHP-FPM با هزاران SSE Connection بدون محاسبه Worker Capacity؛
  • Reload کردن Nginx بدون اجرای nginx -t.

سؤالات متداول

برای WebSocket پشت Nginx چه Headerهایی لازم است؟

باید Headerهای Upgrade و Connection به Upstream منتقل شوند. استفاده از map برای مقدار Connection روش تمیزتری است.

چرا SSE پشت Nginx با تأخیر پیام می‌فرستد؟

دلیل رایج فعال بودن Response Buffering است. روی Location مربوط به SSE معمولاً باید proxy_buffering off تنظیم شود و Backend نیز می‌تواند X-Accel-Buffering: no ارسال کند.

چرا WebSocket بعد از حدود ۶۰ ثانیه قطع می‌شود؟

اگر Upstream برای مدت طولانی داده‌ای نفرستد، proxy_read_timeout می‌تواند باعث بسته‌شدن Connection شود. افزایش منطقی Timeout و استفاده از Ping/Pong سمت Backend راه‌حل مناسب‌تری است.

برای SSE چگونه Connection را زنده نگه داریم؟

می‌توانید هر چند ثانیه یک Comment مانند : ping ارسال کنید. این داده توسط EventSource به Application تحویل داده نمی‌شود اما Traffic روی Connection ایجاد می‌کند.

آیا SSE به Upgrade Header نیاز دارد؟

خیر. SSE یک HTTP Response طولانی با Content-Type برابر text/event-stream است و مانند WebSocket از Protocol Upgrade استفاده نمی‌کند.

آیا SSE دوطرفه است؟

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

چرا SSE با چند Tab مرورگر مشکل پیدا می‌کند؟

در HTTP/1.x مرورگرها تعداد Connectionهای هم‌زمان به یک Origin را محدود می‌کنند. در HTTP/2 چند Stream می‌توانند روی یک Connection مدیریت شوند و این محدودیت کمتر آزاردهنده است.

آیا proxy_buffering off برای WebSocket هم لازم است؟

معمولاً خیر. مسئله اصلی WebSocket مکانیزم Upgrade و Timeout Connection است. proxy_buffering off بیشتر برای Streaming HTTP مثل SSE اهمیت دارد.

جمع‌بندی: SSE و WebSocket را پشت Nginx با یک نسخه Configuration یکسان مدیریت نکنید. برای WebSocket، Upgrade Headerها و Timeout اتصال مهم‌اند؛ برای SSE، Buffering باید خاموش شود و Stream باید با text/event-stream و Heartbeat منظم ارسال شود. بهترین روش عیب‌یابی این است که اول Backend را مستقیم تست کنید و بعد تفاوت رفتار پشت Nginx را بررسی کنید.

اشتراک‌گذاری مقاله
گفت‌وگو درباره آموزش

دیدگاه‌ها (۰)

هنوز دیدگاهی ثبت نشده است.

دیدگاه یا سؤال خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *