Поддержка WebSockets в ASP.NET Core

Эта статья описывает начало работы с WebSocket в ASP.NET Core. WebSocket (RFC 6455) — это протокол, предоставляющий сохраняемые двусторонние каналы связи по TCP-подключениям. Он используется в приложениях, где нужна быстрая связь в режиме реального времени, например в чатах, панелях мониторинга и играх.

Просмотрите или скачайте пример кода (описание процедуры скачивания, описание процедуры выполнения).

Поддержка WebSockets по HTTP/2

При использовании соединений WebSocket по HTTP/2 доступны новые возможности, в том числе следующие:

  • сжатие заголовка;
  • мультиплексирование, которое сокращает время и ресурсы, необходимые для выполнения нескольких запросов к серверу.

Эти функции доступны в Kestrel на всех платформах с поддержкой HTTP/2. Согласование версии выполняется в браузерах и Kestrel автоматически, поэтому новые интерфейсы API не требуются.

В .NET 7 была введена поддержка WebSocket по HTTP/2 для Kestrel, клиента JavaScript SignalR и SignalR с Blazor WebAssembly.

Примечание.

Соединения WebSocket по HTTP/2 используют запросы CONNECT, а не GET, поэтому ваши собственные маршруты и контроллеры могут потребовать обновления. Дополнительные сведения см. в разделе Добавление поддержки WebSocket по HTTP/2 для существующих контроллеров в этой статье.

В Chrome и Edge протокол WebSocket по HTTP/2 включен по умолчанию, а в FireFox его можно включить на странице about:config с помощью флага network.http.spdy.websockets.

Протокол WebSocket изначально был разработан для HTTP/1.1, но с тех пор был адаптирован для работы по протоколу HTTP/2. (RFC 8441)

SignalR

ASP.NET Core SignalR — это библиотека, которая упрощает добавление веб-функций в режиме реального времени в приложения. Она использует WebSocket, когда это возможно.

Для большинства приложений рекомендуется использовать SignalR вместо прямых соединений WebSocket. SignalR:

  • обеспечивает резервный транспорт для сред, в которых соединения WebSocket недоступны;
  • предоставляет простую модель приложений на основе удаленного вызова процедур;
  • в большинстве сценариев по производительности не уступает прямым соединениям WebSocket.

WebSocket по HTTP/2 поддерживается для следующих компонентов:

  • клиент JavaScript ASP.NET Core SignalR
  • ASP.NET Core SignalR с Blazor WebAssembly

Для некоторых приложений gRPC в .NET предоставляет альтернативу WebSockets.

Необходимые компоненты

  • Любая ОС, поддерживающая ASP.NET Core:
    • Windows 7/Windows Server 2008 или более поздних версий
    • Linux
    • macOS
  • Если приложение работает в Windows с помощью IIS:
  • Если приложение выполняется в HTTP.sys:
    • Windows 8/Windows Server 2012 или более поздних версий
  • Список поддерживаемых обозревателей см. на странице Можно ли использовать.

Настройка ПО промежуточного слоя

Добавьте ПО промежуточного слоя WebSockets в Program.cs:

app.UseWebSockets();

Можно настроить следующие параметры:

  • KeepAliveInterval — как часто нужно отправлять клиенту кадры проверки связи, чтобы прокси-серверы удерживали соединение открытым. Значение по умолчанию — две минуты.
  • AllowedOrigins — список допустимых значений заголовка Origin для запросов WebSocket. По умолчанию разрешены все источники. Дополнительные сведения см. в разделе Ограничения для источников WebSocket в этой статье.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Принятие запросов WebSocket

На более позднем этапе жизненного цикла запроса (например, далее в методе Program.cs или методе действия) проверьте, относится ли запрос к WebSocket, и примите его.

Следующий пример взят из дальнейшей части метода Program.cs:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

Запрос WebSocket может поступить по любому URL-адресу, но этот пример кода принимает только запросы для /ws.

Аналогичный подход можно использовать в методе контроллера:

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

При использовании WebSocket вам необходимо поддерживать работу конвейера ПО промежуточного слоя, пока соединение активно. При попытке отправить или получить сообщение WebSocket после того, как конвейер ПО промежуточного слоя завершил работу, вы можете получить исключение такого вида:

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Если вы используете фоновую службу для записи данных в WebSocket, убедитесь, что конвейер ПО промежуточного слоя продолжает работу. Это можно сделать с помощью TaskCompletionSource<TResult>. Передайте TaskCompletionSource фоновой службе, а после завершения работы с WebSocket вызовите TrySetResult с ее помощью. Затем используйте конструкцию await для ожидания свойства Task во время запроса, как показано в следующем примере:

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

Исключение закрытия WebSocket может также возникнуть, если метод действия возвращает управление слишком быстро. При принятии сокета в методе действия дождитесь завершения выполнения кода, в котором используется сокет, прежде чем возвращать управление из метода действия.

Никогда не используйте Task.Wait, Task.Result или аналогичные блокирующие вызовы для ожидания выполнения сокета, так как это может привести к серьезным проблемам потокового выполнения. Всегда используйте await.

Добавление поддержки WebSocket по HTTP/2 для существующих контроллеров

В .NET 7 была введена поддержка WebSocket по HTTP/2 для Kestrel, клиента JavaScript SignalR и SignalR с Blazor WebAssembly. Соединения WebSocket по HTTP/2 используют запросы CONNECT, а не GET. Если вы ранее использовали [HttpGet("/path")] метод действия контроллера для запросов Websocket, обновите его, чтобы использовать [Route("/path")] его.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

Сжатие

Предупреждение

Включение сжатия через зашифрованные подключения может сделать приложение уязвимым к атакам CRIME/BREACH. При отправке конфиденциальной информации не включайте сжатие или используйте WebSocketMessageFlags.DisableCompression в вызове WebSocket.SendAsync. Это относится к обеим сторонам подключения по WebSocket. Обратите внимание, что для API WebSockets в браузере нет конфигурации, позволяющей отключить сжатие для операции отправки.

Если требуется реализовать сжатие сообщений через WebSockets, в коде принятия должно быть указано, что сжатие допустимо, как показано ниже:

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBits и WebSocketAcceptContext.DisableServerContextTakeover — это расширенные параметры, которые отвечают за процесс сжатия.

Сжатие согласовывается между клиентом и сервером при установке первого подключения. Дополнительные сведения о согласовании см. в статье Расширения сжатия для WebSocket RFC.

Примечание.

Если согласование сжатия не принято сервером или клиентом, подключение все равно устанавливается. Однако в этом случае в подключении не будет использоваться сжатие при отправке или получении сообщений.

Отправка и получение сообщений

Метод AcceptWebSocketAsync обновляет TCP-соединение до соединения WebSocket и предоставляет объект WebSocket. Используйте объект WebSocket для отправки и получения сообщений.

Приведенный выше код, который принимает запрос WebSocket, передает объект WebSocket в метод Echo. Код принимает сообщение и сразу отправляет такое же сообщение обратно. Сообщения отправляются и получаются циклически, пока клиент не закроет подключение:

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

Если вы принимаете подключение WebSocket до начала этого цикла, конвейер ПО промежуточного слоя завершается. После закрытия сокета конвейер развертывается. То есть запрос перестает перемещаться по конвейеру после принятия WebSocket. После завершения цикла и закрытия сокета запрос возвращается в конвейер.

Обработка отключений клиента

При отключении клиента из-за потери связи сервер не получает сведения автоматически. Сервер получает сообщение об отключении, только если клиент отправляет его. В случае потери подключения к Интернету это невозможно. Если при этом вы хотите выполнить определенное действие, установите время ожидания сигнала клиента с определенным интервалом.

Если клиент не всегда отправляет сообщения, а вы не хотите ожидать истечения времени ожидания только потому, что подключение не используется, тогда клиент может использовать таймер для отправки сообщения проверки связи каждые X секунд. Если сообщение не было получено на сервере в течение 2*X секунд после предыдущего, завершите подключение и сообщите о том, что клиент отключен. Подождите вдвое дольше ожидаемого временного интервала, чтобы оставить дополнительное время на задержки в сети при отправке сообщения проверки связи.

Ограничения для источников WebSocket

Варианты защиты, предоставляемые CORS, не применяются к WebSocket. Браузеры не поддерживают следующие задачи:

  • выполнение предварительных запросов CORS;
  • использование ограничений, указанных в заголовках Access-Control, при выполнении запросов WebSocket.

Однако браузеры отправляют заголовок Origin при выпуске запросов WebSocket. Приложения должны быть настроены для проверки этих заголовков, чтобы использовались только WebSocket из ожидаемых источников.

Если вы размещаете сервер по адресу "https://server.com", а клиент — по адресу "https://client.com", добавьте "https://client.com" в список AllowedOrigins, подлежащих проверке WebSocket.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Примечание.

Заголовок Origin контролируется клиентом и, как и заголовок Referer, может быть подделан. Не используйте эти заголовки в качестве механизма проверки подлинности.

Поддержка служб IIS/IIS Express

Windows Server 2012 или более поздней версии и Windows 8 или более поздней версии с IIS и IIS Express 8 или более поздней версии поддерживают протокол WebSocket, но не протокол WebSocket по HTTP/2.

Примечание.

Соединения WebSockets всегда включены при использовании IIS Express.

Включение WebSockets в службах IIS

Чтобы включить поддержку протокола WebSocket в Windows Server 2012 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. В меню Управление запустите мастер Добавить роли и компоненты или в окне Диспетчер серверов щелкните соответствующую ссылку.
  2. Выберите Установка ролей или компонентов. Выберите Далее.
  3. Выберите подходящий сервер (по умолчанию выбирается локальный сервер). Выберите Далее.
  4. Разверните Веб-сервер (IIS) в дереве Роли, разверните Веб-сервер, а затем Разработка приложений.
  5. Выберите протокол WebSocket. Выберите Далее.
  6. Если дополнительные функции не требуются, нажмите Далее.
  7. Выберите Установить.
  8. По завершении установки выберите Закрыть, чтобы выйти из мастера.

Чтобы включить поддержку протокола WebSocket в Windows 8 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. Последовательно выберите Панель управления>Программы>Программы и компоненты>Включение или отключение компонентов Windows (в левой части экрана).
  2. Откройте следующе узлы: IIS>Службы Интернета>Компоненты разработки приложений.
  3. Выберите компонент Протокол WebSocket. Нажмите ОК.

Отключите WebSocket при использовании socket.io на Node.js

Если используется поддержка WebSocket в socket.io на Node.js, отключите модуль WebSocket IIS по умолчанию с помощью элемента webSocket в web.config или applicationHost.config. Если не выполнить этот шаг, модуль IIS WebSocket попытается обработать соединение WebSocket, а не Node.js и приложение.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Пример приложения

Пример приложения в этой статье — это эхо-приложение. Оно имеет веб-страницу, которая устанавливает соединения WebSocket, а сервер перенаправляет все полученные сообщения обратно клиенту. Пример приложения поддерживает WebSocket по HTTP/2 при использовании целевой платформы .NET 7 или более поздней версии.

Запустите приложение:

  • Чтобы запустить приложение в Visual Studio, откройте пример проекта в этой среде и нажмите клавиши CTRL+F5 для запуска без отладчика.
  • Чтобы запустить приложение в командной оболочке, выполните команду dotnet run и перейдите в браузере по адресу http://localhost:<port>.

На этой веб-странице отображается состояние подключения.

Initial state of webpage before WebSockets connection

Выберите Connect (Подключить), чтобы отправить запрос WebSocket на показанный URL-адрес. Введите тестовое сообщение и выберите Send (Отправить). После этого выберите Close Socket (Закрыть сокет). В разделе Communication Log (Журнал связи) выводится каждое выполняемое действие открытия, отправки и закрытия.

Final state of webpage after WebSockets connection and test messages are sent and received

Эта статья описывает начало работы с WebSocket в ASP.NET Core. WebSocket (RFC 6455) — это протокол, предоставляющий сохраняемые двусторонние каналы связи по TCP-подключениям. Он используется в приложениях, где нужна быстрая связь в режиме реального времени, например в чатах, панелях мониторинга и играх.

Просмотрите или скачайте пример кода (описание процедуры скачивания, описание процедуры выполнения).

SignalR

ASP.NET Core SignalR — это библиотека, которая упрощает добавление веб-функций в режиме реального времени в приложения. Она использует WebSocket, когда это возможно.

Для большинства приложений рекомендуется использовать SignalR вместо прямых соединений WebSocket. Служба SignalR обеспечивает резервный транспорт для сред, в которых соединения WebSocket недоступны. Она также предоставляет простую модель приложений на основе удаленного вызова процедур. В большинстве сценариев SignalR по производительности не уступает прямым соединениям WebSocket.

Для некоторых приложений gRPC в .NET предоставляет альтернативу WebSockets.

Необходимые компоненты

  • Любая ОС, поддерживающая ASP.NET Core:
    • Windows 7/Windows Server 2008 или более поздних версий
    • Linux
    • macOS
  • Если приложение работает в Windows с помощью IIS:
  • Если приложение выполняется в HTTP.sys:
    • Windows 8/Windows Server 2012 или более поздних версий
  • Список поддерживаемых обозревателей см. на странице Можно ли использовать.

Настройка ПО промежуточного слоя

Добавьте ПО промежуточного слоя WebSockets в Program.cs:

app.UseWebSockets();

Можно настроить следующие параметры:

  • KeepAliveInterval — как часто нужно отправлять клиенту кадры проверки связи, чтобы прокси-серверы удерживали соединение открытым. Значение по умолчанию — две минуты.
  • AllowedOrigins — список допустимых значений заголовка Origin для запросов WebSocket. По умолчанию разрешены все источники. Дополнительные сведения см. в разделе Ограничения для источников WebSocket в этой статье.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Принятие запросов WebSocket

На более позднем этапе жизненного цикла запроса (например, далее в методе Program.cs или методе действия) проверьте, относится ли запрос к WebSocket, и примите его.

Следующий пример взят из дальнейшей части метода Program.cs:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

Запрос WebSocket может поступить по любому URL-адресу, но этот пример кода принимает только запросы для /ws.

Аналогичный подход можно использовать в методе контроллера:

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

При использовании WebSocket вам необходимо поддерживать работу конвейера ПО промежуточного слоя, пока соединение активно. При попытке отправить или получить сообщение WebSocket после того, как конвейер ПО промежуточного слоя завершил работу, вы можете получить исключение такого вида:

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Если вы используете фоновую службу для записи данных в WebSocket, убедитесь, что конвейер ПО промежуточного слоя продолжает работу. Это можно сделать с помощью TaskCompletionSource<TResult>. Передайте TaskCompletionSource фоновой службе, а после завершения работы с WebSocket вызовите TrySetResult с ее помощью. Затем используйте конструкцию await для ожидания свойства Task во время запроса, как показано в следующем примере:

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

Исключение закрытия WebSocket может также возникнуть, если метод действия возвращает управление слишком быстро. При принятии сокета в методе действия дождитесь завершения выполнения кода, в котором используется сокет, прежде чем возвращать управление из метода действия.

Никогда не используйте Task.Wait, Task.Result или аналогичные блокирующие вызовы для ожидания выполнения сокета, так как это может привести к серьезным проблемам потокового выполнения. Всегда используйте await.

Сжатие

Предупреждение

Включение сжатия через зашифрованные подключения может сделать приложение уязвимым к атакам CRIME/BREACH. При отправке конфиденциальной информации не включайте сжатие или используйте WebSocketMessageFlags.DisableCompression в вызове WebSocket.SendAsync. Это относится к обеим сторонам подключения по WebSocket. Обратите внимание, что для API WebSockets в браузере нет конфигурации, позволяющей отключить сжатие для операции отправки.

Если требуется реализовать сжатие сообщений через WebSockets, в коде принятия должно быть указано, что сжатие допустимо, как показано ниже:

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBits и WebSocketAcceptContext.DisableServerContextTakeover — это расширенные параметры, которые отвечают за процесс сжатия.

Сжатие согласовывается между клиентом и сервером при установке первого подключения. Дополнительные сведения о согласовании см. в статье Расширения сжатия для WebSocket RFC.

Примечание.

Если согласование сжатия не принято сервером или клиентом, подключение все равно устанавливается. Однако в этом случае в подключении не будет использоваться сжатие при отправке или получении сообщений.

Отправка и получение сообщений

Метод AcceptWebSocketAsync обновляет TCP-соединение до соединения WebSocket и предоставляет объект WebSocket. Используйте объект WebSocket для отправки и получения сообщений.

Приведенный выше код, который принимает запрос WebSocket, передает объект WebSocket в метод Echo. Код принимает сообщение и сразу отправляет такое же сообщение обратно. Сообщения отправляются и получаются циклически, пока клиент не закроет подключение:

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

Если вы принимаете подключение WebSocket до начала этого цикла, конвейер ПО промежуточного слоя завершается. После закрытия сокета конвейер развертывается. То есть запрос перестает перемещаться по конвейеру после принятия WebSocket. После завершения цикла и закрытия сокета запрос возвращается в конвейер.

Обработка отключений клиента

При отключении клиента из-за потери связи сервер не получает сведения автоматически. Сервер получает сообщение об отключении, только если клиент отправляет его. В случае потери подключения к Интернету это невозможно. Если при этом вы хотите выполнить определенное действие, установите время ожидания сигнала клиента с определенным интервалом.

Если клиент не всегда отправляет сообщения, а вы не хотите ожидать истечения времени ожидания только потому, что подключение не используется, тогда клиент может использовать таймер для отправки сообщения проверки связи каждые X секунд. Если сообщение не было получено на сервере в течение 2*X секунд после предыдущего, завершите подключение и сообщите о том, что клиент отключен. Подождите вдвое дольше ожидаемого временного интервала, чтобы оставить дополнительное время на задержки в сети при отправке сообщения проверки связи.

Ограничения для источников WebSocket

Варианты защиты, предоставляемые CORS, не применяются к WebSocket. Браузеры не поддерживают следующие задачи:

  • выполнение предварительных запросов CORS;
  • использование ограничений, указанных в заголовках Access-Control, при выполнении запросов WebSocket.

Однако браузеры отправляют заголовок Origin при выпуске запросов WebSocket. Приложения должны быть настроены для проверки этих заголовков, чтобы использовались только WebSocket из ожидаемых источников.

Если вы размещаете сервер по адресу "https://server.com", а клиент — по адресу "https://client.com", добавьте "https://client.com" в список AllowedOrigins, подлежащих проверке WebSocket.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Примечание.

Заголовок Origin контролируется клиентом и, как и заголовок Referer, может быть подделан. Не используйте эти заголовки в качестве механизма проверки подлинности.

Поддержка служб IIS/IIS Express

Windows Server 2012 или более поздней версии и Windows 8 или более поздней версии с IIS и IIS Express 8 или более поздней версии поддерживают протокол WebSocket.

Примечание.

Соединения WebSockets всегда включены при использовании IIS Express.

Включение WebSockets в службах IIS

Чтобы включить поддержку протокола WebSocket в Windows Server 2012 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. В меню Управление запустите мастер Добавить роли и компоненты или в окне Диспетчер серверов щелкните соответствующую ссылку.
  2. Выберите Установка ролей или компонентов. Выберите Далее.
  3. Выберите подходящий сервер (по умолчанию выбирается локальный сервер). Выберите Далее.
  4. Разверните Веб-сервер (IIS) в дереве Роли, разверните Веб-сервер, а затем Разработка приложений.
  5. Выберите протокол WebSocket. Выберите Далее.
  6. Если дополнительные функции не требуются, нажмите Далее.
  7. Выберите Установить.
  8. По завершении установки выберите Закрыть, чтобы выйти из мастера.

Чтобы включить поддержку протокола WebSocket в Windows 8 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. Последовательно выберите Панель управления>Программы>Программы и компоненты>Включение или отключение компонентов Windows (в левой части экрана).
  2. Откройте следующе узлы: IIS>Службы Интернета>Компоненты разработки приложений.
  3. Выберите компонент Протокол WebSocket. Нажмите ОК.

Отключите WebSocket при использовании socket.io на Node.js

Если используется поддержка WebSocket в socket.io на Node.js, отключите модуль WebSocket IIS по умолчанию с помощью элемента webSocket в web.config или applicationHost.config. Если не выполнить этот шаг, модуль IIS WebSocket попытается обработать соединение WebSocket, а не Node.js и приложение.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Пример приложения

Пример приложения в этой статье — это эхо-приложение. Оно имеет веб-страницу, которая устанавливает соединения WebSocket, а сервер перенаправляет все полученные сообщения обратно клиенту. Этот пример приложения не настроен для запуска из Visual Studio с IIS Express, поэтому его необходимо запустить в командной оболочке с dotnet run и затем перейти в браузере по адресу http://localhost:<port>. На этой веб-странице отображается состояние подключения.

Initial state of webpage before WebSockets connection

Выберите Connect (Подключить), чтобы отправить запрос WebSocket на показанный URL-адрес. Введите тестовое сообщение и выберите Send (Отправить). После этого выберите Close Socket (Закрыть сокет). В разделе Communication Log (Журнал связи) выводится каждое выполняемое действие открытия, отправки и закрытия.

Final state of webpage after WebSockets connection and test messages are sent and received

Эта статья описывает начало работы с WebSocket в ASP.NET Core. WebSocket (RFC 6455) — это протокол, предоставляющий сохраняемые двусторонние каналы связи по TCP-подключениям. Он используется в приложениях, где нужна быстрая связь в режиме реального времени, например в чатах, панелях мониторинга и играх.

Просмотреть или скачать пример кода (описание скачивания). Процедура выполнения.

SignalR

ASP.NET Core SignalR — это библиотека, которая упрощает добавление веб-функций в режиме реального времени в приложения. Она использует WebSocket, когда это возможно.

Для большинства приложений рекомендуется использовать SignalR вместо прямых соединений WebSocket. Служба SignalR обеспечивает резервный транспорт для сред, в которых соединения WebSocket недоступны. Она также предоставляет простую модель приложений на основе удаленного вызова процедур. В большинстве сценариев SignalR по производительности не уступает прямым соединениям WebSocket.

Для некоторых приложений gRPC в .NET предоставляет альтернативу WebSockets.

Необходимые компоненты

  • Любая ОС, поддерживающая ASP.NET Core:
    • Windows 7/Windows Server 2008 или более поздних версий
    • Linux
    • macOS
  • Если приложение работает в Windows с помощью IIS:
  • Если приложение выполняется в HTTP.sys:
    • Windows 8/Windows Server 2012 или более поздних версий
  • Список поддерживаемых обозревателей см. на странице Можно ли использовать.

Настройка ПО промежуточного слоя

Добавьте ПО промежуточного слоя WebSocket в метод Configure класса Startup:

app.UseWebSockets();

Примечание.

Если вы хотите принимать запросы WebSocket в контроллере, вызов app.UseWebSockets должен предшествовать app.UseEndpoints.

Можно настроить следующие параметры:

  • KeepAliveInterval — как часто нужно отправлять клиенту кадры проверки связи, чтобы прокси-серверы удерживали соединение открытым. Значение по умолчанию — две минуты.
  • AllowedOrigins — список допустимых значений заголовка Origin для запросов WebSocket. По умолчанию разрешены все источники. Дополнительные сведения см. в разделе "Ограничения для источников WebSocket" ниже.
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

Принятие запросов WebSocket

На более позднем этапе жизненного цикла запроса (например, далее в методе Configure или методе действия) проверьте, относится ли запрос к WebSocket, и примите его.

Следующий пример взят из дальнейшей части метода Configure:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});

Запрос WebSocket может поступить по любому URL-адресу, но этот пример кода принимает только запросы для /ws.

Аналогичный подход можно использовать в методе контроллера:

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

При использовании WebSocket вам необходимо поддерживать работу конвейера ПО промежуточного слоя, пока соединение активно. При попытке отправить или получить сообщение WebSocket после того, как конвейер ПО промежуточного слоя завершил работу, вы можете получить исключение такого вида:

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Если вы используете фоновую службу для записи данных в WebSocket, убедитесь, что конвейер ПО промежуточного слоя продолжает работу. Это можно сделать с помощью TaskCompletionSource<TResult>. Передайте TaskCompletionSource фоновой службе, а после завершения работы с WebSocket вызовите TrySetResult с ее помощью. Затем используйте конструкцию await для ожидания свойства Task во время запроса, как показано в следующем примере:

app.Use(async (context, next) =>
{
    using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
    {
        var socketFinishedTcs = new TaskCompletionSource<object>();

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});

Исключение закрытия WebSocket может также возникнуть, если метод действия возвращает управление слишком быстро. При принятии сокета в методе действия дождитесь завершения выполнения кода, в котором используется сокет, прежде чем возвращать управление из метода действия.

Никогда не используйте Task.Wait, Task.Result или аналогичные блокирующие вызовы для ожидания выполнения сокета, так как это может привести к серьезным проблемам потокового выполнения. Всегда используйте await.

Отправка и получение сообщений

Метод AcceptWebSocketAsync обновляет TCP-соединение до соединения WebSocket и предоставляет объект WebSocket. Используйте объект WebSocket для отправки и получения сообщений.

Приведенный выше код, который принимает запрос WebSocket, передает объект WebSocket в метод Echo. Код принимает сообщение и сразу отправляет такое же сообщение обратно. Сообщения отправляются и получаются циклически, пока клиент не закроет подключение:

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

Если вы принимаете подключение WebSocket до начала этого цикла, конвейер ПО промежуточного слоя завершается. После закрытия сокета конвейер развертывается. То есть запрос перестает перемещаться по конвейеру после принятия WebSocket. После завершения цикла и закрытия сокета запрос возвращается в конвейер.

Обработка отключений клиента

При отключении клиента из-за потери связи сервер не получает сведения автоматически. Сервер получает сообщение об отключении, только если клиент отправляет его. В случае потери подключения к Интернету это невозможно. Если при этом вы хотите выполнить определенное действие, установите время ожидания сигнала клиента с определенным интервалом.

Если клиент не всегда отправляет сообщения, а вы не хотите ожидать истечения времени ожидания только потому, что подключение не используется, тогда клиент может использовать таймер для отправки сообщения проверки связи каждые X секунд. Если сообщение не было получено на сервере в течение 2*X секунд после предыдущего, завершите подключение и сообщите о том, что клиент отключен. Подождите вдвое дольше ожидаемого временного интервала, чтобы оставить дополнительное время на задержки в сети при отправке сообщения проверки связи.

Примечание.

Внутренние ManagedWebSocket обработчики кадров Ping/Pong неявно обрабатывают подключение, если KeepAliveInterval параметр больше нуля, который по умолчанию составляет 30 секунд (TimeSpan.FromSeconds(30)).

Ограничения для источников WebSocket

Варианты защиты, предоставляемые CORS, не применяются к WebSocket. Браузеры не поддерживают следующие задачи:

  • выполнение предварительных запросов CORS;
  • использование ограничений, указанных в заголовках Access-Control, при выполнении запросов WebSocket.

Однако браузеры отправляют заголовок Origin при выпуске запросов WebSocket. Приложения должны быть настроены для проверки этих заголовков, чтобы использовались только WebSocket из ожидаемых источников.

Если вы размещаете сервер по адресу "https://server.com", а клиент — по адресу "https://client.com", добавьте "https://client.com" в список AllowedOrigins, подлежащих проверке WebSocket.

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Примечание.

Заголовок Origin контролируется клиентом и, как и заголовок Referer, может быть подделан. Не используйте эти заголовки в качестве механизма проверки подлинности.

Поддержка служб IIS/IIS Express

Windows Server 2012 или более поздней версии и Windows 8 или более поздней версии с IIS и IIS Express 8 или более поздней версии поддерживают протокол WebSocket.

Примечание.

Соединения WebSockets всегда включены при использовании IIS Express.

Включение WebSockets в службах IIS

Чтобы включить поддержку протокола WebSocket в Windows Server 2012 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. В меню Управление запустите мастер Добавить роли и компоненты или в окне Диспетчер серверов щелкните соответствующую ссылку.
  2. Выберите Установка ролей или компонентов. Выберите Далее.
  3. Выберите подходящий сервер (по умолчанию выбирается локальный сервер). Выберите Далее.
  4. Разверните Веб-сервер (IIS) в дереве Роли, разверните Веб-сервер, а затем Разработка приложений.
  5. Выберите протокол WebSocket. Выберите Далее.
  6. Если дополнительные функции не требуются, нажмите Далее.
  7. Выберите Установить.
  8. По завершении установки выберите Закрыть, чтобы выйти из мастера.

Чтобы включить поддержку протокола WebSocket в Windows 8 или более поздней версии:

Примечание.

Эти действия не требуется выполнять при использовании IIS Express

  1. Последовательно выберите Панель управления>Программы>Программы и компоненты>Включение или отключение компонентов Windows (в левой части экрана).
  2. Откройте следующе узлы: IIS>Службы Интернета>Компоненты разработки приложений.
  3. Выберите компонент Протокол WebSocket. Нажмите ОК.

Отключите WebSocket при использовании socket.io на Node.js

Если используется поддержка WebSocket в socket.io на Node.js, отключите модуль WebSocket IIS по умолчанию с помощью элемента webSocket в web.config или applicationHost.config. Если не выполнить этот шаг, модуль IIS WebSocket попытается обработать соединение WebSocket, а не Node.js и приложение.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Пример приложения

Пример приложения в этой статье — это эхо-приложение. Оно имеет веб-страницу, которая устанавливает соединения WebSocket, а сервер перенаправляет все полученные сообщения обратно клиенту. Этот пример приложения не настроен для запуска из Visual Studio с IIS Express, поэтому его необходимо запустить в командной оболочке с dotnet run и затем перейти в браузере по адресу http://localhost:5000. На этой веб-странице отображается состояние подключения.

Initial state of webpage before WebSockets connection

Выберите Connect (Подключить), чтобы отправить запрос WebSocket на показанный URL-адрес. Введите тестовое сообщение и выберите Send (Отправить). После этого выберите Close Socket (Закрыть сокет). В разделе Communication Log (Журнал связи) выводится каждое выполняемое действие открытия, отправки и закрытия.

Final state of webpage after WebSockets connection and test messages are sent and received