توی این درس یاد میگیری اتصالهای WebSocket (اتصال دوطرفه و ماندگار بین مرورگر و سرور، که چت، اعلان زنده، بازی آنلاین و داشبورد لحظهای با آن ساخته میشوند) را از Nginx عبور بدهی. یک اپ چت واقعی میسازی و میبینی با proxy_pass ساده کار نمیکند؛ دلیلش را در دستدادن (handshake) WebSocket و هدرهای Upgrade و Connection پیدا میکنی، با سه خط درستش میکنی، الگوی استاندارد map $http_upgrade $connection_upgrade را یاد میگیری، و مشکل «اتصال بعد از یک دقیقه قطع میشود» را با proxy_read_timeout و ping حل میکنی.
مسئله: HTTP برای «زنده» ساخته نشده
Section titled “مسئله: HTTP برای «زنده» ساخته نشده”HTTP معمولی «درخواست و پاسخ» است: مرورگر میپرسد، سرور جواب میدهد، تمام. برای یک چت، سرور باید هر وقت پیام تازهای رسید خودش به مرورگر بگوید، بدون اینکه مرورگر هر ثانیه بپرسد «پیام جدید داری؟». WebSocket همین را حل میکند: مرورگر یک بار یک درخواست HTTP خاص میفرستد که میگوید «بیا این اتصال را به WebSocket ارتقا (upgrade) بدهیم»، و اگر سرور قبول کند، همان اتصال TCP باز میماند و هر دو طرف هر وقت خواستند پیام میفرستند. مشکل اینجاست که Nginx بهصورت پیشفرض این درخواست «ارتقا» را به اپ نمیرساند.
تشبیه: تماس تلفنی بهجای نامه
Section titled “تشبیه: تماس تلفنی بهجای نامه”HTTP معمولی مثل نامه است: میفرستی، جواب میآید، تمام. WebSocket مثل تماس تلفنی است: یک بار شماره میگیری و میپرسی «میتوانیم صحبت کنیم؟» (دستدادن)، طرف میگوید «بله» (پاسخ 101)، و از آن به بعد خط باز میماند و هر دو هر وقت خواستند حرف میزنند. Nginx مثل اپراتور تلفنخانه است که تماس را وصل میکند. اپراتور معمولی فقط نامه جابهجا میکند؛ باید به او یاد بدهی وقتی کسی گفت «تماس میخواهم» (هدر Upgrade)، خط را وصل و باز نگه دارد، و اگر چند دقیقه سکوت شد، گوشی را نگذارد (timeout).
دستدادن WebSocket
Section titled “دستدادن WebSocket”مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، Python 3.12) با کاربر root اجرا شده. اپ چت با کتابخانهی websockets پایتون (بستهی python3-websockets در Ubuntu) ساخته شده؛ در دنیای واقعی همین را با Node (ws یا Socket.IO)، Go یا هر زبان دیگری میسازند و تنظیم Nginx دقیقاً همین است.
مثال ۱: اپ چت
Section titled “مثال ۱: اپ چت”نصب کتابخانه و نوشتن سرور چت: هر پیامی که یک کاربر بفرستد، برای همهی کاربران متصل پخش (broadcast) میشود.
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-websockets > /dev/null 2>&1python3 -c 'import websockets; print("websockets", websockets.__version__)'websockets 10.4# chat_server.py: broadcast every message to all connected clientsimport asyncioimport websockets
CLIENTS = set()
async def handler(ws, path=None): CLIENTS.add(ws) name = f"user{len(CLIENTS)}" websockets.broadcast(CLIENTS, f"* {name} joined ({len(CLIENTS)} online)") try: async for message in ws: websockets.broadcast(CLIENTS, f"{name}: {message}") finally: CLIENTS.discard(ws) websockets.broadcast(CLIENTS, f"* {name} left ({len(CLIENTS)} online)")
async def main(): async with websockets.serve(handler, "127.0.0.1", 8765, ping_interval=20): await asyncio.Future() # run forever
asyncio.run(main())و یک کلاینت آزمایشی که وصل میشود، (اختیاری) پیامی میفرستد، چند ثانیه هر چه میرسد را چاپ میکند و میرود:
# chat_client.py URL SECONDS [MESSAGE]: connect, maybe send, print what arrivesimport asyncioimport sysimport websockets
async def main(url, seconds, message): try: async with websockets.connect(url, ping_interval=None) as ws: if message: await asyncio.sleep(0.5) await ws.send(message) end = asyncio.get_running_loop().time() + seconds while (left := end - asyncio.get_running_loop().time()) > 0: try: print(" got:", await asyncio.wait_for(ws.recv(), left), flush=True) except asyncio.TimeoutError: break except Exception as e: print(" ERROR:", type(e).__name__, e, flush=True)
asyncio.run(main(sys.argv[1], float(sys.argv[2]), sys.argv[3] if len(sys.argv) > 3 else ""))(exec python3 /srv/chat/chat_server.py) > /srv/chat/server.log 2>&1 &sleep 1ss -tln | grep ':8765'echo "=== دو کاربر مستقیم به اپ؛ کاربر دوم سلام میکند:"python3 /srv/chat/chat_client.py ws://127.0.0.1:8765/ 2 > /tmp/alice.txt &alice=$!sleep 0.3python3 /srv/chat/chat_client.py ws://127.0.0.1:8765/ 1.5 "hello from bob"wait "$alice"echo "--- کاربر اول دید:"cat /tmp/alice.txtLISTEN 0 100 127.0.0.1:8765 0.0.0.0:*=== دو کاربر مستقیم به اپ؛ کاربر دوم سلام میکند: got: * user2 joined (2 online) got: user2: hello from bob got: * user1 left (1 online)--- کاربر اول دید: got: * user1 joined (1 online) got: * user2 joined (2 online) got: user2: hello from bobچت مستقیم کار میکند: کاربر اول پیوستن کاربر دوم و پیامش را بدون اینکه چیزی بپرسد دریافت کرد؛ سرور خودش فرستاد. حالا باید از پشت Nginx هم همین کار کند.
مثال ۲: دستدادن را با curl ببینیم
Section titled “مثال ۲: دستدادن را با curl ببینیم”دستدادن WebSocket در واقع یک درخواست HTTP است، پس با curl میشود دیدش. -H هدرهای ارتقا را میفرستد و -m 2 بعد از ۲ ثانیه قطع میکند (چون اتصال باز میماند):
curl -s -i -m 2 \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ http://127.0.0.1:8765/ | tr -d '\r' | head -n 6HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Date: Sun, 04 Oct 2026 13:15:55 GMTServer: Python/3.12 websockets/10.4101 Switching Protocols: سرور قبول کرد که پروتکل را عوض کند. Sec-WebSocket-Accept اثبات میکند سرور واقعاً WebSocket میفهمد (پشت پرده میگوییم از کجا آمده). بعد از این خط، دیگر HTTP نیست؛ فریمهای WebSocket است.
مثال ۳: پشت Nginx با proxy_pass ساده، شکست
Section titled “مثال ۳: پشت Nginx با proxy_pass ساده، شکست”server { listen 80; server_name chat.test;
location /ws/ { proxy_pass http://127.0.0.1:8765; } location /raw/ { proxy_pass http://127.0.0.1:8799; }}(location /raw/ با همان تنظیم ساده، درخواست را به یک nc میفرستد که فقط درخواست خام را ثبت میکند؛ تا ببینیم Nginx دقیقاً چه چیزی به اپ میدهد.)
ln -s /etc/nginx/sites-available/chat.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== دستدادن از میان Nginx:"curl -s -i -m 2 \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ http://chat.test/ws/ | tr -d '\r' | head -n 1echo "=== کلاینت چت از میان Nginx:"python3 /srv/chat/chat_client.py ws://chat.test/ws/ 1echo "=== درخواستی که Nginx واقعاً به اپ میفرستد:"timeout 3 nc -l 127.0.0.1 8799 > /tmp/raw.txt &ncpid=$!sleep 0.5curl -s -m 1 -o /dev/null \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ http://chat.test/raw/wait "$ncpid"tr -d '\r' < /tmp/raw.txtnginx: configuration file /etc/nginx/nginx.conf test is successful=== دستدادن از میان Nginx:HTTP/1.1 400 Bad Request=== کلاینت چت از میان Nginx: ERROR: InvalidStatusCode server rejected WebSocket connection: HTTP 400=== درخواستی که Nginx واقعاً به اپ میفرستد:GET /raw/ HTTP/1.0Host: 127.0.0.1:8799Connection: closeUser-Agent: curl/8.5.0Accept: */*Sec-WebSocket-Version: 13Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==بهجای 101، اپ با 400 Bad Request رد کرد. درخواست خامی که nc گرفت دلیلش را نشان میدهد: Nginx درخواست را با HTTP/1.0 و Connection: close فرستاد و هدر Upgrade اصلاً در آن نیست؛ هدرهای Sec-WebSocket-* رسیدند، ولی درخواستِ «ارتقا» گم شد.
مثال ۴: سه خط درست
Section titled “مثال ۴: سه خط درست”cat > /etc/nginx/sites-available/chat.test <<'EOF'server { listen 80; server_name chat.test;
location /ws/ { proxy_pass http://127.0.0.1:8765; 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; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== دستدادن از میان Nginx:"curl -s -i -m 2 \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ http://chat.test/ws/ | tr -d '\r' | grep -E '^(HTTP|Upgrade|Connection|Sec-WebSocket-Accept)'echo "=== دو کاربر از میان Nginx:"python3 /srv/chat/chat_client.py ws://chat.test/ws/ 2 > /tmp/alice.txt &alice=$!sleep 0.3python3 /srv/chat/chat_client.py ws://chat.test/ws/ 1.5 "hi through nginx"wait "$alice"cat /tmp/alice.txtnginx: configuration file /etc/nginx/nginx.conf test is successful=== دستدادن از میان Nginx:HTTP/1.1 101 Switching ProtocolsConnection: upgradeUpgrade: websocketSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo==== دو کاربر از میان Nginx: got: * user2 joined (2 online) got: user2: hi through nginx got: * user1 left (1 online) got: * user1 joined (1 online) got: * user2 joined (2 online) got: user2: hi through nginxحالا 101 از میان Nginx آمد و چت کار میکند. سه خط کلیدی:
proxy_http_version 1.1;: ارتقای پروتکل در HTTP/1.1 تعریف شده؛ پیشفرض Nginx برای اپ HTTP/1.0 است (درس reverse proxy).proxy_set_header Upgrade $http_upgrade;: هدرUpgradeکاربر (websocket) را به اپ برسان.proxy_set_header Connection "upgrade";: به اپ بگو این اتصال برای ارتقاست.
(Host و X-Real-IP را هم گذاشتیم؛ چون این location proxy_set_header خودش را دارد، هیچ هدری از بیرون به ارث نمیرسد.)
مثال ۵: الگوی map برای یک location مشترک
Section titled “مثال ۵: الگوی map برای یک location مشترک”خیلی وقتها یک اپ هم صفحهی HTTP معمولی دارد و هم WebSocket، روی همان مسیرها (مثلاً Socket.IO که اول با HTTP شروع میکند). گذاشتن Connection "upgrade" روی همهی درخواستها درست نیست. الگوی استاندارد مستندات Nginx: یک map که اگر کاربر Upgrade فرستاده بود، upgrade و وگرنه close بدهد:
cat > /etc/nginx/conf.d/connection-upgrade.conf <<'EOF'map $http_upgrade $connection_upgrade { default upgrade; '' close;}EOFsed -i 's|proxy_set_header Connection "upgrade";|proxy_set_header Connection $connection_upgrade;|' /etc/nginx/sites-available/chat.testgrep -n 'Connection' /etc/nginx/sites-available/chat.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== WebSocket:"python3 /srv/chat/chat_client.py ws://chat.test/ws/ 0.5echo "=== HTTP معمولی به همان مسیر:"curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://chat.test/ws/9: proxy_set_header Connection $connection_upgrade;nginx: configuration file /etc/nginx/nginx.conf test is successful=== WebSocket: got: * user1 joined (1 online)=== HTTP معمولی به همان مسیر:HTTP 426WebSocket کار میکند و درخواست HTTP معمولی هم (که اپ چت ما به آن جواب 426 Upgrade Required میدهد، چون فقط WebSocket میفهمد) بدون هدر ارتقای جعلی به اپ رسید. map در conf.d (context http) است تا در همهی سایتها قابل استفاده باشد.
مثال ۶: قطع شدن بعد از سکوت، و راهحل
Section titled “مثال ۶: قطع شدن بعد از سکوت، و راهحل”proxy_read_timeout (پیشفرض ۶۰ ثانیه) برای WebSocket هم معتبر است: اگر در این مدت هیچ دادهای از اپ نیاید، Nginx اتصال را میبندد. برای اینکه آزمایش طول نکشد، آن را روی ۳ ثانیه میگذاریم و یک کلاینت ۶ ثانیه ساکت متصل میماند. برای اینکه اثر ping را جدا ببینیم، یک نسخهی دوم از سرور چت بدون ping روی پورت ۸۷۶۶ هم بالا میآوریم:
sed 's/8765, ping_interval=20/8766, ping_interval=None/' /srv/chat/chat_server.py > /srv/chat/chat_server_noping.py(exec python3 /srv/chat/chat_server_noping.py) > /dev/null 2>&1 &sed -i 's| location /ws/ {| location /ws-noping/ {\n proxy_pass http://127.0.0.1:8766;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection $connection_upgrade;\n proxy_read_timeout 3s;\n }\n\n location /ws/ {|' /etc/nginx/sites-available/chat.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== بدون ping، timeout سه ثانیه، ۶ ثانیه سکوت:"start=$SECONDSpython3 - <<'EOF'import asyncio, websocketsasync def main(): async with websockets.connect("ws://chat.test/ws-noping/", ping_interval=None) as ws: print(" got:", await ws.recv()) try: await asyncio.wait_for(ws.recv(), 6) except asyncio.TimeoutError: print(" still connected after 6s of silence") except websockets.ConnectionClosed as e: print(" CLOSED by the other side:", e)asyncio.run(main())EOFecho " (took $((SECONDS - start))s)"nginx: configuration file /etc/nginx/nginx.conf test is successful=== بدون ping، timeout سه ثانیه، ۶ ثانیه سکوت: got: * user1 joined (1 online) CLOSED by the other side: no close frame received or sent (took 3s)اتصال بعد از حدود ۳ ثانیه سکوت بسته شد (CLOSED؛ Nginx بست، نه اپ). در چت واقعی یعنی «یک دقیقه کسی چیزی نگفت، چت قطع شد». دو راهحل که معمولاً با هم به کار میروند:
۱. timeout بزرگتر برای location WebSocket: proxy_read_timeout 1h;.
۲. ping منظم: اپ (یا کلاینت) هر چند ثانیه یک فریم کوچک ping میفرستد تا اتصال هیچوقت «ساکت» نباشد. سرور اصلی ما ping_interval=20 دارد؛ برای دیدن اثرش، timeout /ws/ را ۳ ثانیه و ping سرور را روی ۱ ثانیه میگذاریم:
pkill -f 'chat_server.py'sed -i 's/ping_interval=20/ping_interval=1/' /srv/chat/chat_server.py(exec python3 /srv/chat/chat_server.py) > /srv/chat/server.log 2>&1 &sed -i '/location \/ws\/ {/,/}/ s| proxy_set_header X-Real-IP $remote_addr;| proxy_set_header X-Real-IP $remote_addr;\n proxy_read_timeout 3s;|' /etc/nginx/sites-available/chat.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== با ping هر ۱ ثانیه، timeout سه ثانیه، ۶ ثانیه سکوت:"python3 - <<'EOF'import asyncio, websocketsasync def main(): async with websockets.connect("ws://chat.test/ws/", ping_interval=None) as ws: print(" got:", await ws.recv()) try: await asyncio.wait_for(ws.recv(), 6) except asyncio.TimeoutError: print(" still connected after 6s of silence") except websockets.ConnectionClosed as e: print(" CLOSED by the other side:", e)asyncio.run(main())EOFnginx: configuration file /etc/nginx/nginx.conf test is successful=== با ping هر ۱ ثانیه، timeout سه ثانیه، ۶ ثانیه سکوت: got: * user1 joined (1 online) still connected after 6s of silenceبا اینکه هیچ پیام چتی رد و بدل نشد، فریمهای ping هر ثانیه از اپ گذشتند و Nginx اتصال را «ساکت» ندید. (در عمل ping_interval حدود ۲۰ تا ۳۰ ثانیه و proxy_read_timeout چند دقیقه یا یک ساعت است.)
پشت پرده: hop-by-hop و Sec-WebSocket-Accept
Section titled “پشت پرده: hop-by-hop و Sec-WebSocket-Accept”چرا Nginx خودش هدر Upgrade را رد نمیکند؟ در HTTP، هدرهای Connection و Upgrade از نوع hop-by-hop هستند: یعنی فقط برای همان یک اتصال (مثلاً مرورگر ← Nginx) معنی دارند و طبق استاندارد، proxy نباید آنها را به اتصال بعدی (Nginx ← اپ) منتقل کند. برای همین باید صریح بگویی «این بار، ارتقا را جلو ببر». بعد از 101، Nginx دیگر محتوا را تفسیر نمیکند و فقط بایتها را بین دو اتصال جابهجا میکند (تونل).
و Sec-WebSocket-Accept؟ سرور کلید کلاینت را با یک رشتهی ثابت جهانی (258EAFA5-E914-47DA-95CA-C5AB0DC85B11، تعریفشده در استاندارد RFC 6455) میچسباند، SHA-1 میگیرد و base64 میکند. خودمان حساب کنیم و با جواب سرور مقایسه کنیم:
key='dGhlIHNhbXBsZSBub25jZQ=='printf '%s' "${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | openssl dgst -sha1 -binary | base64curl -s -i -m 2 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \ -H "Sec-WebSocket-Key: $key" http://chat.test/ws/ | tr -d '\r' | grep '^Sec-WebSocket-Accept's3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=یکساناند. این مکانیزم ثابت میکند پاسخ را یک سرور WebSocket واقعی داده، نه یک کش یا proxy قدیمی که فقط درخواست را تکرار کرده.
جدولهای مرجع
Section titled “جدولهای مرجع”تنظیم کامل یک location WebSocket:
| خط | چرا |
|---|---|
proxy_pass http://127.0.0.1:8765; |
آدرس اپ |
proxy_http_version 1.1; |
ارتقا فقط در HTTP/1.1 |
proxy_set_header Upgrade $http_upgrade; |
رساندن هدر hop-by-hop Upgrade |
proxy_set_header Connection $connection_upgrade; |
upgrade برای WebSocket، close برای HTTP (با map) |
proxy_set_header Host $host; و X-Real-IP |
اطلاعات کاربر (درس reverse proxy) |
proxy_read_timeout 1h; |
اتصال ساکت قطع نشود |
کدهایی که در این درس دیدیم:
| کد | معنی |
|---|---|
101 Switching Protocols |
دستدادن موفق؛ از این به بعد WebSocket |
400 Bad Request / 426 Upgrade Required |
اپ WebSocket درخواست بدون هدر ارتقا گرفته |
502 / 504 |
مثل هر proxy دیگری: اپ در دسترس نیست / دیر جواب داد |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) proxy_pass ساده برای WebSocket
Section titled “۱) proxy_pass ساده برای WebSocket”مثال ۳. راهحل: سه خط proxy_http_version 1.1، Upgrade، Connection.
۲) Connection "upgrade" روی همهچیز
Section titled “۲) Connection "upgrade" روی همهچیز”اگر یک location هم HTTP معمولی دارد و هم WebSocket، Connection: upgrade برای درخواستهای معمولی غلط است. راهحل: الگوی map $http_upgrade $connection_upgrade (مثال ۵).
۳) قطع شدن چت بعد از یک دقیقه سکوت
Section titled “۳) قطع شدن چت بعد از یک دقیقه سکوت”مثال ۶: پیشفرض proxy_read_timeout ۶۰ ثانیه است. راهحل: timeout بزرگتر برای location WebSocket و ping در اپ یا کلاینت.
۴) WebSocket پشت load balancer با چند نسخهی اپ
Section titled “۴) WebSocket پشت load balancer با چند نسخهی اپ”اگر اپ وضعیت را در حافظه نگه دارد (مثل CLIENTS در اپ ما) و چند نسخه از آن پشت Nginx باشد، کاربرانی که به نسخههای مختلف وصلاند پیام هم را نمیبینند. راهحل: اپ وضعیت را در یک سرویس مشترک نگه دارد (مثلاً Redis pub/sub) یا (موقت) ip_hash در upstream تا هر کاربر همیشه به یک نسخه برسد (درس load balancing).
۵) proxy_set_header ناقص با ارثبری
Section titled “۵) proxy_set_header ناقص با ارثبری”اگر include proxy_params در سطح server باشد و location WebSocket فقط Upgrade و Connection را بگذارد، Host و X-Real-IP در آن location از دست میروند (قاعدهی ارثبری آرایهای؛ درس ساختار تنظیمات). راهحل: همهی هدرها را در همان location بنویس (یا آنجا هم include کن).
بدون کلاینت پایتون و فقط با curl، ثابت کن مسیر /ws/ روی chat.test دستدادن WebSocket را با 101 جواب میدهد، و همان درخواست بدون هدر Upgrade جواب دیگری میگیرد.
دیدن جواب
echo "=== با هدرهای ارتقا:"curl -s -i -m 2 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \ -H 'Sec-WebSocket-Key: SGVsbG8gTG9vcFggMTIzNA==' http://chat.test/ws/ | tr -d '\r' | head -n 1echo "=== بدون هدرهای ارتقا:"curl -s -i -m 2 http://chat.test/ws/ | tr -d '\r' | head -n 1=== با هدرهای ارتقا:HTTP/1.1 101 Switching Protocols=== بدون هدرهای ارتقا:HTTP/1.1 426 Upgrade Requiredبا هدرها 101 Switching Protocols؛ بدون آنها اپ با 426 Upgrade Required گفت «من فقط WebSocket میفهمم». کلید (Sec-WebSocket-Key) هر رشتهی ۱۶ بایتی base64 شده است.
تمرین اصلی درس: اپ چت را پشت Nginx راه بینداز، بهشکلی که سه کاربر از میان Nginx متصل شوند، دو نفر پیام بدهند و نفر سوم (که فقط گوش میدهد) هر دو پیام را ببیند. تنظیم نهایی location WebSocket باید proxy_read_timeout 1h داشته باشد.
دیدن جواب
sed -i 's/ping_interval=1/ping_interval=20/' /srv/chat/chat_server.pypkill -f 'chat_server'(exec python3 /srv/chat/chat_server.py) > /srv/chat/server.log 2>&1 &cat > /etc/nginx/sites-available/chat.test <<'EOF'server { listen 80; server_name chat.test;
location /ws/ { proxy_pass http://127.0.0.1:8765; 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_read_timeout 1h; }}EOFsleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1python3 /srv/chat/chat_client.py ws://chat.test/ws/ 3 > /tmp/listener.txt &listener=$!sleep 0.5python3 /srv/chat/chat_client.py ws://chat.test/ws/ 1 "who is on support today?" > /dev/null &sleep 0.3python3 /srv/chat/chat_client.py ws://chat.test/ws/ 1 "Sara is, ask her" > /dev/nullwait "$listener"echo "--- کاربری که فقط گوش داد:"cat /tmp/listener.txtnginx: configuration file /etc/nginx/nginx.conf test is successful--- کاربری که فقط گوش داد: got: * user1 joined (1 online) got: * user2 joined (2 online) got: * user3 joined (3 online) got: user2: who is on support today? got: user3: Sara is, ask her got: * user2 left (2 online) got: * user3 left (1 online)کاربر شنونده پیوستن و رفتن دو نفر دیگر و هر دو پیام را دید. چون هر کلاینت در یک پروسهی جداست، ترتیب دقیق پیامها (پیوستنها و پیامها) ممکن است کمی فرق کند؛ مهم این است که همه از میان Nginx رسیدند.
یک صفحهی HTML ساده برای چت (فایل استاتیک) و WebSocket را روی یک دامنه سرو کن: / صفحهی HTML را از دیسک بدهد و /ws/ به اپ برود. در صفحه، آدرس WebSocket را نسبی به خود صفحه بساز (ws:// یا wss:// بر اساس پروتکل صفحه) تا هم روی HTTP و هم بعد از HTTPS کار کند. با curl ثابت کن هر دو بخش درست جواب میدهند.
دیدن جواب
mkdir -p /var/www/chat.testcat > /var/www/chat.test/index.html <<'EOF'<!doctype html><meta charset="utf-8"><title>LoopX chat</title><ul id="log"></ul><input id="msg" placeholder="message"><button id="send">send</button><script> const proto = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${proto}//${location.host}/ws/`); const log = (t) => { const li = document.createElement("li"); li.textContent = t; document.getElementById("log").append(li); }; ws.onmessage = (e) => log(e.data); document.getElementById("send").onclick = () => ws.send(document.getElementById("msg").value);</script>EOFsed -i 's| location /ws/ {| root /var/www/chat.test;\n index index.html;\n\n location /ws/ {|' /etc/nginx/sites-available/chat.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -o /dev/null -w "/ -> HTTP %{http_code} %{content_type}\n" http://chat.test/curl -s http://chat.test/ | grep -o 'new WebSocket([^)]*)'curl -s -i -m 2 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \ -H 'Sec-WebSocket-Key: SGVsbG8gTG9vcFggMTIzNA==' http://chat.test/ws/ | tr -d '\r' | head -n 1 | sed 's|^|/ws/ -> |'nginx: configuration file /etc/nginx/nginx.conf test is successful/ -> HTTP 200 text/htmlnew WebSocket(`${proto}//${location.host}/ws/`)/ws/ -> HTTP/1.1 101 Switching Protocolsصفحه با location.protocol میفهمد خودش روی HTTP است یا HTTPS و ws: یا wss: انتخاب میکند؛ location.host هم دامنه و پورت فعلی است. پس همین فایل بدون تغییر بعد از فعال کردن HTTPS (درس بعد) کار میکند، و Nginx هر دو بخش (فایل و WebSocket) را روی یک دامنه سرو میکند (بدون مشکلهای CORS).
آزمونک
Section titled “آزمونک”کدام سه خط در location، WebSocket را از Nginx عبور میدهد؟
Upgrade و Connection هدرهای hop-by-hop هستند و باید صریح جلو برده شوند؛ ارتقا هم فقط در HTTP/1.1 است.
پاسخ موفق دستدادن WebSocket چه کدی دارد؟
بعد از 101 همان اتصال برای فریمهای WebSocket باز میماند.
چرا Nginx هدر Upgrade کاربر را خودکار به اپ نمیرساند؟
با proxy_set_header Upgrade $http_upgrade صریح منتقلش کن.
چت پشت Nginx بعد از یک دقیقه سکوت قطع میشود. علت محتمل؟
timeout بزرگتر برای location WebSocket و ping منظم.
map $http_upgrade $connection_upgrade { default upgrade; '' close; } برای چیست؟
الگوی پیشنهادی مستندات Nginx.
روی سایت HTTPS، مرورگر به چه آدرسی برای WebSocket وصل میشود؟
تنظیم WebSocket در server block 443 همان است.
سه نسخه از اپ چت (که کاربران را در حافظه نگه میدارد) پشت upstream با round robin هستند. چه مشکلی پیش میآید؟
هر نسخه فقط کلاینتهای خودش را میشناسد.
جمعبندی
Section titled “جمعبندی”- WebSocket با یک درخواست HTTP (
Upgrade: websocket،Connection: Upgrade،Sec-WebSocket-Key) شروع میشود و با101 Switching Protocolsبه یک اتصال دوطرفهی ماندگار تبدیل میشود. proxy_passساده کافی نیست؛UpgradeوConnectionهدرهای hop-by-hopاند. سه خط لازم:proxy_http_version 1.1;،proxy_set_header Upgrade $http_upgrade;،proxy_set_header Connection "upgrade";(یا$connection_upgradeباmap).- بعد از
101، Nginx فقط تونل است.proxy_read_timeout(پیشفرض ۶۰ ثانیه) اتصال ساکت را میبندد: timeout بزرگتر + ping در اپ. - هدرهای
HostوX-Real-IPرا در همان location WebSocket بنویس (ارثبری). - روی HTTPS،
wss://از بیرون و همان تنظیم داخل server ۴۴۳. با چند نسخهی اپ، به وضعیت مشترک یاip_hashفکر کن.
| دستور | کاری که میکند |
|---|---|
proxy_http_version 1.1; | لازم برای ارتقای پروتکل |
proxy_set_header Upgrade $http_upgrade; | رساندن هدر Upgrade به اپ |
proxy_set_header Connection $connection_upgrade; | upgrade یا close (با map) |
map $http_upgrade $connection_upgrade { default upgrade; '' close; } | در context http (conf.d) |
proxy_read_timeout 1h; | قطع نشدن اتصال ساکت |
curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: ...' URL | آزمایش دستدادن (انتظار: 101) |
new WebSocket(`${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws/`) | آدرس WebSocket نسبی در مرورگر |
sudo apt install python3-websockets | کتابخانهی WebSocket پایتون (برای آزمایش) |