.NET-клиент
DarkWS.Client — асинхронный клиент для .NET 8, 9 и 10. У него нет зависимостей от
пакетов во время выполнения и не требуется ASP.NET, поэтому он работает в консольных приложениях, сервисах,
десктопных приложениях и интеграционных тестах.
dotnet add package DarkWS.Client --version 4.0.0
using DarkWS.Client;
await using var client = new DarkWsClient(new Uri("wss://example.com/ws"));
var result = await client.RequestAsync<SumResult>("math:sum", new { left = 10, right = 20 });
Console.WriteLine(result.Value); // 30
public sealed record SumResult(int Value);
Первый запрос открывает соединение. Вызовите ConnectAsync(ct) для ранней
проверки готовности или если приложение только слушает рассылки.
Для полной настройки передайте DarkWsClientOptions:
await using var client = new DarkWsClient(new DarkWsClientOptions {
Endpoint = new Uri("wss://example.com/ws"),
RequestTimeout = TimeSpan.FromSeconds(30),
AuthenticationTokenProvider = ct => tokenStore.GetAccessTokenAsync(ct),
});
Запросы
| Вызов | Отправляет | Возвращает |
|---|---|---|
RequestAsync<T>(action, ct) | Без data | Десериализованное data |
RequestAsync<T>(action, payload, ct) | payload как data | Десериализованное data |
RequestAsync(action, ct) | Без data | Завершается по подтверждению |
RequestAsync(action, payload, ct) | payload как data | Завершается по подтверждению |
- Действия без результата всё равно ждут подтверждения и выбрасывают исключение при ошибках сервера.
(object?)nullотправляет явный JSON null; если payload опущен,dataтоже опускается.- Типизированные запросы требуют поле
dataв ответе. - Nullable-аннотации во время выполнения не проверяются; JSON null обрабатывается по правилам System.Text.Json.
- Для динамических результатов используйте
JsonElement. Возвращённые элементы остаются валидными после освобождения буферов приёма.
Ошибка сервера выбрасывает DarkWsResponseException:
try {
await client.RequestAsync("message:delete", new { id }, ct);
} catch (DarkWsResponseException error) when (error.Code == "message:forbidden") {
ShowForbidden();
}
Исключение содержит Code, Action, RequestId и необязательное ErrorData (JsonElement?).
Отмена запроса или таймаут не откатывают работу на сервере. Если запись уже началась, отмена со стороны вызывающего кода не отменяет общий сокет.
Уведомления
using var created = client.On<MessageCreated>("message:created",
message => Console.WriteLine(message.Text));
using var saved = client.OnAsync<MessageCreated>("message:created",
async (message, ct) => await SaveMessageAsync(message, ct));
using var ping = client.On("system:tick", () => Console.WriteLine("tick"));
- Чтобы удалить подписку, освободите её (Dispose). Подписки сохраняются при переподключении.
- Callback-и выполняются по одному, вне потока чтения сокета, и могут ожидать запросы к тому же клиенту.
- Callback, уже выбранный для вызова, может выполниться и после отписки.
- Исключения передаются через
Errorи не останавливают доставку остальным. - Типизированные подписки требуют поле
data.
Callback-и не захватывают UI-контекст. Передавайте в него сами:
var ui = SynchronizationContext.Current
?? throw new InvalidOperationException("Call from the UI thread.");
using var subscription = client.On<MessageCreated>("message:created",
message => ui.Post(_ => UpdateView(message), null));
Медленные callback-и заполняют очередь уведомлений (NotificationQueueCapacity, 256).
Переполнение останавливает соединение, а не отбрасывает уведомления молча. При
разрыве соединения уведомления в очереди отбрасываются, а токен выполняющегося callback-а
отменяется. Рассылки, пришедшие во время разрыва, не воспроизводятся повторно; перезагрузите состояние
после переподключения.
Аутентификация
Вручную
await client.ConnectAsync(ct);
await client.AuthenticateAsync(accessToken, ct);
await client.RequestAsync("message:send", new { text = "Hello" }, ct);
await client.LogoutAsync(ct);
AuthenticateAsyncотправляетauth:<token>и ждётauth:success. Отказ выбрасываетDarkWsResponseExceptionс кодомauth:failed, действиемauthи пустым id запроса.LogoutAsyncотправляетlogoutи ждётlogout:success.- Команды выполняются по одной, а ожидание каждого ответа ограничено
RequestTimeout. Таймаут или отмена во время выполнения команды отбрасывает сокет, чтобы запоздавший ответ не завершил следующую команду. - Ручная аутентификация не сохраняет токен для переподключения.
Автоматически на каждом сокете
await using var client = new DarkWsClient(new DarkWsClientOptions {
Endpoint = new Uri("wss://example.com/ws"),
AuthenticationTokenProvider = ct => tokenStore.GetAccessTokenAsync(ct),
});
Провайдер возвращает ValueTask<string?> и вызывается для каждого нового сокета. Запросы
ждут успешной аутентификации. Когда провайдер настроен, любой сбой при
восстановлении сессии — постоянный отказ готовности: отсутствие токена, исключение в
провайдере, auth:failed, таймаут или обрыв сокета до auth:success. Состояние
становится Disconnected с указанием причины, и автоматические повторы прекращаются. Устраните причину и
вызовите ConnectAsync.
Вызов LogoutAsync отключает провайдер, даже если подтверждение потеряно или вызов
отклонён из-за MaxPendingRequests. Последующий успешный AuthenticateAsync снова
включает его.
Учётные данные на уровне HTTP
ConfigureWebSocketOptionsAsync выполняется перед каждым upgrade и может задать заголовки,
cookie, сертификаты или прокси:
ConfigureWebSocketOptionsAsync = async (socket, ct) => {
var token = await tokenStore.GetAccessTokenAsync(ct);
socket.SetRequestHeader("Authorization", $"Bearer {token}");
},
Используйте его, когда HTTP endpoint требует аутентификации до открытия сокета. HTTP-идентичность и сессия DarkWS независимы: при выходе очищайте заголовки и cookie, которыми владеет приложение, чтобы переподключение не восстановило старую идентичность.
Провайдер и callback-и конфигурации должны учитывать отмену и не должны вызывать клиент, соединение которого они готовят.
Жизненный цикл
State принимает одно из значений Disconnected, Connecting, Connected, Reconnecting и
Disposed. StateChanged сообщает о переходах с причиной сбоя в Reason; Error
сообщает о фоновых сбоях.
- Один экземпляр владеет одной серверной сессией и поддерживает параллельные вызовы. Для разных аккаунтов или endpoint используйте отдельные экземпляры.
ConnectAsyncприсоединяется к одной общей попытке подключения. Отмена со стороны вызывающего кода прекращает только его ожидание.- При сбоях транспорта клиент переподключается с экспоненциальным jitter до 30 секунд. Уже отправленные запросы завершаются ошибкой и никогда не воспроизводятся повторно; новые запросы ждут готовности.
- При
Reconnect = falseоборванный сокет остаётся закрытым, и последующие запросы завершаются ошибкой до явного вызоваConnectAsync. - Неудачная автоматическая аутентификация, сбой callback-а сокета, HTTP 401 или 403, ошибки протокола и переполнение очереди уведомлений прекращают автоматические повторы.
CloseAsyncкорректно закрывает соединение в пределахCloseTimeout, прекращает восстановление и отклоняет ожидающих до следующегоConnectAsync.DisposeиDisposeAsyncсразу останавливают сетевую активность;DisposeAsyncвдобавок ожидает завершения сетевых задач клиента. Для корректного закрытия сначала вызовитеCloseAsync. Освобождение окончательно и идемпотентно.
Наблюдатели событий выполняются вне контекста сокета и UI; делайте их короткими. Исключения в
наблюдателях Error изолируются.
Исключения
| Исключение | Значение |
|---|---|
DarkWsResponseException | Сервер ответил ошибкой |
DarkWsTimeoutException | Stage — Connection, Send или Response |
DarkWsConnectionException | Подключение не удалось или закрыто; CloseStatus, CloseReason |
DarkWsProtocolException | Сервер нарушил протокол; CloseStatus |
DarkWsClientLimitException | Достигнут MaxPendingRequests (ничего не отправлено) или переполнилась очередь уведомлений |
JsonException | Не удалось преобразовать payload или результат |
Опции
| Опция | По умолчанию | Назначение |
|---|---|---|
Endpoint | обязательна | URI ws:// или wss:// |
ConnectionTimeout | 30 с | Открытие сокета |
SendTimeout | 30 с | Запись сообщения |
RequestTimeout | 5 мин | Ожидание ответа; Timeout.InfiniteTimeSpan отключает |
CloseTimeout | 5 с | Корректное закрытие в CloseAsync |
Reconnect | true | Переподключаться после сбоев транспорта |
PingInterval / PongTimeout | по 30 с | Heartbeat |
MaxMessageSizeBytes | 1 MiB | Максимальный размер входящего сообщения с учётом всех фрагментов |
MaxPendingRequests | 256 | Ожидающие вызовы, включая аутентификацию и ожидание подключения |
NotificationQueueCapacity | 256 | Уведомления в очереди до переполнения |
JsonOptions | JsonSerializerDefaults.Web | Собственная копия опций сериализации |
ConfigureWebSocketOptionsAsync | нет | Настройка каждого запроса upgrade |
AuthenticationTokenProvider | нет | Токен для автоматической аутентификации |
Конечные таймауты должны быть от 1 до 4294967294 миллисекунд. Ожидания подключения, отправки и ответа
независимы, поэтому вызов может занять их сумму; для общего срока используйте токен отмены.
Backpressure на сервере может задерживать pong, поэтому настраивайте heartbeat под
свою нагрузку.
Клиент закрывает сокет со статусом 1003, если сервер отправляет бинарный фрейм.
Внедрение зависимостей
DarkWS.Client.DependencyInjection регистрирует клиент в Microsoft DI. Пакет
ссылается только на абстракции DI, а не на ASP.NET или Generic Host.
dotnet add package DarkWS.Client.DependencyInjection --version 4.0.0
services.AddDarkWsClient(options => {
options.Endpoint = new Uri("wss://example.com/ws");
});
public sealed class Calculator(IDarkWsClient client) {
public Task<int> SumAsync(int left, int right) =>
client.RequestAsync<int>("math:sum", new { left, right });
}
AddDarkWsClient регистрирует один ленивый singleton IDarkWsClient. Его разрешение не
подключает клиент; подключает первый запрос или ConnectAsync. Освобождением владеет контейнер:
потребители освобождают свои подписки, а не клиент. Повторные или конфликтующие
регистрации выбрасывают исключение до изменения коллекции; keyed-регистрация
IDarkWsClient тоже считается такой.
Настройка с использованием других сервисов:
services.AddDarkWsClient((provider, options) => {
options.Endpoint = new Uri("wss://example.com/ws");
var tokens = provider.GetRequiredService<TokenStore>();
options.AuthenticationTokenProvider = ct => tokens.GetAccessTokenAsync(ct);
});
TokenStore должен быть безопасен для использования из singleton. Не захватывайте scoped-сервисы в
callback-ах singleton.
Одна сессия на область
Singleton подходит для одной общей серверной идентичности. Для отдельной идентичности в каждой области зарегистрируйте клиент сами:
services.AddScoped<IDarkWsClient>(provider => {
var tokens = provider.GetRequiredService<UserTokenStore>();
return new DarkWsClient(new DarkWsClientOptions {
Endpoint = new Uri("wss://example.com/ws"),
AuthenticationTokenProvider = ct => tokens.GetAccessTokenAsync(ct),
});
});
Согласуйте область со временем жизни сессии: область HTTP-запроса заканчивается вместе с
запросом. Не сочетайте эту регистрацию с AddDarkWsClient. Для нескольких
endpoint используйте keyed-регистрации или экземпляры, которыми владеет приложение.
Поддержка платформ
Native AOT, trimming, .NET Framework, Unity, браузерный WebAssembly и интеграция с жизненным циклом мобильных приложений не сертифицированы.