vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    WebSockets і реалтайм

    Зміст

    Чат, у якому повідомлення зʼявляється без перезавантаження. Дашборд, де цифра змінюється сама. Статус замовлення, що переїжджає з «Готується» в «У дорозі», поки ти дивишся на екран. За всім цим — одна зміна: ініціативу віддали серверу.

    Для тестувальника це окремий клас поведінки: дані приходять асинхронно, а звичне «зробив запит — перевірив відповідь» не працює, бо запиту немає. Глава — канон теми на сайті: тут повний виклад, решта глав розділу посилаються сюди.

    Чому запиту-відповіді не вистачає

    HTTP побудований на асиметрії ролей: клієнт встановлює зʼєднання й надсилає запит, сервер слухає й відповідає на те, що прийшло. Сервер не знає наперед, хто до нього прийде, і не має способу заговорити першим.

    Доки не було нічого кращого, двобічний обмін імітували (polling): клієнт раз на N секунд питав «є щось нове?», а власні повідомлення слав окремими запитами. Специфікація WebSocket називає це прямо — зловживання HTTP. Ціна не лише в порожніх відповідях: кожна ітерація — повний цикл із заголовками, кукі й автентифікацією, а новина, що зʼявилася одразу після опитування, чекає наступного. long polling майже прибирає — запит висить, доки не буде що віддати, — але кожне повідомлення коштує закриття одного циклу й відкриття наступного. Мета WebSocket так і сформульована: двобічний звʼязок без багатьох HTTP-зʼєднань і .

    Що не змінюється — ініціатором зʼєднання лишається клієнт: WebSocket не робить зі сторінки сервер, він дозволяє серверу говорити всередині вже відкритого каналу.

    Рукостискання: HTTP відчиняє двері й виходить

    Протокол складається з двох частин: відкривальне (handshake) і передача даних. Рукостискання навмисно зроблене звичайним HTTP-запитом GET із заголовками Upgrade: websocket, Connection: Upgrade і Sec-WebSocket-Key — щоб один порт обслуговував і HTTP-, і WebSocket-клієнтів.

    Успіх означає рівно один статус — 101 Switching Protocols із заголовком Sec-WebSocket-Accept. Будь-який інший код каже, що рукостискання не завершилося й далі діє звичайна семантика HTTP: 401 в автентифікації, 404 — немає шляху, 200 — сервер відповів як на звичайний запит і каналу не відкрив. Немає 101 — шукати кадри марно.

    Після 101 HTTP закінчується: WebSocket — самостійний протокол поверх TCP, і повʼязує їх лише те, що рукостискання розпізнається HTTP-серверами як апгрейд. Наслідок великий — заголовки, кукі й автентифікація на кожне повідомлення більше не потрібні, вони пішли один раз.

    СерверКлієнтСерверКлієнтдалі це вже не HTTP — кадри поверх того самого TCPGET /chat + Upgrade websocket + Sec-WebSocket-Key101 Switching Protocols + Sec-WebSocket-Acceptкадр даних (маскований)кадр без запиту з боку клієнтаPingPongClose 1001СерверКлієнтСерверКлієнтдалі це вже не HTTP — кадри поверх того самого TCPGET /chat + Upgrade websocket + Sec-WebSocket-Key101 Switching Protocols + Sec-WebSocket-Acceptкадр даних (маскований)кадр без запиту з боку клієнтаPingPongClose 1001

    TCP під ним: що надійність дає і чого не покриває

    WebSocket не винаходить транспорт: він лягає поверх TCP і гарантії бере звідти. TCP дає надійний упорядкований потік байтів: втрати виявляються за номерами послідовності й контрольними сумами, а лікуються повторною передачею; заразом TCP керує потоком і перевантаженням. Зʼєднання встановлюється тристороннім рукостисканням SYNSYN-ACKACK і далі ідентифікується парою сокетів.

    Звідси два наслідки. Перший: порядок кадрів у межах зʼєднання гарантований транспортом. Другий, важливіший: гарантія закінчується разом зі зʼєднанням — надійність це виявлення втрат і повторна передача в живому каналі, а не обіцянка, що канал доживе до кінця сценарію. Протилежний вибір — UDP: без зʼєднання й без гарантій, зате з мінімальними накладними витратами.

    Ціна не нульова: перед першим корисним байтом проходить TCP-рукостискання, а для захищеного каналу ще й TLS, і ця затримка потрапляє в кожен . Різниця з полінгом у тому, що тут вона платиться один раз: HTTP теж уміє перевикористовувати зʼєднання замість нового рукостискання, але щойно воно закрилося — наступний цикл починає з нуля.

    ws і wss: чому на HTTPS-сторінці працює лише wss

    Специфікація визначає дві схеми адрес: ws (типовий порт 80) і wss (443) — той самий протокол, але поверх TLS. Схема тут така сама частина URL, як http і https (див. главу URL і кодування), і різниця не косметична: wss дає ті самі три гарантії, що й HTTPS, — автентифікацію сервера, конфіденційність і цілісність. бачить IP-адресу й доменне імʼя (SNI у рукостисканні TLS), але не вміст кадрів.

    Головне для практики: сторінка, відкрита по https, — захищений контекст, і ws:// із неї дає (mixed content). Поділ простий: вміст або «оновлюваний» (браузер сам переписує схему на https), або «блокований», причому блокованим вважається все, що не входить до переліку оновлюваних, — а там лише зображення, аудіо й відео, задані атрибутом src, та зображення з CSS (background-image і подібні). Тому ws:// мовчки не «оновиться» — його заблокують.

    Три наслідки, що економлять годину дебагу:

    • слід завжди є в — браузер пише попередження і про оновлення, і про блокування;
    • на локальному стенді дефект не відтворюється: file: і петльові адреси (http://127.0.0.1/, http://localhost/) вважаються захищеними , тож перевіряти треба на домені;
    • на стенді з IP-адресою замість домену блокується навіть оновлюваний запит — окреме правило, яке ловить внутрішні середовища.

    Кадр — це не запит

    Після рукостискання дані передаються послідовністю кадрів (frames). Кадр не має ні методу, ні шляху, ні і — головне — не має пари: сервер надсилає його, коли має що сказати. Звідси складнощі тестування — чекати на кадр за номером запиту неможливо, бо номера немає.

    Асиметрія в іншому: клієнт зобовʼязаний маскувати всі кадри, які шле серверу, випадковим 32-бітним ключем, а сервер маскувати свої не має права. Це не приватність: від підслуховування захищає TLS у wss, а не маскування — по ws:// кадри йдуть відкрито. Маскування захищає проміжні вузли: щоб скрипт не сформував байти, які прийме за HTTP-запит і покладе собі в кеш.

    Поверх даних є керівні кадри. Ping і Pong — вбудований keepalive: сторона, що отримала Ping, мусить відповісти Pong із тими самими даними, якщо вже не отримала кадр Close. Нюанс: браузерний API керувати цими кадрами з JavaScript не дає, тому застосунки будують власний heartbeat звичайними повідомленнями — у трафіку ти побачиш саме його.

    З боку сторінки зʼєднання видно як обʼєкт зі станом readyStateCONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3) — і чотирма обробниками: open, message, error, close.

    Розрив, коди закриття й перепідключення

    Закриття буває двох сортів, і плутати їх дорого. Нормальне — кадр Close з кодом: 1000 означає, що зʼєднання виконало своє призначення, 1001 — що сторона «йде» (сервер лягає або браузер пішов зі сторінки). Аварійне — коли кадру Close не було взагалі: обірвався Wi-Fi, помер процес на бекенді, проксі закрив неактивне зʼєднання.

    Тут живе код, який найчастіше бачать у логах тестів, — 1006. Специфікація прямо каже: це зарезервоване значення, і ставити його в кадр Close заборонено — його виставляє сама реалізація, коли зʼєднання закрилося ненормально. По мережі він не передається ніколи й існує лише в обробнику close. Побачив 1006 — читай як «канал обірвався», а не «сервер щось сказав».

    Відновлення — робота клієнта, а не протоколу, і вимоги специфікація адресує саме йому: після аварійного закриття клієнт SHOULD уживати (backoff), першу спробу відкладати на випадковий час, а наступні — на дедалі довші, наприклад за схемою урізаного двійкового експоненційного нарощування (truncated binary exponential backoff). Механізму «продовжити стару сесію» немає: після перепідключення це нове зʼєднання з новим рукостисканням, тож автентифікацію й підписки застосунок відновлює сам.

    Перезавантаження сторінки: реалтайм починається з нуля

    Чи переживе зʼєднання перезавантаження сторінки? Відповідь коротка — ні. WebSocket не є властивістю вкладки чи домену: обʼєкт зʼєднання живе в контексті виконання JavaScript конкретного документа, і разом із документом зникає.

    Наслідок для реалтайму більший, ніж здається, бо стан у WebSocket не зберігається ніде, крім самого зʼєднання. Механізму «продовжити стару сесію» протокол не має — після перезавантаження це нове зʼєднання з новим рукостисканням, а отже, і нова автентифікація, і нова підписка на потрібні події.

    Наша практика (не канон). Того, що зʼєднання вбиває саме перезавантаження, джерело не каже — це наш вивід із того, що обʼєкт живе в контексті виконання сторінки. Робочі правила звідси такі: після кожного page.reload() або переходу на нову сторінку стан реалтайму скидається повністю, тож розраховувати, що підписка чи отримані до перезавантаження повідомлення «доживуть» до наступного кроку тесту, не можна — після перезавантаження перевірку починають із того, що канал відкрився знову.

    Сусіди по темі: SSE і полінг

    WebSocket — не єдиний спосіб зробити реалтайм, і вибір між варіантами треба вміти обґрунтувати. Найближчий сусід — Server-Sent Events (SSE): однобічний канал сервер → клієнт через інтерфейс EventSource, де сервер віддає відповідь із типом text/event-stream. Назад події слати не можна.

    Дві властивості SSE варто знати напамʼять. Перша: перепідключення вбудоване — обірване зʼєднання браузер відновлює сам, сервер може задати паузу полем retry, а зупиняє потік лише метод .close(). Друга: без HTTP/2 браузер тримає щонайбільше шість SSE-зʼєднань, причому ліміт рахується парою «браузер + домен» і дійсний на всі вкладки разом; HTTP/2 його не знімає, а замінює на узгоджуване число з дефолтом 100.

    СпосібНапрямокХто ініціює обмінПерепідключення
    ПолінгКлієнт питаєКлієнт, за таймеромКожен цикл окремий
    long pollingВідповідь чекає на подіюКлієнт, після відповідіНовий запит на повідомлення
    SSEЛише сервер → клієнтКлієнт відкриває каналВбудоване в браузер
    WebSocketВ обидва бокиКлієнт відкриває каналПише застосунок

    Межа теми: черги повідомлень, WebRTC і мобільні push сюди не входять — це окремі теми зі своєю механікою.

    Що видно у вкладці Network

    Мережева панель бачить не тільки HTTP: серед фільтрів типів ресурсів є кнопка WS, а DevTools від версії 99 тротлить і WebSocket-зʼєднання. У списку запитів зʼєднання — один рядок, те саме рукостискання зі статусом 101; кадри окремими рядками не додаються, тож сто повідомлень не дадуть ста запитів.

    Щоб побачити кадри, клацни URL зʼєднання в колонці Name і відкрий вкладку Messages: таблиця показує останні 100 повідомлень із колонками Data, Length і Time, а напрямок передано кольором — вихідні світло-зелені, вхідні білі. Решта панелі — у главі DevTools: вкладка Network і дебаг.

    Що саме перевіряє QA

    Функціональні перевірки реалтайму зводяться до чотирьох питань, і всі випливають із механіки вище.

    • Доставка. Чи дійшло повідомлення до потрібного клієнта — і чи не дійшло до чужого. Перевірка двома сесіями: дія в одній, наслідок в іншій.
    • Порядок. Порядок кадрів гарантує транспорт, тож не в мережі, а в застосунку: чи не переставить їх фронтенд, отримавши два оновлення того самого обʼєкта підряд.
    • Перепідключення. Що робить клієнт після аварійного закриття: чи пробує знову, чи нарощує паузу, чи не влаштовує шторм запитів на сервер.
    • Стан після розриву. Після перепідключення це нове зʼєднання, тож предмет перевірки — відновлення стану: чи повернулися підписки, чи не загубилися повідомлення з моменту розриву, чи не показує UI застаріле як актуальне.

    Правило автотестів одне: кадр не має пари, тому чекати треба на наслідок, а не на час. Жорстка пауза або зайва, або замала.

    // Чекаємо на наслідок кадру в інтерфейсі
    await expect(page.getByTestId('order-status')).toHaveText('У дорозі');
    
    // Так не треба
    await page.waitForTimeout(3000);

    Сам трафік теж доступний із тесту, і не лише очима: інструмент автоматизації підтримує огляд, й модифікацію WebSocket «з коробки». Точка входу — подія, що спрацьовує на кожне створення сокета; далі з обʼєкта зʼєднання слухають окремі кадри в обидва боки.

    page.on('websocket', (ws) => {
      ws.on('framesent', (f) => console.log('→', f.payload));
      ws.on('framereceived', (f) => console.log('←', f.payload));
    });

    Це дає перевірку не лише наслідку в інтерфейсі, а й самого обміну: чи клієнт справді підписався, чи після дії в UI пішов очікуваний кадр. Докладніше про очікування — у главі Практичні сценарії AQA: флак і синхронізація.

    Наша практика (не канон). Далі — те, як це роблять команди, з якими ми працювали; окремого стандарту під це немає. Реалтайм ми вважаємо стабільним джерелом (flakiness) — це наша оцінка, а не теза джерела: повідомлення приходять непередбачувано в часі, і тест, який на це не розрахований, падає раз на N прогонів. Одне зʼєднання тримає стан підписок і чергу повідомлень, тому реалтайм-тести чутливі до ізоляції: свіжий на тест дешевший за полювання на протікання стану. Стратегій три: очікування UI-наслідків перевіряє інтеграцію з живим сервером; мокання WebSocket (сама техніка — у главі Перехоплення й мокання мережі) — поведінку фронтенду й сценарії розриву; на реальні кадри з таймінгом «на око» — найкрихкіший варіант.

    Типові помилки

    • «Зʼєднання не відкрилося — значить, WebSocket зламаний». Насправді статус рукостискання каже, що сталося: усе, крім 101, — звичайна HTTP-відповідь, і 401 чи 404 діагностуються як завжди.
    • «У Network немає кадрів — реалтайм не працює». Насправді кадрів там і не буває: у списку запитів лише рукостискання, а повідомлення — у вкладці Messages усередині зʼєднання.
    • «Сервер закрив зʼєднання з кодом 1006». Насправді 1006 заборонено передавати в кадрі Close — його ставить локальна реалізація, коли канал обірвався без прощання. Це діагноз мережі, а не слова сервера.
    • «Кадри маскуються, отже, зашифровані». Насправді маскування захищає проміжні вузли від підробленого HTTP-запиту, а шифрування дає лише wss: по ws:// вміст кадрів читається відкрито.
    • «На стенді ws:// працює — значить, і в проді запрацює». Насправді локальні адреси вважаються захищеними походженнями, тож блокування там не спрацьовує; на домені з https той самий код помре з попередженням у консолі.
    • «Підписка переживе перезавантаження — це ж та сама вкладка». Насправді зʼєднання живе в контексті виконання документа: після page.reload() буде нове рукостискання, нова автентифікація й нова підписка.

    Підсумок

    • WebSocket — це рукостискання плюс кадри поверх TCP. HTTP потрібен рівно один раз; ознака успіху — 101, інший код означає, що каналу немає.
    • Ініціатива зміщується, ролі — ні. Зʼєднання відкриває клієнт, але всередині каналу сервер шле повідомлення сам — тому в кадра немає пари, і тест чекає на наслідок.
    • На HTTPS-сторінці живе лише wss. ws:// із захищеного контексту блокують як змішаний контент; на локальних адресах цього не видно.
    • TCP гарантує порядок і доставку, доки живе зʼєднання. Розрив гарантії скасовує: 1006 — обрив без прощання, а перепідключення з бекофом пише застосунок.
    • Стан після перепідключення — окремий предмет тесту. Нове зʼєднання не успадковує ні автентифікації, ні підписок: «канал відновився» і «застосунок працює як раніше» — різні твердження.

    Можливі питання

    • «Чим WebSocket відрізняється від HTTP?» Чекають механізм, а не список властивостей: одне зʼєднання замість циклу запитів, ініціатива сервера всередині каналу, відсутність заголовків на кожне повідомлення. Сильна відповідь називає й межу — HTTP потрібен для рукостискання.
    • «Що відбувається під час апгрейду зʼєднання?» Перевіряють, звідки береться 101: GET із Upgrade: websocket і Sec-WebSocket-Key, відповідь 101 Switching Protocols із Sec-WebSocket-Accept, далі HTTP-семантика закінчується.
    • «Чому на HTTPS-сторінці не працює ws:// Дивляться, чи звʼязуєш тему зі змішаним контентом, а не переказуєш правило. Хороша відповідь додає, чому дефект доїжджає до продакшену: на localhost блокування не відтворюється.
    • «Як ти тестуватимеш чат?» Часта форма питання на мідл-рівні: перевіряють структуру мислення. Сильна відповідь розкладає задачу на доставку, порядок, перепідключення й стан після розриву — і проговорює, що чекати треба на подію, а не на секунди.
    • «Коли краще SSE, а коли WebSocket?» Чекають критерій: потрібен зворотний канал — WebSocket; вистачає односторонніх оновлень — SSE з вбудованим перепідключенням. Плюсом — згадка про ліміт зʼєднань SSE на всі вкладки.

    Джерела

    Чому запиту-відповіді не вистачає

    Рукостискання: HTTP відчиняє двері й виходить

    TCP під ним: що надійність дає і чого не покриває

    ws і wss: чому на HTTPS-сторінці працює лише wss

    Кадр — це не запит

    Розрив, коди закриття й перепідключення

    Перезавантаження сторінки: реалтайм починається з нуля

    Сусіди по темі: SSE і полінг

    Що видно у вкладці Network

    Що саме перевіряє QA

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.