WebSocket
протокол постійного двостороннього з'єднання

Фото: Arulonline / Pixabay 
Фото: It_was_a_pleasure / Pixabay
Застосуйте поняття до рішення
Застосуйте «Що таке WebSocket?» до конкретного рішення. Ми можемо пов'язати поняття із системою, яку ви створюєте або купуєте. Та частина, якої користувач не бачить: де зберігаються дані, які діють правила і як інші системи можуть із нею взаємодіяти.
Перевірте припущення за терміном
Використайте пояснення та пов'язані роботи, щоб сформулювати запитання для проєкту. Ми можемо перевірити припущення разом до вибору інструмента. Спроєктувала й підтримувала 6 пайплайнів ETL/ELT — 6 пайплайнів ETL на трьох рушіях баз даних
Спроєктувала й підтримувала 6 пайплайнів ETL/ELT Зв'язатися з нами
Що це таке
WebSocket — це протокол, який перетворює один запит HTTP на з’єднання, куди
обидві сторони можуть писати будь-коли і яке лишається відкритим, доки одна з
них його не закриє. Його описує RFC 6455, що вийшов у грудні 2011 року, а ту
частину, яку браузер відкриває скриптам, — об’єкт WebSocket — визначає
WebSockets Standard від WHATWG. У коді й документації назву часто пишуть
«websocket» або «web socket», а множина WebSockets зазвичай означає технологію
загалом.
З’єднання починається зі звичайного запиту GET за HTTP/1.1. Клієнт додає
Upgrade: websocket і Connection: Upgrade, Sec-WebSocket-Version зі
значенням 13 та Sec-WebSocket-Key — випадкові 16 байтів, закодовані в base64.
Сервер, який погоджується, відповідає кодом стану 101 Switching Protocols і
полем заголовка Sec-WebSocket-Accept. Це значення доводить, що сервер прочитав
саме це рукостискання, а не повторює стару відповідь: це ключ клієнта з
дописаним до нього фіксованим GUID, хешований SHA-1 і закодований у base64. Від
цієї миті HTTP на з’єднанні закінчується, і сторони обмінюються кадрами.
Кадр невеликий. Його заголовок займає щонайменше два байти, виростає ще на два або вісім, коли корисне навантаження довше за 125 байтів, і ще на чотири в кадрах від клієнта, бо ті несуть ключ маскування. Кожен кадр має код операції: текст, що мусить бути коректним UTF-8, двійкові дані або продовження, коли повідомлення розбито на кілька кадрів, а також три керувальні кадри — для закриття, ping і pong. Кожен кадр, який надсилає клієнт, маскується новим випадковим 32-бітним ключем, а сервер не маскує ніколи. Маскування — не шифрування. Воно існує для того, щоб ворожий скрипт не міг вибрати точні байти, які підуть мережею, і з їхньою допомогою отруїти кешувальний проксі десь на шляху.
Адреси починаються з ws:// або wss://, за замовчуванням використовують порти
80 і 443, а wss веде весь обмін усередині TLS. Це ті самі порти, що й у HTTP,
і так задумано: для брандмауерів і проксі, крізь які проходить рукостискання,
воно виглядає як будь-який інший вебзапит.
Чому це важливо
HTTP — це запит і відповідь, тож сервер говорить, лише коли його питають. Багатьом продуктам потрібне протилежне: повідомлення в чаті, ціна, що щойно змінилася, курсор у спільному документі, фонове завдання, яке завершилося. Без каналу, у який може писати сервер, клієнтові лишається опитування. Він питає кожні кілька секунд, більшість відповідей лише повідомляє, що нічого не змінилося, кожна несе повний набір полів заголовка, а новина чекає до цілого інтервалу, перш ніж її хтось побачить. Відкрите з’єднання WebSocket прибирає обидві витрати. Сервер пише тієї ж миті, коли щось стається, а повідомлення йде з кількома байтами кадру замість цілої сторінки заголовків, тож затримка зводиться до того, що додає сама мережа.
Ціна — це стан. Запит HTTP може потрапити на будь-який сервер за балансувальником навантаження, а з’єднання WebSocket належить одному процесу, доки живе. Цей процес тримає пам’ять під кожне відкрите з’єднання, розгортання чи перезапуск обриває їх усі разом, а кожен проксі й балансувальник навантаження на шляху має пропустити перемикання протоколу й терпіти з’єднання, яке може мовчати хвилинами. Коли серверів більше одного, постає питання, якого звичайний HTTP ніколи не ставить: якщо щось сталося на сервері A, як про це дізнається клієнт, чиє з’єднання живе на сервері B? Звична відповідь — спільний канал publish/subscribe, який слухають усі сервери, і це ще одна система, яку треба підтримувати.
Тож WebSocket — це свідомий вибір, а не відправна точка. Коли новини є лише в
сервера, server-sent events, описані в HTML Standard, надсилають події звичайною
відповіддю HTTP з типом text/event-stream, а браузер сам перепідключається,
коли відповідь обривається. WebSocket виправдовує свої додаткові рухомі частини
тоді, коли клієнтові є що сказати не менше, ніж серверові: у чатах, іграх і
спільному редагуванні документів.
Як це виглядає на практиці
Наведений нижче обмін відкриває з’єднання для чату. Ключ — зразкове значення,
узяте з самого RFC 6455, а значення Sec-WebSocket-Accept — саме те, яке
наводить стандарт і яке на цей ключ мусить повернути будь-який коректний сервер.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Після порожнього рядка, що завершує відповідь сервера, на з’єднанні вже немає HTTP. Браузер виконує все рукостискання одним викликом конструктора й передає решту обробникам подій:
const socket = new WebSocket("wss://example.com/chat");
socket.addEventListener("open", () => socket.send("привіт"));
socket.addEventListener("message", (event) => console.log(event.data));
socket.addEventListener("close", (event) => {
console.log(event.code, event.reason, event.wasClean);
});Подія закриття повідомляє, як завершилося з’єднання. Код 1000 — нормальне закриття, а 1001 означає, що одна зі сторін іде: наприклад, сервер вимикається або браузер залишає сторінку. Код 1006 означає, що з’єднання обірвалося зовсім без кадру закриття. Жодна сторона не має права його надсилати: він існує лише для того, щоб програма зверху могла відрізнити аварійне завершення від чистого.
Межу між моделлю «запит — відповідь» і сокетом добре видно в одному проєкті з портфоліо сайту. Коли API переводили на Huma v2, усі маршрути перейшли на модель запиту й відповіді Huma, крім рівно двох: ендпоінти WebSocket лишилися на чистому Fiber, бо там коду справді потрібен сам сокет, а модель Huma не пасує. Фреймворк, побудований навколо форми самого HTTP, добре описує й перевіряє запити та відповіді, а довгоживуче двостороннє з’єднання — просто інша річ.
Де виникають проблеми
Сторінка з іншого сайту відкриває з’єднання з cookie ваших відвідувачів.
Браузер виконує рукостискання як запит до хоста сервера й додає до нього cookie
цього хоста, звідки б не прийшов скрипт, що попросив з’єднання. Тому сервер,
який автентифікує за cookie і ніколи не дивиться на Origin, прийме з’єднання,
відкрите будь-якою сторінкою в мережі, від імені того, хто якраз її відвідує.
RFC 6455 радить серверові, призначеному лише для певних сайтів, звіряти Origin
з очікуваними й відмовляти решті кодом 403 Forbidden. Робіть це під час
рукостискання, до перемикання протоколу.
Токен доступу потрапляє в журнали. Конструктор WebSocket у браузері
приймає URL і необов’язковий список підпротоколів — і більше нічого, тож скрипт
не може додати поле Authorization. Швидкий обхід — токен в URL, де його
записують зворотні проксі й журнали доступу. RFC залишає автентифікацію
відкритою й називає як варіанти cookie, автентифікацію HTTP і автентифікацію
TLS. Працює cookie разом із перевіркою Origin, описаною вище, і так само
працює короткоживучий одноразовий квиток, який сторінка отримує через
автентифікований виклик HTTP і витрачає на рукостискання.
Перезапуск перетворює всіх клієнтів на лавину. Розгортання закриває всі з’єднання одночасно, кожен клієнт отримує код 1006, і якщо всі вони негайно підключаються знову й не припиняють спроб, сервер, піднявшись, отримує те, що RFC порівнює з атакою на відмову в обслуговуванні. RFC 6455 просить клієнтів почекати випадковий час перед першою спробою, пропонуючи від 0 до 5 секунд, а після кожної невдачі чекати довше, наприклад з усіченою двійковою експоненційною затримкою. Сервер, який перед зупинкою закриває свої з’єднання з кодом 1001, принаймні повідомляє клієнтам, що обрив був запланований.
function reconnectDelay(attempt) {
if (attempt === 0) return Math.random() * 5000;
const ceiling = Math.min(60_000, 1000 * 2 ** attempt);
return Math.random() * ceiling;
}З’єднання вмирають, і ніхто цього не помічає. З’єднання без трафіку не дає жодній стороні знаку, що шлях досі існує, а співрозмовник, який зник без кадру закриття, виглядає точнісінько як той, що просто мовчить, аж до наступного невдалого запису. Для цього й існують ping і pong: сторона мусить відповідати на кожен ping кадром pong, а RFC зазначає, що ping може слугувати і keepalive, і перевіркою того, що інша сторона ще відповідає. API браузера не дає способу надіслати ping, тож таймер має жити на сервері: пінгуйте кожне з’єднання з фіксованим інтервалом і закривайте ті, що не відповіли кадром pong.
Одне повідомлення з’їдає пам’ять сервера. Поле довжини в заголовку кадру
вміщує розміри, на які не вистачить пам’яті жодного сервера, і RFC попереджає,
що ворожа сторона може надіслати один величезний кадр або нескінченну низку
дрібних фрагментів. Реалізації мусять себе захищати, тож задайте в серверній
бібліотеці максимальний розмір повідомлення й закривайте з’єднання кодом 1009,
message too big, коли співрозмовник його перевищує. Те саме накопичення буває й
у зворотному напрямку. У браузері bufferedAmount показує, скільки переданих у
send() байтів досі чекають на відправлення в мережу, і приклад у стандарті
WHATWG надсилає наступне оновлення лише тоді, коли це число дорівнює нулю. На
сервері клієнт, що читає повільно, так само наповнює вихідний буфер, тож кожному
з’єднанню потрібна межа для нього й правило, що робити, коли межу досягнуто.
Що почитати далі
- RFC 6455: The WebSocket Protocol — рукостискання, кадри, маскування й коди закриття, а також міркування безпеки, що стоять за кожним із них.
- WebSockets Standard — API браузера:
конструктор,
send(),close(),bufferedAmountі подія закриття. - RFC 9110: HTTP Semantics — поле
заголовка
Upgradeі код стану 101 Switching Protocols, на які спирається рукостискання. - Server-sent events — односпрямована альтернатива з HTML Standard із вбудованим перепідключенням.

