منابع آموزشی و پژوهشی، برای دانشگاه و مدرسه
پشتیبانیانتشار محتوا
دورهٔ آموزشیرایگان

Node.js تکمیلی؛ رویداد، جریان داده و سرور چت

۸ درس در ۴ فصل۱۰ ساعت و ۱۵ دقیقهدربارهٔ دوره
آزمون، دیباگ و اجرای چتدرس ۸ از ۸

اجرای چت در اوبونتو؛ WSS، پراکسی و عیب‌یابی نهایی

۹۰ دقیقه آموزش متنی
در این درس ۶ بخش

تا اینجا چت روی دستگاه خودمان کار کرده است. برای مشاهده از یک دستگاه دیگر، نسخهٔ آموزشی را پشت Nginx و HTTPS قرار می‌دهیم؛ اتصال زندهٔ مرورگر در این حالت WSS خواهد بود. این درس ادامهٔ مسیر استقرار اوبونتو است و دریافت سرور، SSH و صدور گواهی را از ابتدا تکرار نمی‌کند. یک زیردامنهٔ اختصاصی با گواهی معتبر و Node.js پشتیبانی‌شده لازم دارید. مسیرهای جدید، سرویس و کاربر این پروژه جدا هستند تا نمونهٔ HTTP قبلی با چت مخلوط نشود. این اتاق همچنان مشترک و بدون حساب کاربری است؛ برای نام و پیام واقعی استفاده‌اش نکنید.

نقشهٔ مسیر و پیش‌نیاز قابل بررسی

مرورگر به https://chat.example.com وصل می‌شود، Nginx TLS را پایان می‌دهد و درخواست را به Node روی 127.0.0.1:3001 می‌فرستد. این دامنه نمونه است و باید همه‌جا با زیردامنهٔ خودتان عوض شود. پیش‌نیاز گواهی و تمدید را از درس کامل TLS انجام دهید: https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=7b0cf12c-fec5-46da-9c8e-e62346417ead
برای مسیر Nginx و تفاوت پورت داخلی با عمومی، درس زیر مرجع مستقیم است: https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=9cbb5041-c878-43cc-b361-2eb8ea492cff
اگر DNS یا دسترسی ۴۴۳ هنوز درست نیست، ابتدا همان بخش را کامل کنید؛ تغییر کد چت گواهی نامعتبر را اصلاح نمی‌کند.

در اوبونتوی آموزشی، فایل ZIP را در پوشهٔ تازهٔ darsavand-chat-source زیر خانهٔ کاربر مدیر استخراج کنید؛ package.json باید مستقیم در همان پوشه باشد. node_modules ویندوز را منتقل نکنید؛ وابستگی را روی اوبونتو از lock نصب کنید. فرمان‌های زیر به ترمینال Bash همان سرور تعلق دارند و با کاربر معمولی دارای sudo اجرا می‌شوند. مسیر /opt/node-current/bin/node باید از نصب معتبر دورهٔ پیشین موجود باشد. خروجی نسخه را ببینید و آزمون‌ها را پیش از کپی به مسیر اجرا بگذرانید. اگر پورت ۳۰۰۱ اشغال است، صاحب آن را بررسی کنید و سرویس دیگر را بی‌اجازه متوقف نکنید.

bash
set -euo pipefail
# Extract the project ZIP here first; run as your normal Ubuntu admin user.
cd "$HOME/darsavand-chat-source"
test "$(id -u)" -ne 0
test -f package-lock.json
test -f deploy/chat.service
test -f deploy/chat.nginx.conf
export PATH="/opt/node-current/bin:$PATH"
node --version
npm ci --omit=dev --ignore-scripts
npm test
npm run check
node -e "if (process.versions.node.split('.')[0] < 24) process.exit(1)"
sudo ss -ltnp 'sport = :3001'

در این مسیر، npm با کاربر معمولی اجرا می‌شود و فقط کپی نهایی به مجوز مدیر نیاز دارد. دستور ci نصب مطابق lock را می‌سازد و ignore-scripts از اجرای اسکریپت نصب وابستگی جلوگیری می‌کند؛ وابستگی این نسخه برای کار خود به چنین اسکریپتی نیاز ندارد. #modules-import-export برای دنبال کردن وارد کردن فایل‌های محلی مفید است. تغییر اسم فایل یا حذف package.json می‌تواند نوع ماژول را خراب کند؛ برای رفع خطا، کل پروژهٔ نسخهٔ همراه را نگه دارید و وابستگی را با نصب تصادفی بسته‌های تازه حدس نزنید.

سرویس اختصاصی با دسترسی محدود

فایل کامل deploy/chat.service زیر، هنگام نصب نام darsavand-chat.service می‌گیرد. کاربر chatapp فقط برنامه را اجرا می‌کند؛ فایل‌های کد مالک root دارند و سرویس مجاز به تغییر آن‌ها نیست. تنظیم PORT و PUBLIC_ORIGIN در /etc/darsavand-chat/app.env قرار می‌گیرد؛ مدیر سرویس آن را هنگام شروع می‌خواند و به محیط پردازه می‌دهد. لازم نیست chatapp به فایل خصوصی گواهی دسترسی داشته باشد، چون TLS را Nginx مدیریت می‌کند. زمان توقف سرویس هشت ثانیه است تا مهلت سه‌ثانیه‌ای بستن برنامه فرصت اجرا داشته باشد. این واحد را با واحد پروژهٔ قبلی جایگزین نکنید.

ini
[Unit]
Description=Darsavand small WebSocket teaching chat
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=chatapp
Group=chatapp
WorkingDirectory=/opt/darsavand-chat
EnvironmentFile=/etc/darsavand-chat/app.env
ExecStart=/opt/node-current/bin/node /opt/darsavand-chat/server.mjs
Restart=on-failure
RestartSec=3
TimeoutStopSec=8
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target

PUBLIC_ORIGIN نشانی مبدأ صفحهٔ وب است: https://chat.example.com بدون مسیر و بدون اسلش پایانی. مقدار wss://chat.example.com اشتباه است؛ WSS روش انتقال اتصال زنده است، ولی Origin مرورگر از مبدأ صفحه می‌آید. برنامه مقدار نادرست را هنگام شروع رد می‌کند. اگر پورت عمومی غیراستاندارد دارید، مبدأ باید همان پورت را هم داشته باشد. تغییر فایل محیط روی پردازهٔ در حال اجرا اثر فوری ندارد و پس از اصلاح، سرویس خودتان باید restart شود. مجاز کردن همهٔ مبدأها یا جعل Origin ثابت در پراکسی، راه رفع 403 نیست و کنترل قبلی را بی‌اثر می‌کند.

چرا پراکسی HTTP معمولی کافی نیست؟

آغاز WebSocket با درخواست Upgrade انجام می‌شود. هدرهای Upgrade و Connection مخصوص هر گام ارتباط‌اند و پراکسی آن‌ها را مانند هدر معمولی خودکار عبور نمی‌دهد. در location دقیق /chat، HTTP/1.1 و این دو هدر را صریح می‌فرستیم؛ برای فایل‌ها مسیر HTTP معمولی داریم. proxy_pass بدون مسیر اضافه نوشته شده تا /chat بازنویسی نشود. timeout خواندن این مسیر ۷۵ ثانیه است و از فاصلهٔ معمول heartbeat بیشتر است؛ رسیدن Ping نیز دادهٔ مسیر محسوب می‌شود. این عدد تضمین زنده ماندن شبکه نیست و پاک‌سازی heartbeat همچنان لازم است.

nginx
server {
    listen 80;
    listen [::]:80;
    server_name chat.example.com;

    location / {
        return 308 https://chat.example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name chat.example.com;

    ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 8k;
    access_log /var/log/nginx/darsavand-chat.access.log;
    error_log /var/log/nginx/darsavand-chat.error.log;

    location = /chat {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 3s;
        proxy_read_timeout 75s;
        proxy_send_timeout 15s;
    }

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        proxy_send_timeout 15s;
    }
}

این نسخه برای Nginx اوبونتو، حتی نسخه‌هایی که پیش‌فرض پراکسی‌شان HTTP/1.1 نیست، رفتار صریح دارد. Connection برابر upgrade فقط در مسیر چت تنظیم شده و به همهٔ فایل‌های سایت تحمیل نمی‌شود. Origin ورودی بدون جعل به برنامه می‌رسد. هدر X-Forwarded-For در این طرح تک‌پراکسی از نشانی واقعی اتصال Nginx ساخته می‌شود؛ این نمونه قرارداد زنجیرهٔ چند پراکسی ندارد. گواهی fullchain باید زنجیرهٔ درست داشته باشد و کلید خصوصی با آن جفت باشد. نام پوشهٔ Certbot ممکن است پسوند داشته باشد؛ مسیر واقعی را از sudo certbot certificates بردارید و حدس نزنید.

نصب تازه؛ ابتدا بررسی، سپس فعال‌سازی

بلوک زیر فقط برای نصب تازهٔ همین پروژه است. سه مقدار ابتدای آن را با دامنه و مسیر گواهی خودتان عوض کنید؛ مقدار نمونه عمداً باعث توقف می‌شود. بررسی وجود مسیر یا پیوند نمادین و کاربر قبلی، از بازنویسی ناآگاهانه جلوگیری می‌کند. اگر متوقف شد، دلیل را بخوانید؛ حذف مسیر موجود برای عبور از کنترل، روش ارتقا نیست. قبل از اجرا، در خروجی nginx -T بررسی کنید همین زیردامنه قبلاً در سایت دیگری تعریف نشده باشد. فایل‌های Nginx سایت‌های دیگر و تنظیم SSH را تغییر نمی‌دهیم؛ دسترسی‌های شبکهٔ لازم باید طبق درس قبلی برقرار باشند.

bash
set -euo pipefail
cd "$HOME/darsavand-chat-source"
# Replace all three values with YOUR existing domain and certificate paths.
CHAT_DOMAIN='REPLACE_WITH_YOUR_REAL_DOMAIN'
CHAT_FULLCHAIN='/etc/letsencrypt/live/REPLACE_WITH_YOUR_REAL_DOMAIN/fullchain.pem'
CHAT_KEY='/etc/letsencrypt/live/REPLACE_WITH_YOUR_REAL_DOMAIN/privkey.pem'
if [[ ! "$CHAT_DOMAIN" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] ||
   [[ "$CHAT_DOMAIN" != *.* ]] || [[ "$CHAT_DOMAIN" == *example.com* ]]; then
  echo 'Set a real lower-case DNS hostname first.' >&2; exit 1
fi
for target in /opt/darsavand-chat /etc/darsavand-chat \
  /etc/systemd/system/darsavand-chat.service \
  /etc/nginx/sites-available/darsavand-chat \
  /etc/nginx/sites-enabled/darsavand-chat; do
  if sudo test -e "$target" || sudo test -L "$target"; then
    echo "Existing target: $target — stop and review, do not overwrite." >&2; exit 1
  fi
done
if getent passwd chatapp >/dev/null || getent group chatapp >/dev/null; then
  echo 'chatapp already exists; review its ownership before proceeding.' >&2; exit 1
fi
sudo test -x /opt/node-current/bin/node
sudo test -s "$CHAT_FULLCHAIN"
sudo test -s "$CHAT_KEY"
sudo openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
  -untrusted "$CHAT_FULLCHAIN" -verify_hostname "$CHAT_DOMAIN" \
  -purpose sslserver "$CHAT_FULLCHAIN"
sudo openssl x509 -in "$CHAT_FULLCHAIN" -noout -checkend 86400
CHAT_CERT_PUB=$(sudo openssl x509 -in "$CHAT_FULLCHAIN" -pubkey -noout | \
  openssl pkey -pubin -outform DER | sha256sum)
CHAT_KEY_PUB=$(sudo openssl pkey -in "$CHAT_KEY" -pubout -outform DER | sha256sum)
test "$CHAT_CERT_PUB" = "$CHAT_KEY_PUB"
CHAT_LISTENERS=$(sudo ss -ltnH 'sport = :3001')
if test -n "$CHAT_LISTENERS"; then
  echo 'Port 3001 already has a listener; stop and review its owner.' >&2; exit 1
fi
sudo nginx -t
# Only after the preflight succeeds, create this new project's own files.
sudo useradd --system --user-group --no-create-home --home-dir /nonexistent \
  --shell /usr/sbin/nologin chatapp
sudo install -d -m 0755 /opt/darsavand-chat
sudo cp -a app.mjs server.mjs protocol.mjs room.mjs connection.mjs \
  package.json package-lock.json public node_modules /opt/darsavand-chat/
sudo chown -R root:root /opt/darsavand-chat
sudo find /opt/darsavand-chat -type d -exec chmod 0755 {} +
sudo find /opt/darsavand-chat -type f -exec chmod 0644 {} +
sudo install -d -m 0700 /etc/darsavand-chat
CHAT_ENV=$(mktemp)
umask 077
printf 'PORT=3001\nPUBLIC_ORIGIN=https://%s\n' "$CHAT_DOMAIN" > "$CHAT_ENV"
sudo install -m 0600 -o root -g root "$CHAT_ENV" /etc/darsavand-chat/app.env
rm -- "$CHAT_ENV"
sudo install -m 0644 deploy/chat.service /etc/systemd/system/darsavand-chat.service
sudo systemd-analyze verify /etc/systemd/system/darsavand-chat.service
sudo systemctl daemon-reload
sudo systemctl enable --now darsavand-chat.service
sudo systemctl --no-pager --full status darsavand-chat.service
curl --fail --show-error http://127.0.0.1:3001/api/status
# Prepare the Nginx candidate in your own directory, then inspect it.
CHAT_CANDIDATE=$(mktemp)
python3 - "$CHAT_DOMAIN" "$CHAT_FULLCHAIN" "$CHAT_KEY" > "$CHAT_CANDIDATE" <<'PYCONF'
import sys
from pathlib import Path
host, fullchain, key = sys.argv[1:]
for value in (fullchain, key):
    if not value.startswith('/') or any(c in value for c in '\n\r;{} '):
        raise SystemExit('Use absolute certificate paths without Nginx metacharacters')
text = Path('deploy/chat.nginx.conf').read_text()
text = text.replace('/etc/letsencrypt/live/chat.example.com/fullchain.pem', fullchain)
text = text.replace('/etc/letsencrypt/live/chat.example.com/privkey.pem', key)
text = text.replace('chat.example.com', host)
if 'REPLACE_' in text or 'chat.example.com' in text:
    raise SystemExit('Unresolved configuration placeholder')
print(text, end='')
PYCONF
cat "$CHAT_CANDIDATE"
sudo install -m 0644 "$CHAT_CANDIDATE" /etc/nginx/sites-available/darsavand-chat
rm -- "$CHAT_CANDIDATE"
sudo ln -s /etc/nginx/sites-available/darsavand-chat /etc/nginx/sites-enabled/darsavand-chat
if sudo nginx -t; then
  sudo systemctl reload nginx
else
  # Remove ONLY the new enabling link. Existing sites have not been edited.
  sudo rm -- /etc/nginx/sites-enabled/darsavand-chat
  echo 'Nginx rejected the new site; no reload performed. Review the candidate.' >&2
  exit 1
fi
curl --fail --show-error "https://$CHAT_DOMAIN/api/status"
curl --head --show-error "http://$CHAT_DOMAIN/"

کنترل checkend حداقل یک روز اعتبار باقی‌مانده را بررسی می‌کند و جای برنامهٔ تمدید نیست. دستور verify، اعتماد زنجیره و نام میزبان را بررسی می‌کند؛ مقایسهٔ هش کلید عمومی نیز جفت‌بودن کلید و گواهی را پیش از ساخت سرویس می‌سنجد. nginx -t پیکربندی را بررسی می‌کند. پس از موفقیت، وضعیت سرویس باید active و پاسخ وضعیت محلی یک شیء #json با ok:true باشد. فایل محیط root با مجوز ۶۰۰ نگهداری می‌شود و کد برای خواندن سرویس مجوز دارد. اگر نصب در میانه شکست خورد، مسیرهای ساخته‌شده را ثبت و علت را حل کنید؛ تکرار کورکورانهٔ نصب تازه به کنترل وجود مسیر برخورد می‌کند.

فعال‌سازی Nginx فقط پس از nginx -t موفق reload می‌شود. اگر فایل جدید رد شود، فقط پیوند فعال‌سازی همین نمونه برداشته می‌شود و reload انجام نمی‌گیرد. برای تغییرهای بعدی، از فایل فعلی خودتان نسخهٔ پشتیبان بگیرید، تغییر کوچک را بررسی و سپس reload کنید؛ راهنمای RUNBOOK بسته مسیرها را یادآوری می‌کند. صدور یا تمدید گواهی را با تنظیم سرویس Node یکی ندانید. برنامهٔ چت نباید با root اجرا شود یا برای خواندن کلید TLS مجوز گسترده بگیرد؛ این دو نیاز در طرح فعلی وجود ندارند.

بررسی نهایی و عیب‌یابی مرحله‌ای

از دستگاه دیگر، صفحهٔ HTTPS را باز کنید و دو تب عضو بسازید. در Network باید اتصال wss به /chat با پاسخ 101 ببینید؛ درخواست /api/status فقط HTTP است و با #fetch-http-json قابل مقایسه است. پورت ۳۰۰۱ را روی اینترنت باز نکنید؛ Nginx همان ماشین به loopback دسترسی دارد. اگر TLS پیش از HTTP خطا داد، نام میزبان، زنجیره و تاریخ گواهی را بررسی کنید. گزینهٔ نادیده گرفتن اعتبار گواهی، نتیجهٔ صحیح این تمرین نیست. اگر رکورد AAAA دارید، مسیر IPv6 نیز باید به سرور درست برسد.

برای 502، ابتدا systemctl status darsavand-chat.service، سپس curl محلی و journalctl -u darsavand-chat.service -n 50 --no-pager را بررسی کنید. برای 403 پیش از Upgrade، Origin واقعی را با محیط سرویس مقایسه کنید. مسیر /ws یا /chat/ با قرارداد دقیق /chat یکی نیست. اگر صفحه باز می‌شود ولی 101 ندارید، هدرهای پراکسی و مسیر را بررسی کنید. قطع شدن در زمان بیکاری می‌تواند به timeout واسطه یا heartbeat مربوط باشد؛ صرف افزایش همهٔ زمان‌ها علت را پیدا نمی‌کند. #debugging-console در این مرحله برای دیدن درخواست و کد بسته شدن کاربرد دارد.

تمرین و پاسخ راهنما

در کپی محلی تنظیمات، PUBLIC_ORIGIN را با اسلش پایانی بنویسید و رفتار شروع را توضیح دهید: برنامه باید مقدار را رد کند، نه اینکه هر مبدأ را بپذیرد. سپس سه گزارش بنویسید: TLS نامعتبر، 502 و 403 اتصال. پاسخ‌ها به‌ترتیب بررسی گواهی، رسیدن به Node و مقایسهٔ مبدأ هستند. معیار تحویل نهایی، HTTPS معتبر، پاسخ وضعیت، دو تب با یک پیام مشترک، توقف مرتب و مستند بودن محدودیت تاریخچه است. این پایان یک پروژهٔ آموزشی قابل اجراست؛ برای محصول عمومی، حساب، مجوز، ذخیرهٔ پایدار و سیاست نگهداری داده به طراحی جداگانه نیاز دارند.

منابع رسمی انگلیسی:
Nginx — WebSocket proxying: https://nginx.org/en/docs/http/websocket.html
Nginx — proxy_pass and proxy timeouts: https://nginx.org/en/docs/http/ngx_http_proxy_module.html
Nginx — SSL module: https://nginx.org/en/docs/http/ngx_http_ssl_module.html
systemd — Service units: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
systemd — Execution environment: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
npm — npm ci: https://docs.npmjs.com/cli/v11/commands/npm-ci/
OpenSSL — verify: https://docs.openssl.org/3.0/man1/openssl-verify/
OpenSSL — x509 and public key: https://docs.openssl.org/3.0/man1/openssl-x509/
OpenSSL — pkey: https://docs.openssl.org/3.0/man1/openssl-pkey/

تألیف و تنظیمات نمونهٔ استقرار: تحریریه درس‌آوند. این درس صدور گواهی تازه یا تغییر خودکار سرور واقعی انجام نمی‌دهد.

تألیف و مثال‌های اختصاصی: تحریریه درس‌آوند. فایل‌های کامل پروژه و راهنمای اجرا و دیباگ:
https://www.darsavand.com/resources/simple-websocket-chat-project

ورود برای ثبت پیشرفت

متن درس‌های عمومی را بدون ورود بخوانید.

یادگیری‌ات را دنبال کن

برای نگهداری پیشرفت و ادامهٔ یادگیری از حساب خود استفاده کن.

ورود و ثبت‌نام
دربارهٔ این دوره

دورهٔ رایگان تکمیلی Node.js در ۸ درس؛ EventEmitter و Stream، WebSocket، پخش پیام، اتصال سالم، آزمون و دیباگ تا اجرای سرور چت با WSS روی اوبونتو.

پرسشی دربارهٔ این درس داری؟گفت‌وگو با پشتیبانی در حساب کاربری
یکجا انتخاب کن، راحت‌تر دریافت کن

انتخاب‌های من ۰ منبع

مجموعهٔ مطالعه‌ات را بساز.

کنار مقاله‌ها «انتخاب» را بزنید تا چند فایل را با هم دریافت کنید.

انتخاب مقاله‌های رایگان