# WebSocket

Руководство по публичному API DuckoNet 0.11.5 для Python 3.12 x64 на Windows и Linux. Для WebSocket используйте `HttpClient` или `AsyncHttpClient`; для HTTP/3 через прокси задайте `MasqueProxy`. Установите предоставленный wheel для вашей ОС обычным `pip install <путь-к-wheel>` с его зависимостями. API-ключ берите из своей конфигурации, конечные точки `.example` замените адресами ваших серверов.

## WebSocket

Открывайте соединение через уже настроенный `HttpClient` или `AsyncHttpClient`. Используются API-ключ, выбранный браузерный профиль, каталог состояния, CA и cookie jar этого клиента. Требуются совместимые компоненты DuckoNet и закреплённая зависимость `websockets==17.3` из поставки. Проверять возможности можно после входа в контекст клиента: `client.capabilities.websocket`.

```python
import os
from duckonet import HttpClient

with HttpClient(api_key=os.environ["DUCKONET_API_KEY"], browser="chrome") as client:
    with client.websocket("wss://echo.example/ws", origin="https://app.example") as ws:
        ws.send("Привет")
        message = ws.recv()          # str для text, bytes для binary
        print(message)
        ws.send(b"\x00\x01")
        print(ws.recv())
        print(ws.selected_subprotocol)
        print(ws.handshake_response.status_code)  # 101
```

Адреса `.example` в примерах замените своими конечными точками. `websocket()` возвращает менеджер контекста; открытие происходит при входе в `with` / `async with`, выход закрывает соединение.

Все дополнительные аргументы `websocket(url, ...)` передаются по имени. У обоих клиентов одинаковые параметры и значения по умолчанию, приведённые в таблицах ниже. `AsyncHttpClient.websocket()` используется непосредственно с `async with`, без отдельного `await` перед вызовом.

```python
import asyncio
import os
from duckonet import AsyncHttpClient

async def main():
    async with AsyncHttpClient(api_key=os.environ["DUCKONET_API_KEY"], browser="chrome") as client:
        async with client.websocket("wss://echo.example/ws", compression=True) as ws:
            await ws.send("Привет")
            print(await ws.recv())
            await ws.ping(b"probe")
            await ws.close(code=1000, reason="Готово")

asyncio.run(main())
```

| Параметр | Поведение |
|---|---|
| `url` (обязателен) | Строка `ws://` или `wss://`, без credentials и fragment. |
| `origin=None` | Явный Origin; при `None` заголовок Origin не добавляется. Он не выводится из адреса WS. Если `context` пропущен, явный Origin также задаёт контекст верхнего уровня и инициатора для cookies. |
| `subprotocols=()` | Последовательность уникальных строковых HTTP tokens. Строка вместо последовательности недопустима. Выбранное значение доступно как `ws.selected_subprotocol` (`str` или `None`). |
| `compression=True` | Только `bool`. `True` предлагает `permessage-deflate`; сервер вправе его не согласовать, тогда обмен продолжается без сжатия. `False` отключает предложение. Настройка window bits/context takeover отдельно не предоставляется. |
| `headers=None` | Пользовательские заголовки в формате HTTP API: mapping или пары, в том числе повторные. При `None` наследуются настройки клиента; переданные поля объединяются с ними по правилам HTTP API. |
| `cookies=None` | Mapping или пары для одного открытия; при `None` наследуются cookies клиента. Пустой mapping снимает унаследованное разовое наложение, но не отключает cookie jar. Разовые cookies не заменяют управление постоянным jar через `client.cookies`. |
| `proxy` (не передан) | Пропущен — маршрут клиента; `None` — явный прямой маршрут; строка URL или `ProxyRoute` — выбранный обычный прокси. WS использует HTTP/HTTPS/SOCKS5/SOCKS5H, с учётом возможностей профиля/runtime. MASQUE предназначен для строгого H3 и для WS не подходит. |
| `context` (не передан) | Расширенный аргумент: настоящий `BrowserHopContext` с `mode="cors"`, `preflight=False`, без тела; проверяется согласованность cookie-настроек. При пропуске применяется штатный контекст клиента с отображением `ws→http`, `wss→https`. Для обычного использования достаточно `origin`; вручную собирать контекст не требуется. |

`Host`, `Connection`, `Upgrade`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version`, `Sec-WebSocket-Extensions`, `Sec-WebSocket-Protocol` и `Origin` управляются библиотекой: ручная передача этих полей, в том числе в общих headers клиента, отклоняется. Для Origin, subprotocols и compression используйте отдельные параметры. `Proxy-Authorization` задаётся через credentials маршрута. Нельзя сочетать raw `Cookie` с разовым `cookies=`; HTTP cookie-политика остаётся действующей.

При автоматически созданном контексте Referer для открытия WS не добавляется; явные пользовательские заголовки и расширенный контекст сохраняют свои правила.

Set-Cookie ответа открытия обрабатывается общим jar согласно настройкам клиента `cookies_enabled` и `persist_cookies`. Значения этих настроек по умолчанию — `True`. WS не имеет отдельного Python cookie-хранилища.

### Сообщения, сроки и завершение

| Метод | Результат и условия |
|---|---|
| `send(data)` | Принимает ровно `str` или `bytes`, включая пустые; возвращает `None`. Текст кодируется строгим UTF-8. Сообщения при необходимости фрагментируются библиотекой. |
| `recv()` | Возвращает одно целое `str` или `bytes`; собирает фрагменты и обрабатывает управляющие кадры. |
| `ping(payload=b"")` | Принимает `bytes` длиной до 125, отправляет Ping и возвращает `None`; ожидание соответствующего Pong этим методом не предоставляется. Ответ на входящий Ping обрабатывается автоматически. |
| `close(code=1000, reason="")` | Идемпотентное ограниченное по времени закрытие, возвращает `None`. Нужны допустимый протокольный close code и строка reason не длиннее 123 UTF-8 байт. |
| `abort()` | Идемпотентно прерывает соединение и освобождает его ресурсы, возвращает `None`. |

Для `AsyncWebSocket` те же операции выполняются с `await`. Синхронное соединение поддерживает `for message in ws`, асинхронное — `async for message in ws`. Итерация заканчивается без ошибки только при кодах закрытия 1000 или 1001; прямой вызов `recv()` после исчерпания сообщений поднимает `WebSocketClosedError`, в том числе при нормальном закрытии. Сообщения, полностью полученные до закрытия сервером, доступны для чтения; локальное `close()` / `abort()` отбрасывает ожидающие сообщения.

Доступны свойства `closed`, `selected_subprotocol`, `handshake_response` и `generation_receipt`. У `handshake_response` есть `status_code`, `headers` (`Headers`), `url`, `subprotocol`; это ответ открытия, без HTTP body. Заголовки могут содержать конфиденциальные значения: не выводите их целиком в общие журналы. `generation_receipt` относится к поколению runtime/profile, закреплённому для соединения.

| Ограничение | По умолчанию | Допустимые значения |
|---|---:|---|
| `handshake_timeout` | 30 с | Конечное число `int`/`float`, `0 < x <= 300`; `bool` недопустим. |
| `receive_timeout` | 30 с | То же; срок отдельного `recv()`, а не общий срок жизни WS. |
| `send_timeout` | 30 с | То же; срок отдельной отправки `send()` / `ping()`. |
| `close_timeout` | 3 с | То же; окно обмена Close, затем освобождение транспорта. |
| `max_message_bytes` | 16 MiB | Целое 125…64 MiB; граница размера сообщения в байтах, включая UTF-8 текст и данные после распаковки. На входе ограничивается также суммарный payload кадров сообщения до распаковки. |
| `max_frame_bytes` | 1 MiB | Целое 125…`max_message_bytes`; ограничивает входной payload кадра. |
| `max_queue` | 1 | Целое 1…16; в двух ограниченных слоях вместе может находиться до `2 * max_queue` полных полученных сообщений. При заполнении чтение приостанавливается. Это не лимит общего объёма памяти процесса. |

В одном клиенте допускается до 32 открытых или открывающихся WS одновременно. Различные HTTP/proxy routes также ограничены общим лимитом HTTP API (8). Для handshake применяется предел заголовков до 64 KiB; меньший `header_limit` клиента ограничивает принятый ответ дополнительно. HTTP `body_limit` не задаёт размер WS-сообщений.

Разрешены один активный reader и один активный writer на соединение, в том числе одновременно друг с другом. Перекрывающиеся `recv()` либо `send()` / `ping()` поднимают `WebSocketConcurrencyError`. Обрабатывайте сообщения без накопления собственного неограниченного списка.

Истечение срока `recv()` или отправки завершает это соединение. Отмена async-операции также прерывает именно принадлежащее ей соединение и ожидает освобождения native ресурсов, после чего возвращает `asyncio.CancelledError`. Закрытие клиента завершает его WS. Автоматических reconnect и повторной отправки сообщений нет; успешный `send()` сам по себе не подтверждает обработку сообщения сервером. ОС, планировщик и освобождение native ресурсов могут добавить время сверх протокольного close window.

### Ошибки WS

Публичные исключения импортируются из `duckonet`.

| Исключение | Как обрабатывать |
|---|---|
| `WebSocketHandshakeError` | Отклонённый статус или неверные Accept/subprotocol/extensions. `status_code` — HTTP статус либо `None`; например, 301 не вызывает redirect, а 401/407 не считаются успешным открытием. |
| `WebSocketClosedError` | Соединение закрыто; `code` — код закрытия, обычно 1006 при EOF без Close. |
| `WebSocketTimeoutError` | Также наследует `TimeoutError`; `phase` — `handshake`, `receive`, `send` либо `close`. |
| `WebSocketProtocolError` | Недопустимые входные данные либо превышение лимита сообщения; `code`: 1002 для протокольной ошибки, 1007 для UTF-8, 1009 для размера. |
| `WebSocketConcurrencyError` | Уже выполняется операция той же стороны (reader или writer). |
| `WebSocketTransportError` | Ошибка транспорта/освобождения, включая отказ открытия транспорта. |
| `NativeError` с `code == -2` | Новая возможность недоступна в загруженных компонентах; требуется совместимое обновление DuckoNet. |
| `ValueError` / `TypeError` | Недопустимые аргументы, headers, URI, deadline, payload или локальный ресурсный лимит. Ошибки открытия/профиля клиента сохраняют собственные типы HTTP/startup API. |

Все WS-исключения выше, кроме последних двух строк, наследуют `WebSocketError`. TLS, DNS и другие транспортные причины открытия могут быть сведены к безопасному `WebSocketTransportError`: отдельного публичного WS-класса для каждого TLS/proxy отказа нет. Redirect WS всегда отключён, opt-in параметр для него отсутствует.

Если сервер завершил соединение сразу после корректного ответа Upgrade, открытие может
вернуть уже закрытый WS с сохранёнными `handshake_response` и `generation_receipt`.
Чтение сообщает `WebSocketClosedError` (при EOF без Close — код 1006); полностью полученные
сообщения сохраняют обычные правила чтения. Это отличается от отказа самого handshake:
неверный статус или Accept по-прежнему дают `WebSocketHandshakeError`.

WS выполняется через HTTP/1.1 Upgrade. WebSocket-over-HTTP/2 и WebSocket-over-HTTP/3 не предоставляются. Использование профиля и компрессии само по себе не является обещанием полного совпадения трафика с браузером.

## Профиль, cookies и полный прогон

WebSocket и MASQUE используют существующую установку, каталог `state_dir`, API-ключ
и подписанные профили DuckoNet. Для обновления программы сохраняйте прежний `state_dir`:
в нём находятся идентичность установки и история доверия. Cookies живут в памяти клиента;
для сохранения после закрытия приложения используйте export_data() и import_data().
Выбор доступных версий браузера по ключу остаётся общим для HTTP и WS.

Когда используется `version="last"`, клиент подготавливает новые доступные поколения
в фоне. Обрамляйте весь связанный прогон `client.run()`: HTTP, открываемые в нём WS
и потоки используют закреплённое поколение до завершения своих операций.
Следующий внешний `run()` выбирает уже подготовленное поколение; вложенный `run()`
наследует текущий. Обновление не переключает профиль внутри открытого WS.
При явно выбранной версии профиль остаётся закреплённым.

```python
import os
from duckonet import HttpClient

with HttpClient(
    api_key=os.environ["DUCKONET_API_KEY"], browser="chrome", version="last",
) as client:
    with client.run():
        response = client.get("https://app.example/session")
        response.raise_for_status()
        with client.websocket("wss://app.example/events", origin="https://app.example") as ws:
            ws.send("start")
            print(ws.recv())
        print(client.browser_profile)
```

В асинхронном коде используются `async with client.run()` и `async with client.websocket(...)`.
Закрывайте WS и потоки внутри своего `run()`; сам выход из `run()` не является командой
немедленно прервать уже открытое соединение. Без явного `run()` автоматические операции
используют исходное поколение клиента. `client.profile_update_status` показывает состояние
подготовки профиля, а `client.browser_profile` — фактический профиль выбранного прогона.
Поколение не даёт бессрочное право на сетевую работу: действуют обычные проверки
срока действия, отзыва и доступа по ключу, в том числе при обмене по WS.

Cookies принадлежат клиенту и сохраняют штатные domain/path/secure/SameSite правила.
Они доступны HTTP и WS в пределах того же владельца и переживают штатную смену поколения.
Для общего cookie-состояния переиспользуйте один клиент; два независимых клиента не становятся
одним cookie jar только из-за одинакового адреса сервера. `cookies=` — разовое наложение,
а постоянными cookies управляйте через `client.cookies`.

## Поддерживаемые маршруты

| Сценарий | Поддержка |
|---|---|
| `ws://` / `wss://` напрямую | HTTP/1.1 Upgrade; текст, binary и permessage-deflate по согласованию. |
| WS через обычный HTTP/HTTPS/SOCKS5/SOCKS5H proxy | С учётом возможностей профиля и транспорта. `socks5` использует локальное разрешение цели; `socks5h` передаёт имя прокси. Для `wss` сохраняется проверка TLS сайта. |
| HTTP/3 к HTTPS-сайту через `MasqueProxy` | Строгий H3 внутри CONNECT-UDP, внешний carrier H2 с reliable capsules. |
| WS поверх H2/H3, WS через MASQUE | Не предоставляются. |
| H3 через обычную HTTP/HTTPS/SOCKS строку | Не предоставляется; SOCKS5 UDP ASSOCIATE отсутствует. |
| MASQUE AUTO, H1/H2 к сайту, внешний H1/H3 carrier или другой URI template | Не предоставляются в этом API. |

Внешнее соединение с прокси и соединение с сайтом имеют разные назначения.
`response.protocol` относится к сайту. Настройки браузерного профиля применяются к соединению с целевым сайтом.
TLS-соединение с прокси устанавливается отдельно. Библиотека не гарантирует полное
совпадение всех сетевых признаков WebSocket и TLS прокси с браузером.
