QA Interview Hub · База знань · Тести
Мережі
WebSocket, SSE і real-time
WebSocket: handshake, кадри, ping/pong і закриття
WebSocket — постійне двоспрямоване з'єднання поверх TCP: після короткого HTTP-рукостискання обидві сторони можуть надсилати повідомлення в будь-який момент без нових запитів.
Handshake — звичайний HTTP/1.1 запит із Upgrade:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Відповідь 101 означає перехід на новий протокол. Адреса починається з ws:// або wss:// (WebSocket поверх TLS; у продакшені — лише wss). Автентифікація зазвичай при handshake: cookie, токен у query-рядку чи в підпротоколі. Браузерний WebSocket API не дозволяє задавати власні заголовки (наприклад, Authorization), тому токен часто передають першим повідомленням або в URL. Origin перевіряє сервер (захист від cross-site hijacking).
Кадри. Повідомлення — це один чи кілька кадрів: текстові (UTF-8), бінарні та керівні (ping, pong, close). Кадри від клієнта маскуються. Ping/pong перевіряє, що з'єднання живе, і утримує його від idle timeout на проксі; зазвичай ping надсилає сервер, а браузер відповідає pong автоматично. Багато застосунків додають ще й прикладні heartbeat-повідомлення.
Закриття — обмін кадрами close із кодом: 1000 нормальне закриття, 1001 going away (сторінка закривається, сервер перезапускається), 1008 порушення політики, 1009 повідомлення завелике, 1011 внутрішня помилка сервера. Код 1006 — «аномальне закриття»: кадра close не було (обрив мережі, killed процес, таймаут проксі); його не надсилають по мережі, клієнт лише бачить його локально.
Reconnect повинен бути на клієнті: з'єднання рано чи пізно обірветься. Правильна схема — експоненційна затримка з джитером (щоб тисячі клієнтів не кинулися одночасно), ліміт спроб, а після повторного підключення — повторна підписка й синхронізація пропущених подій (наприклад, за останнім id). Повідомлення, надіслані під час обриву, не доставляються автоматично.
WebSocket, SSE чи long polling: що обрати
Усі три способи вирішують одне: показати користувачу зміну на сервері без перезавантаження.
- Short polling: клієнт запитує щосекунди. Простий, але марнує запити й дає затримку до інтервалу;
- Long polling: запит висить, доки не з'явиться подія (або таймаут), після відповіді клієнт одразу робить новий. Працює скрізь, де працює HTTP, але кожна подія коштує запит із заголовками;
- SSE (Server-Sent Events): один HTTP-запит із
Content-Type: text/event-stream, сервер стрімить події. Тільки від сервера до клієнта, лише текст. Браузерний EventSource сам перепідключається й надсилає Last-Event-ID, тому відновлення з місця обриву майже безкоштовне. Це звичайний HTTP: працює через проксі й HTTP/2 без особливостей. На HTTP/1.1 діє ліміт ~6 з'єднань на домен, тому багато вкладок з SSE можуть його вичерпати;
- WebSocket: двоспрямований, текст і бінарні дані, мала затримка, але власний протокол: треба самому робити reconnect, авторизацію, балансування «липких» з'єднань, а проксі й фаєрволи іноді його ріжуть.
Правило вибору. Дані йдуть лише від сервера (нотифікації, прогрес, стрімінг відповіді LLM, котирування) — SSE, і вистачить. Інтенсивна двостороння взаємодія (чат, спільне редагування, ігри, торгові термінали) — WebSocket. Рідкі оновлення та суворі мережеві обмеження — polling. Для тестування різниця суттєва: SSE — звичайний HTTP-відгук, який можна перехопити й замокати як відповідь, а WebSocket потребує окремих API (див. наступну статтю).
Як тестувати WebSocket: Playwright і навантаження
Що перевіряти. Handshake й авторизацію (без токена — відмова), формат повідомлень, порядок і ідемпотентність при повторній доставці, поведінку UI при обриві й перепідключенні, ліміт розміру, права на канали, закриття з правильним кодом, поведінку після offline.
Спостереження. Playwright віддає подію websocket для кожного з'єднання сторінки; у неї є framesent, framereceived, close і socketerror. Поле payload — рядок або Buffer.
const wsPromise = page.waitForEvent('websocket');
await page.goto('/chat');
const ws = await wsPromise;
const reply = ws.waitForEvent('framereceived', {
predicate: (e) => String(e.payload).includes('"type":"message"'),
});
await page.getByRole('textbox').fill('hello');
await page.keyboard.press('Enter');
expect(JSON.parse(String((await reply).payload)).text).toBe('hello');
Подію треба підписати до навігації, інакше пропустите з'єднання, відкрите під час завантаження.
Підміна. page.routeWebSocket(url, handler) (з Playwright 1.48) перехоплює WebSocket. Без connectToServer() сервера немає взагалі: тест сам відповідає, і не потрібен справжній бекенд. З connectToServer() створюється проксі, у якому можна підгледіти чи змінити повідомлення.
await page.routeWebSocket('wss://example.com/ws', (ws) => {
ws.onMessage((message) => {
if (message === 'ping') ws.send('pong');
});
ws.send(JSON.stringify({ type: 'welcome' })); // подія від «сервера»
});
// режим проксі: дописати до справжнього сервера
await page.routeWebSocket(/\/ws$/, (ws) => {
const server = ws.connectToServer();
ws.onMessage((m) => server.send(m));
server.onMessage((m) => ws.send(m));
});
Обрив емулюють ws.close({ code: 1011, reason: '...' }) від імені «сервера» і перевіряють, що клієнт перепідключився. Припущення: тест має чекати не waitForTimeout, а події (waitForEvent, веб-асерти).
Навантаження. Playwright для цього не підходить. Використовують k6 (модуль k6/websockets) чи Artillery (двигун ws): тисячі віртуальних клієнтів, сценарії «підключитися, підписатися, слухати N секунд». Вимірюють кількість одночасних з'єднань, затримку доставки, частку помилок підключення, поведінку при перезапуску сервера (шторм перепідключень) і витоки пам'яті.