WebSocket არის ჩვეულებრივი HTTP მოთხოვნა, რომელიც ითხოვს, რომ აღარ იყოს HTTP მოთხოვნა. ყველაფერი, რაც ტყდება, როცა მის წინ proxy დგება, სწორედ ამ წინადადებიდან მოდის: proxy-ები იმისთვის არის შექმნილი, რომ წაიკითხონ მოთხოვნა, გადააგზავნონ, წაიკითხონ პასუხი და დახურონ კავშირი. თუ შენს proxy-ს არ ეუბნები, რომ კავშირი ღია დატოვოს და ორი კონკრეტული header გაატაროს, handshake ჩავარდება, ან - უარესი - გაივლის და სამოცი წამის შემდეგ უხმოდ მოკვდება. გამოსწორება ოთხი ხაზი კონფიგურაციაა, შეგნებულად დაყენებული timeout და heartbeat. ეს პოსტი სამივეს მოიცავს, ასევე sticky-session პრობლემას, რომელიც მხოლოდ მაშინ ჩნდება, როცა მეორე პროცესს უშვებ.
როგორ იქმნება WebSocket კავშირი სინამდვილეში#
კლიენტი ხსნის ჩვეულებრივ TCP კავშირს, თუ URL wss://-ია, TLS-საც აკეთებს და აგზავნის GET მოთხოვნას ორი header-ით, რომლებიც ნიშნავს: „მინდა პროტოკოლის შეცვლა":
GET /ws HTTP/1.1Host: app.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13თუ სერვერი თანახმაა, პასუხობს სტატუსით 101 და არა 200-ით, და იმ წუთიდან TCP კავშირი HTTP-ის ნაცვლად ორივე მიმართულებით WebSocket ფრეიმებს ატარებს:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=აქედან სამი შედეგი გამომდინარეობს და მთელი პოსტი სწორედ ესაა.
UpgradeდაConnectionheader-ები hop-by-hop-ია. HTTP ამბობს, რომ შუამავალმა მათი ბრმად გადაგზავნა არ უნდა მოახდინოს, ამიტომ proxy მათ აგდებს, სანამ პირდაპირ არ დააბრუნებ. სწორედ ამიტომ იძლევა ნაგულისხმევიproxy_pass101-ის ნაცვლად400-ს ან უბრალო200-ს.- upgrade მხოლოდ HTTP/1.1-ში არსებობს. proxy, რომელიც შენს აპლიკაციასთან HTTP/1.0-ით საუბრობს - ეს არის nginx-ის ნაგულისხმევი upstream პროტოკოლი - მას ვერ შეასრულებს.
101-ის შემდეგ კავშირი გვირაბია, რომელსაც წუთების განმავლობაში შეიძლება არაფერი გაჰქონდეს. ბრაუზერსა და შენს პროცესს შორის ყოველი idle timeout ახლა სრულიად ჯანსაღ კავშირზე მოქმედებს.
ბრაუზერები WebSocket-ებს HTTP/1.1-ით მაინც ხსნიან, HTTP/2 საიტზეც, ამიტომ ამისთვის განსაკუთრებული არაფერი გჭირდება. გჭირდება proxy, რომელიც upgrade-ს გაატარებს.
nginx-ის კონფიგურაცია, რომელიც მუშაობს#
ეს არის მთელი საქმე. map ბლოკი http კონტექსტში დგას და არა server-ის შიგნით, და მისი დანიშნულებაა, რომ ჩვეულებრივმა მოთხოვნამ (Upgrade header-ის გარეშე) შემთხვევითი Connection: upgrade-ის ნაცვლად Connection: close მიიღოს.
map $http_upgrade $connection_upgrade { default upgrade; '' close;}server { listen 443 ssl; server_name app.example.com; location / { proxy_pass http://127.0.0.1:3000; 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 3600s; proxy_send_timeout 3600s; }}ხაზ-ხაზ, მნიშვნელოვანი ნაწილები:
proxy_http_version 1.1- მის გარეშე nginx upstream-ში HTTP/1.0-ით საუბრობს და upgrade-ის მექანიზმი არ არსებობს. ეს ერთი ხაზი400-ის დამბრუნებელი handshake-ის ყველაზე გავრცელებული მიზეზია.proxy_set_header Upgrade/Connection- hop-by-hop header-ებს უკან აბრუნებს.proxy_set_header Host $host- შენი აპლიკაცია ნამდვილ hostname-ს ხედავს. მის გარეშე origin შემოწმებები და multi-tenant routing127.0.0.1:3000-ს ხედავს.proxy_read_timeout- ნაგულისხმევი 60 წამი. ეს არის სამოცწამიანი იდუმალი გათიშვა. დააყენე ხანგრძლივი მნიშვნელობა და გააგზავნე heartbeat-ები, იმის ნაცვლად, რომ დღეღამეზე დააყენო და იმედი გქონდეს.
თუ შენი აპლიკაციის მხოლოდ ერთი ნაწილი საუბრობს WebSocket-ით, შეზღუდე ის location /ws ბლოკით და დანარჩენს ხელი არ ახლო. ჩვეულებრივ route-ზე upgrade header-ები ზიანს არ აყენებს, მაგრამ უფრო ვიწრო კონფიგურაციაზე მსჯელობა უფრო მარტივია, როცა რამე ტყდება. რას აკეთებს reverse proxy სინამდვილეში ჩვეულებრივ HTTP გზას იმავე თანმიმდევრობით გადის, როგორითაც პაკეტები მოდის; nginx-ის სრული კონფიგურაცია TLS-ის ჩათვლით არის nginx reverse proxy-ის გზამკვლევში.
Caddy, Apache და HAProxy#
Caddy 2-ს არაფერი სჭირდება. reverse_proxy 127.0.0.1:3000 upgrade-ს უკვე ამუშავებს, ხოლო websocket matcher, რომელსაც ძველი ბლოგპოსტებიდან იღებენ, Caddy 1-ის ნაწილი იყო:
app.example.com { reverse_proxy 127.0.0.1:3000}Apache-ს სჭირდება მოდული, რომელიც ნაგულისხმევად ჩართული არ არის. ჯერ a2enmod proxy_wstunnel, შემდეგ WebSocket-ის გზა ცალკე მიმართე, რადგან mod_proxy_http upgrade-ს შენთვის ვერ გააკეთებს:
ProxyPass /ws ws://127.0.0.1:3000/wsProxyPassReverse /ws ws://127.0.0.1:3000/wsProxyPass / http://127.0.0.1:3000/ProxyPassReverse / http://127.0.0.1:3000/თანმიმდევრობას მნიშვნელობა აქვს: ჯერ ყველაზე გრძელი გზა, თორემ / /ws-ს შთანთქავს. Apache-ის ProxyTimeout ნაგულისხმევად Timeout-ის ტოლია, ჩვეულებრივ 60 წამი, და აქაც მოქმედებს.
HAProxy upgrade-ებს HTTP რეჟიმში ჩაშენებულად გვირაბავს. პარამეტრი, რომელსაც ხალხი ტოვებს, არის timeout tunnel, რომელიც upgrade-ის შემდეგ კავშირს მართავს; timeout client და timeout server მასზე აღარ მოქმედებს.
defaults timeout client 30s timeout server 30s timeout tunnel 1hპანელზე დაფუძნებულ ჰოსტინგზე ამ ყველაფერს ჩვეულებრივ არ წერ. RE:NODE-ზე აპლიკაციისა და ვებ გეგმები შეიცავს proxy სლოტს: A ჩანაწერს მიუთითებ ტაბზე ნაჩვენებ მისამართზე, სერტიფიკატი 21-დღიან ფანჯარაში ავტომატურად გაიცემა და ახლდება, ხოლო კლიენტის ორიგინალი მისამართი X-Forwarded-For-ში მოდის. რასაც არცერთი proxy შენს მაგივრად ვერ გააკეთებს, არის upgrade-ზე პასუხი - ეს შენი აპლიკაციის საქმეა. და თუ proxy-ს საერთოდ გვერდის ავლა გირჩევნია, შენს სერვერზე გამოყოფილ პორტს პირდაპირ მიწვდები, ხოლო დამატებითი პორტების დამატება Network ტაბზე შეიძლება.
Timeout-ები, heartbeat-ები და socket, რომელიც სამოც წამზე კვდება#
WebSocket, რომელიც ღიაა, მაგრამ ჩუმია, გზაზე ყველაფრისთვის ზუსტად გაჭედილ კავშირს გავს. თითოეულ ჰოპს თავისი წარმოდგენა აქვს, რამდენ ხანს გაუძლოს ამას:
| Hop | პარამეტრი | ტიპური ნაგულისხმევი |
|---|---|---|
| nginx | proxy_read_timeout | 60 წამი |
| Apache | ProxyTimeout | 60 წამი |
| HAProxy | timeout tunnel | იღებს timeout server-ს |
| Cloudflare proxy | დაბალ გეგმებზე არ კონფიგურირდება | დაახლოებით 100 წამი idle |
| Cloud load balancer-ები | idle timeout | 60-დან 350 წამამდე |
| კორპორატიული wifi, მობილური NAT | connection tracking | 30-დან 300 წამამდე, დოკუმენტირებული არ არის |
ბოლოს ვერ დააკონფიგურირებ, ამიტომ პასუხი არასოდეს არის უბრალოდ „გაზარდე timeout". გააგზავნე ტრაფიკი. WebSocket პროტოკოლს სწორედ ამისთვის აქვს ping და pong კონტროლის ფრეიმები, და ყველა სერიოზული ბიბლიოთეკა მათ გამოაქვს.
import { WebSocketServer } from "ws";const wss = new WebSocketServer({ port: 3000 });wss.on("connection", (socket) => { socket.isAlive = true; socket.on("pong", () => { socket.isAlive = true; });});// ყოველ 30 წამში: გადააგდე ის, ვინც ბოლოს არ უპასუხა, დანარჩენებს ping გაუგზავნე.setInterval(() => { for (const socket of wss.clients) { if (!socket.isAlive) { socket.terminate(); continue; } socket.isAlive = false; socket.ping(); }}, 30_000);ოცდაათი წამი კარგი ნაგულისხმევია: ცხრილის ყველა timeout-ზე ბევრად ნაკლებია და საკმარისად იაფია, რომ რამდენიმე ათასი კავშირი არაფერი ჯდება. ბრაუზერები პროტოკოლის დონის ping-ებს ავტომატურად პასუხობენ, ამიტომ raw API-ით კლიენტის მხარეს ამისთვის კოდი საერთოდ არ გჭირდება.
Socket.IO თავისას აკეთებს და კარგად აკეთებს: სერვერი engine.io ping-ს აგზავნის ყოველ pingInterval-ზე (ნაგულისხმევად 25 წამი) და კავშირს ხურავს, თუ pong pingTimeout-ის (ნაგულისხმევად 20 წამი) განმავლობაში არ მოვიდა. ეს ნაგულისხმევი მნიშვნელობები კავშირს უკვე ათბობს, ამიტომ ხელს ნუ ახლებ, სანამ მიზეზს არ გაზომავ.
Socket.IO, polling და sticky session-ები#
Socket.IO WebSocket ბიბლიოთეკა არ არის; ეს არის პროტოკოლი engine.io-ს თავზე, რომელიც იწყება HTTP long-polling-ით და შემდეგ WebSocket-ზე გადადის. სწორედ ეს „პირველი მოთხოვნა polling-ია" იძლევა ამ მთელ სფეროში ყველაზე დამაბნეველ შეცდომას.
handshake ქმნის სესიას (sid), რომელიც ერთი პროცესის მეხსიერებაში ცხოვრობს. მომდევნო polling მოთხოვნები და შემდეგ upgrade ზუსტად იმავე პროცესს უნდა მიაღწიოს. ერთი პროცესის დროს ეს უფასოა. ორის დროს მოთხოვნების დაახლოებით ნახევარი არასწორზე ჯდება და კლიენტი უსასრულოდ ციკლში ბრუნავს ასეთი პასუხით:
{"code":1,"message":"Session ID unknown"}გამოსავალი სამია, და სამივე პატიოსანია:
- გაუშვი ერთი პროცესი. მცირე გეგმაზე ეს ჩვეულებრივ სწორია. ერთ Node პროცესს ათასობით socket-ის დაჭერა შეუძლია და პრობლემას არ მართავ, არამედ აქრობ.
- გახადე load balancer sticky. nginx-ში ეს არის
ip_hashან კლიენტის მისამართზე consistent hash. ის მყიფეა: ყველა, ვინც ერთი ოფისის NAT-ის, ერთი მობილური ოპერატორის ან ერთი CDN-ის უკან დგას, ერთსა და იმავე worker-ზე ხვდება. - გამოტოვე polling. კლიენტზე
transports: ["websocket"]upgrade-ს მაშინვე აგზავნის და polling სესიას არასოდეს ქმნის, ამიტომ stickiness მნიშვნელობას კარგავს. ფასი ისაა, რომ იმ მცირე რაოდენობის ქსელებისთვის, რომლებიც WebSocket-ებს სრულად ბლოკავს, fallback არ გაქვს.
upstream app { ip_hash; server 127.0.0.1:3000; server 127.0.0.1:3001;}Stickiness routing-ს წყვეტს და არა state-ს. ორ პროცესს მაინც ორი ცალკე ოთახების ნაკრები აქვს, ამიტომ worker A-ზე გაგზავნილი შეტყობინება worker B-ზე მყოფ კლიენტს არასოდეს მიაღწევს. ამას სჭირდება adapter - საერთო bus, რომელზეც ყველა პროცესია გამოწერილი. Node-ის @socket.io/cluster-adapter (@socket.io/sticky-თან ერთად) ამას cluster IPC არხით აკეთებს დამატებითი სერვისის გარეშე, რაც ერთ მანქანაზე სწორი პირველი არჩევანია. Redis adapter უფრო დიდი მასშტაბის ჩვეულებრივი პასუხია, მაგრამ Redis მეორე რამეა, რომელიც უნდა გაუშვა და გადაუხადო; Redis და საჭიროა თუ არა ის უკვე წასაკითხია მის დამატებამდე, ხოლო ფონური დავალებები პატარა სერვერზე ამბობს, რა ქნა, როცა ის არ გაქვს.
კიდევ ორი Socket.IO დეტალი, რომელიც კბენს:
- ნაგულისხმევი გზა არის
/socket.io/. თუ მხოლოდlocation /socket.io/-ს აპროქსირებ და შემდეგ მეორე namespace-ს დაამატებ, ისიც ამ გზაზე გაივლის - namespace-ები URL-ები არ არის. maxHttpBufferSizeნაგულისხმევად 1 MB-ია. უფრო დიდი შეტყობინებები კავშირს აშკარა შეცდომის გარეშე ხურავს. გაზარდე შეგნებულად, ან უკეთესია, socket-ში მეგაბაიტებს ნუ ატან.- შეტყობინებაზე შეკუმშვა (
perMessageDeflate) Socket.IO v3-ში და მის ზემოთ ნაგულისხმევად გამორთულია. გამორთულია მიზეზით: ის კავშირზე გაზომვად მეხსიერებას ჭამს. ჩართე მხოლოდ დიდი, განმეორებადი ტექსტური payload-ებისთვის.
კლიენტის ნამდვილი მისამართის მიღება#
როცა proxy დგას, ყველა კავშირი proxy-დან მოდის. WebSocket-ებისთვის ეს უფრო მნიშვნელოვანია, ვიდრე HTTP-სთვის, რადგან abuse-ის ჩვეულებრივი კონტროლი მისამართზე დამოკიდებული კავშირის ლიმიტებია, და გატეხილი კონფიგურაციის დროს ყველა ვიზიტორი ერთ მისამართს იზიარებს.
proxy წერს X-Forwarded-For-ს; შენს აპლიკაციას უნდა უთხრა, რომ დაუჯეროს. Express-ში:
app.set("trust proxy", 1); // number of proxies in front of you, not `true`გამოიყენე რაოდენობა და არა true. true ნიშნავს „ენდე მთელ ჯაჭვს", და რადგან X-Forwarded-For კლიენტის მიერ მოწოდებული header-ია, ნებისმიერს შეუძლია ნებისმიერი მისამართი მოიჩვენოს საკუთარი header-ის გაგზავნით. 1-ის შემთხვევაში Express ბოლო ჩანაწერს იღებს, ესე იგი იმას, რომელიც შენმა proxy-მ დაწერა.
raw ws ბიბლიოთეკით გასამართი framework არ არის - წაიკითხე ის upgrade მოთხოვნიდან თავად:
wss.on("connection", (socket, request) => { const forwarded = request.headers["x-forwarded-for"]; const ip = forwarded ? forwarded.split(",")[0].trim() : request.socket.remoteAddress;});თუ TLS-ს proxy-ზე წყვეტ, X-Forwarded-Proto არის გზა, რომლითაც აპლიკაცია იგებს, რომ ორიგინალი მოთხოვნა დაცული იყო - ეს მნიშვნელოვანია Secure cookie-ებისთვის და ნებისმიერი გადამისამართებისთვის, რომელსაც აპლიკაცია თავად აგებს.
Cloudflare და სხვა proxy-ები შენსას წინ#
Cloudflare WebSocket-ებს ყველა გეგმაზე აპროქსირებს, უფასოზეც, და ჩასართავი გადამრთველი არ არსებობს. ნარინჯისფერი ღრუბლის ჩართვისას სამი რამ იცვლება:
- Idle კავშირები დაახლოებით 100 წამის შემდეგ იხურება. შენი 30-წამიანი heartbeat ამას უკვე ამუშავებს.
- აპროქსირდება მხოლოდ Cloudflare-ის მხარდაჭერილი HTTP და HTTPS პორტები. WebSocket პორტზე 8443 მუშაობს; პორტზე 3000 - არა, რადგან ჩანაწერი grey-cloud უნდა იყოს, რაც origin-ის მისამართს მაინც ამჟღავნებს.
- Bot Fight Mode და აგრესიულმა WAF წესებმა upgrade მოთხოვნას challenge შეიძლება მოუწყონ. ბრაუზერი challenge-ს გადაურჩება; მობილური აპლიკაცია ან server-to-server კლიენტი - არა, და იღებ
403-ს, რომელიც შენს ლოგებში უხილავია. Cloudflare ვებსაიტებისთვის და გეიმ სერვერებისთვის გადის, რომელ ტრაფიკს გაატარებს proxy და რომელს - არა.
თუ proxy-ებს ჯაჭვად აერთებ - Cloudflare, შემდეგ შენი nginx, შემდეგ აპლიკაცია - X-Forwarded-For სიას აგროვებს. მარცხენა ჩანაწერი ორიგინალი კლიენტია, მარჯვენა - უახლოესი proxy, და სანდო ნაწილი მხოლოდ იმდენი ხანია, რამდენიც შენ მართავ ჯაჭვს.
რამდენი კავშირი ეტევა სინამდვილეში#
WebSocket-ები ინდივიდუალურად იაფია და ჯამში ძვირი. დაახლოებით ასე დათვალე:
| რესურსი | ფასი ერთ idle კავშირზე | სად ამოიწურება |
|---|---|---|
| File descriptor-ები | 1 აპლიკაციაში, 2 proxy-ში | ulimit -n, ხშირად ნაგულისხმევად 1024 |
| აპლიკაციის მეხსიერება | 20-60 KB, პლუს ის, რასაც მომხმარებელზე ინახავ | გეგმის მეხსიერების ლიმიტი |
| Proxy-ის მეხსიერება | რამდენიმე KB nginx-ში | იშვიათად პირველი |
| CPU | Idle-ზე თითქმის ნული; broadcast-ზე მთლიანად | ერთდროული fan-out ყველა კლიენტზე |
რიცხვი, რომელიც ხალხს ჰკვირვებს, ბოლო სტრიქონია. ათი ათასი idle socket თითქმის არაფერი ჯდება; ათი ათასი socket, რომელთაგან თითოეული ყოველ წამს 2 KB განახლებას იღებს, არის 20 MB/s სერიალიზაცია და syscall-ები, და ის ერთ ბირთვს გაჯერებს. გამოსწორება თითქმის ყოველთვის ისაა, რომ ნაკლები გააგზავნო: განახლებები tick-ებად გააერთიანე, მთელი ობიექტების ნაცვლად delta-ები გააგზავნე და broadcast მხოლოდ იმ ოთახს გაუკეთე, ვისაც სჭირდება.
ორი პრაქტიკული ლიმიტი, რომელიც უნდა გაზარდო, სანამ ეგზოტიკურს მოძებნი. ulimit -n პროცესისთვის, რადგან 1024-იანი ნაგულისხმევი დაახლოებით ათას მომხმარებელს გიზღუდავს. და worker_connections nginx-ში (ნაგულისხმევი 512 ან 1024 build-ის მიხედვით), იმის გახსენებით, რომ დაპროქსირებული კავშირი ორს ხარჯავს. კონტეინერზე დაფუძნებულ ჰოსტზე ორივე ჩვეულებრივ გონივრულად არის დაყენებული; შენს საკუთარ VDS-ზე - არა.
პატარა გეგმაზე მეხსიერება ნამდვილი ჭერია. თუ შენი აპლიკაცია 1 GB დონეზეა, ზუსტად კავშირზე state არის ის, რაც უნდა გაზომო, და ჩავარდნა უეცარია: RE:NODE-ზე მეხსიერების ლიმიტამდე მისვლა კონტეინერს აჩერებს და სუფთად რესტარტავს, swap-ში ჩაძირვის ნაცვლად, რაც WebSocket აპლიკაციისთვის ნიშნავს, რომ ყველა კლიენტი ერთსა და იმავე წამს ხელახლა უერთდება. ამ thundering herd-ისთვის კლიენტზე შემთხვევითი reconnect დაყოვნებით ღირს მომზადება. Node-ის მეხსიერების ლიმიტები ახსნილი ამის heap მხარეს ეხება, ხოლო graceful shutdown და health check-ები socket-ების თავაზიანად დახურვას აღწერს, რომ reconnect-ები გადანაწილდეს.
პრობლემების გადაჭრა#
`400 Bad Request` handshake-ზე. Proxy-მ header-ები შეჭამა. ჯერ proxy_http_version 1.1 შეამოწმე, შემდეგ ორი proxy_set_header ხაზი. დაადასტურე curl-ით - მომუშავე endpoint პასუხობს 101-ით:
$ curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ https://app.example.com/ws`426 Upgrade Required`. მიაღწიე რაღაცას, რომელიც მხოლოდ WebSocket-ით საუბრობს, უბრალო HTTP მოთხოვნით. ჩვეულებრივ ბრაუზერი ან health check პირდაპირ socket-ის გზას ეხება.
`502 Bad Gateway`. აპლიკაცია იმ პორტზე არ უსმენს, რომელზეც proxy არის მიმართული, ან 127.0.0.1-ზეა მიბმული, მაშინ როცა proxy სხვა კონტეინერშია. მიაბი 0.0.0.0-ს, როცა proxy იმავე ჰოსტზე არ არის, და შეამოწმე, აპლიკაცია მართლა გაიშვა თუ არა.
ერთდება და შემდეგ ზუსტად 60 წამში იხურება. proxy_read_timeout. გათიშვის ლოგში ზუსტად მრგვალი რიცხვები ყოველთვის timeout-ია, და რიცხვი გეუბნება, რომელი hop.
`WebSocket is closed before the connection is established`. Chrome-ის ფორმულირება იმისთვის, რომ „handshake არასდროს დასრულებულა". ქსელის ტაბში ეძებე 301/302 - http-დან https-ზე გადამისამართება ან ბოლოში slash-ის გადამისამართება ამას გამოიწვევს, რადგან WebSocket კლიენტები გადამისამართებას არ მიჰყვებიან.
Mixed content დაბლოკილია. https://-ით მიღებულ გვერდს ws://-ის გახსნა არ შეუძლია. ააგე URL გვერდიდან: ` ${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/ws `.
ლოკალურად მუშაობს, production-ში ვერა, არსად არანაირი შეცდომა. იეჭვე რაღაც შენსა და აპლიკაციას შორის, რაც შენ არ დაგიყენებია: CDN, ოფისის proxy, კორპორატიული TLS interceptor. გამოსცადე ტელეფონიდან მობილურ ინტერნეტზე. თუ იქ მუშაობს, პრობლემა ქსელშია და არა შენს კონფიგურაციაში.
კავშირები გროვდება და არასოდეს ეცემა. არ არის heartbeat, ან სერვერი არასოდეს იძახებს terminate()-ს socket-ზე, რომელმაც pong ვერ გამოაგზავნა. შეადარე ღია socket-ების რაოდენობა იმას, თუ რამდენი ადამიანი იყენებს აპლიკაციას სინამდვილეში.
FAQ#
მჭირდება თუ არა ცალკე პორტი ან ქვედომენი WebSocket-ებისთვის?
არა. upgrade ჩვეულებრივი მოთხოვნაა ჩვეულებრივ გზაზე იმავე ჰოსტსა და პორტზე, და იმავე სერტიფიკატს ფარავს. ცალკე ქვედომენი მხოლოდ მაშინ გამოდგება, თუ WebSocket ტრაფიკის სხვა პროცესზე მიმართვა ან სხვადასხვა proxy timeout-ების გამოყენება გინდა.
რატომ წყდება ჩემი socket ზუსტად 60 წამში?
იმიტომ, რომ ჯაჭვში რაღაცას 60-წამიანი idle timeout აქვს - nginx-ის proxy_read_timeout და Apache-ის ProxyTimeout ორივე ნაგულისხმევად ასეა. გაზარდე იმ hop-ზე, რომელსაც მართავ, და გააგზავნე ping ყოველ 30 წამში, რომ კავშირი არასოდეს იყოს idle.
მჭირდება თუ არა sticky session-ები WebSocket-ებისთვის?
მხოლოდ მაშინ, თუ ერთზე მეტ პროცესს უშვებ და იყენებ ბიბლიოთეკას, რომელიც HTTP polling-ით იწყება, მაგალითად Socket.IO ნაგულისხმევი კონფიგურაციით. ერთ პროცესს არაფერი სჭირდება. WebSocket ტრანსპორტის იძულებითი არჩევა მოთხოვნასაც აქრობს, polling fallback-ის დაკარგვის ფასად.
შემიძლია თუ არა WebSocket-ების გამოყენება Cloudflare-ით?
დიახ, ყველა გეგმაზე, ჩართული proxy-ით და შესაცვლელი პარამეტრის გარეშე. მოელოდე, რომ idle კავშირები დაახლოებით 100 წამის შემდეგ დაიხურება, ამიტომ heartbeat შეინარჩუნე და თვალი ადევნე bot-დაცვის წესებს, რომლებიც არა-ბრაუზერულ კლიენტებს challenge-ს უწყობს.
რამდენი WebSocket კავშირი დაიტევს ერთი პატარა სერვერი?
ათასობით, თუ ისინი ძირითადად idle-ია: თითოეულზე 20-60 KB მეხსიერება დათვალე, პლუს შენი მომხმარებელზე state, და გაზარდე file-descriptor ლიმიტი. CPU-ს პირველად კავშირების რაოდენობა კი არა, broadcast-ის სიხშირე ამოწურავს.
გამოვიყენო თუ არა Server-Sent Events?
თუ მონაცემები მხოლოდ სერვერიდან კლიენტისკენ მიედინება - ცოცხალი ლოგები, პროგრესი, შეტყობინებები - SSE უფრო მარტივია: ის უბრალო HTTP პასუხია, თავად ხელახლა უერთდება და upgrade header-ების ნაცვლად proxy_buffering off სჭირდება. WebSocket-ები აირჩიე მაშინ, როცა კლიენტსაც უწყვეტად უნდა შეეძლოს შეტყობინებების გაგზავნა.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.