ASP.NET Core 中的 WebSocket 支持

本文介绍 ASP.NET Core 中 WebSocket 的入门方法。 WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道。 它用于从快速实时通信中获益的应用,如聊天、仪表板和游戏应用。

查看或下载示例代码如何下载如何运行)。

Http/2 WebSockets 支持

使用基于 HTTP/2 的 WebSockets 可利用以下新功能:

  • 标头压缩。
  • 多路复用,可减少向服务器发出多个请求时所需的时间和资源。

所有支持 HTTP/2 的平台上的 Kestrel 中都提供了这些受支持的功能。 版本协商在浏览器和 Kestrel 中是自动的,因此不需要新的 API。

.NET 7 为 Kestrel、SignalR JavaScript 客户端和带有 Blazor WebAssembly 的 SignalR 引入了基于 HTTP/2 的 Websockets 支持。

注意

HTTP/2 WebSockets 使用 CONNECT 请求而不是 GET,因此可能需要更新你自己的路由和控制器。 有关详细信息,请参阅本文中的为现有控制器添加 HTTP/2 WebSockets 支持

Chrome 和 Edge 默认启用 HTTP/2 WebSocket,对于 FireFox,则可在 about:config 页面中使用 network.http.spdy.websockets 标志来启用它。

WebSockets 最初是为 HTTP/1.1 设计的,但后来改用于 HTTP/2。 (RFC 8441)

SignalR

ASP.NET Core SignalR 是一个库,可用于简化向应用添加实时 Web 功能。 它会尽可能地使用 WebSocket。

对于大多数应用程序,我们建议使用 SignalR,而不是原始 WebSocket。 SignalR:

  • 可为 WebSocket 不可用的环境提供传输回退。
  • 可提供基本的远程过程调用应用模型。
  • 在大多数情况下,与使用原始 WebSocket 相比,没有显著的性能缺点。

基于 HTTP/2 的 WebSockets 适用于:

  • ASP.NET Core SignalR JavaScript 客户端
  • 具有 Blazor WebAssembly 的 ASP.NET Core SignalR

对于某些应用,.NET 上的 gRPC 提供了 WebSocket 的替代方法。

先决条件

  • 支持 ASP.NET Core 的任何操作系统:
    • Windows 7/Windows Server 2008 或更高版本
    • Linux
    • macOS
  • 如果应用在安装了 IIS 的 Windows 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
    • IIS 8 / IIS 8 Express
    • 必须启用 WebSocket。 请参阅 IIS/IIS Express 支持部分。
  • 如果应用在 HTTP.sys 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
  • 有关支持的浏览器,请参阅能否使用

配置中间件

Program.cs 中添加 WebSocket 中间件:

app.UseWebSockets();

可配置以下设置:

  • KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 默认值为 2 分钟。
  • AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。 默认情况下,允许使用所有源。 有关详细信息,请参阅本文中的 WebSocket 源限制
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

接受 WebSocket 请求

在请求生命周期后期(例如在 Program.cs 或操作方法的后期),检查它是否是 WebSocket 请求并接受 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。 在请求期间对 Task 执行 await,如下面的示例所示:

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.WaitTask.Result 或类似阻塞调用来等待套接字完成,因为这可能导致严重的线程处理问题。 请始终使用 await

为现有控制器添加 HTTP/2 WebSockets 支持

.NET 7 为 Kestrel、SignalR JavaScript 客户端和带有 Blazor WebAssembly 的 SignalR 引入了基于 HTTP/2 的 Websockets 支持。 HTTP/2 WebSockets 使用 CONNECT 请求,而不是 GET。 如果以前在用于 Websocket 请求的控制器操作方法上使用过 [HttpGet("/path")],请改为将其更新到 [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 攻击。 如果发送敏感信息,请避免在调用 WebSocket.SendAsync 时启用压缩或使用 WebSocketMessageFlags.DisableCompression。 这适用于 WebSocket 的两端。 请注意,浏览器中的 WebSockets API 没有为每次发送禁用压缩的配置。

如果需要通过 WebSockets 压缩消息,则接受代码必须指定它允许压缩,如下所示:

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

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.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 时,请求停止在管道中推进。 循环结束且套接字关闭时,请求继续回到管道。

处理客户端连接断开

当客户端由于失去连接而断开连接时,不会自动向服务器发送通知。 服务器只有在客户端发送通知时才会收到断开连接消息,而此操作无法在失去 Internet 连接的情况下进行。 如果想要在发生此情况时采取某个操作,在特定时间范围内未收到来自客户端的任何消息后设置超时。

如果客户端并非总是发送消息,并且你不希望仅由于连接进入空闲状态就设置超时,则让客户端使用一个计时器并每隔 X 秒发送一条 ping 消息。 在服务器上,如果某条消息在上一条消息发出后的 2*X 秒内尚未到达,则终止连接并报告客户端已断开连接。 等待两次预测的时间间隔,以便为可能延迟 ping 消息的网络延迟提供额外的时间。

WebSocket 源限制

CORS 提供的保护不适用于 WebSocket。 浏览器不会:

  • 执行 CORS 预检请求。
  • 在发出 WebSocket 请求时,遵守 Access-Control 标头中指定的限制。

但是,浏览器在发出 WebSocket 请求时会发送 Origin 标头。 应将应用程序配置为验证这些标头,以确保只允许来自预期来源的 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);

注意

Referer 标头一样,Origin 标头由客户端控制,并可以伪造。 请勿将这些标头用作身份验证机制。

IIS/IIS Express 支持

安装了 IIS/IIS Express 8 或更高版本的 Windows Server 2012 或更高版本以及 Windows 8 或更高版本支持 WebSocket 协议,但不支持基于 HTTP/2 的 WebSockets。

注意

使用 IIS Express 时始终启用 WebSocket。

在 IIS 上启用 Websocket

在 Windows Server 2012 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 通过“管理”菜单或“服务器管理器”中的链接使用“添加角色和功能”向导。
  2. 选择“基于角色或基于功能的安装”。 选择“下一步” 。
  3. 选择适当的服务器(默认情况下选择本地服务器)。 选择“下一步” 。
  4. 在“角色”树中展开“Web 服务器 (IIS)”、然后依次展开“Web 服务器”和“应用程序开发” 。
  5. 选择“WebSocket 协议”。 选择“下一步” 。
  6. 如果无需其他功能,请选择“下一步”。
  7. 选择“安装” 。
  8. 安装完成后,选择“关闭”以退出向导。

在 Windows 8 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 导航到“控制面板”>“程序”>“程序和功能”>“启用或禁用 Windows 功能”(位于屏幕左侧) 。
  2. 打开以下节点:“Internet Information Services”>“万维网服务”>“应用程序开发功能” 。
  3. 选择“WebSocket 协议”功能。 选择“确定”。

在 Node.js 上使用 socket.io 时禁用 WebSocket

如果在 Node.jssocket.io 中使用 WebSocket 支持,请使用 web.config 或 applicationHost.config 中的 webSocket 元素禁用默认的 IIS WebSocket 模块 。如果不执行此步骤,IIS WebSocket 模块将尝试处理 WebSocket 通信而不是 Node.js 和应用。

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

示例应用

本文附带的示例应用是一个 echo 应用。 它有一个可建立 WebSocket 连接的网页,且服务器将其收到的消息都重新发回到客户端。 使用 .NET 7 或更高版本的目标框架时,示例应用支持基于 HTTP/2 的 WebSocket。

运行应用:

  • 在 Visual Studio 中运行应用:在 Visual Studio 中打开示例项目,然后按 Ctrl+F5 在不使用调试程序的情况下运行。
  • 若要在命令行界面中运行应用:运行命令 dotnet run 并在浏览器中导航到 http://localhost:<port>

该网页显示连接状态:

Initial state of webpage before WebSockets connection

选择“连接”,向显示的 URL 发送 WebSocket 请求。 输入测试消息并选择“发送”。 完成后,请选择“关闭套接字”。 “通信日志”部分会报告每一个发生的“打开”、“发送”和“关闭”操作。

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

本文介绍 ASP.NET Core 中 WebSocket 的入门方法。 WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道。 它用于从快速实时通信中获益的应用,如聊天、仪表板和游戏应用。

查看或下载示例代码如何下载如何运行)。

SignalR

ASP.NET Core SignalR 是一个库,可用于简化向应用添加实时 Web 功能。 它会尽可能地使用 WebSocket。

对于大多数应用程序,我们建议使用 SignalR,而不是原始 WebSocket。 SignalR 可为 WebSocket 不可用的环境提供传输回退。 它还可提供基本的远程过程调用应用模型。 并且在大多数情况下,与使用原始 WebSocket 相比,SignalR 没有显著的性能缺点。

对于某些应用,.NET 上的 gRPC 提供了 WebSocket 的替代方法。

先决条件

  • 支持 ASP.NET Core 的任何操作系统:
    • Windows 7/Windows Server 2008 或更高版本
    • Linux
    • macOS
  • 如果应用在安装了 IIS 的 Windows 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
    • IIS 8 / IIS 8 Express
    • 必须启用 WebSocket。 请参阅 IIS/IIS Express 支持部分。
  • 如果应用在 HTTP.sys 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
  • 有关支持的浏览器,请参阅能否使用

配置中间件

Program.cs 中添加 WebSocket 中间件:

app.UseWebSockets();

可配置以下设置:

  • KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 默认值为 2 分钟。
  • AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。 默认情况下,允许使用所有源。 有关详细信息,请参阅本文中的 WebSocket 源限制
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

接受 WebSocket 请求

在请求生命周期后期(例如在 Program.cs 或操作方法的后期),检查它是否是 WebSocket 请求并接受 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。 在请求期间对 Task 执行 await,如下面的示例所示:

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.WaitTask.Result 或类似阻塞调用来等待套接字完成,因为这可能导致严重的线程处理问题。 请始终使用 await

压缩

警告

通过加密连接启用压缩可能会使应用受到 CRIME/BREACH 攻击。 如果发送敏感信息,请避免在调用 WebSocket.SendAsync 时启用压缩或使用 WebSocketMessageFlags.DisableCompression。 这适用于 WebSocket 的两端。 请注意,浏览器中的 WebSockets API 没有为每次发送禁用压缩的配置。

如果需要通过 WebSockets 压缩消息,则接受代码必须指定它允许压缩,如下所示:

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

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.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 时,请求停止在管道中推进。 循环结束且套接字关闭时,请求继续回到管道。

处理客户端连接断开

当客户端由于失去连接而断开连接时,不会自动向服务器发送通知。 服务器只有在客户端发送通知时才会收到断开连接消息,而此操作无法在失去 Internet 连接的情况下进行。 如果想要在发生此情况时采取某个操作,在特定时间范围内未收到来自客户端的任何消息后设置超时。

如果客户端并非总是发送消息,并且你不希望仅由于连接进入空闲状态就设置超时,则让客户端使用一个计时器并每隔 X 秒发送一条 ping 消息。 在服务器上,如果某条消息在上一条消息发出后的 2*X 秒内尚未到达,则终止连接并报告客户端已断开连接。 等待两次预测的时间间隔,以便为可能延迟 ping 消息的网络延迟提供额外的时间。

WebSocket 源限制

CORS 提供的保护不适用于 WebSocket。 浏览器不会:

  • 执行 CORS 预检请求。
  • 在发出 WebSocket 请求时,遵守 Access-Control 标头中指定的限制。

但是,浏览器在发出 WebSocket 请求时会发送 Origin 标头。 应将应用程序配置为验证这些标头,以确保只允许来自预期来源的 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);

注意

Referer 标头一样,Origin 标头由客户端控制,并可以伪造。 请勿将这些标头用作身份验证机制。

IIS/IIS Express 支持

安装了 IIS/IIS Express 8 或更高版本的 Windows Server 2012 或更高版本以及 Windows 8 或更高版本支持 WebSocket 协议。

注意

使用 IIS Express 时始终启用 WebSocket。

在 IIS 上启用 Websocket

在 Windows Server 2012 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 通过“管理”菜单或“服务器管理器”中的链接使用“添加角色和功能”向导。
  2. 选择“基于角色或基于功能的安装”。 选择“下一步” 。
  3. 选择适当的服务器(默认情况下选择本地服务器)。 选择“下一步” 。
  4. 在“角色”树中展开“Web 服务器 (IIS)”、然后依次展开“Web 服务器”和“应用程序开发” 。
  5. 选择“WebSocket 协议”。 选择“下一步” 。
  6. 如果无需其他功能,请选择“下一步”。
  7. 选择“安装” 。
  8. 安装完成后,选择“关闭”以退出向导。

在 Windows 8 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 导航到“控制面板”>“程序”>“程序和功能”>“启用或禁用 Windows 功能”(位于屏幕左侧) 。
  2. 打开以下节点:“Internet Information Services”>“万维网服务”>“应用程序开发功能” 。
  3. 选择“WebSocket 协议”功能。 选择“确定”。

在 Node.js 上使用 socket.io 时禁用 WebSocket

如果在 Node.jssocket.io 中使用 WebSocket 支持,请使用 web.config 或 applicationHost.config 中的 webSocket 元素禁用默认的 IIS WebSocket 模块 。如果不执行此步骤,IIS WebSocket 模块将尝试处理 WebSocket 通信而不是 Node.js 和应用。

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

示例应用

本文附带的示例应用是一个 echo 应用。 它有一个可建立 WebSocket 连接的网页,且服务器将其收到的消息都重新发回到客户端。 示例应用未配置为使用 IIS Express 从 Visual Studio 运行,因此请在命令行界面中使用 dotnet run 运行应用,并在浏览器中导航到 http://localhost:<port>。 该网页显示连接状态:

Initial state of webpage before WebSockets connection

选择“连接”,向显示的 URL 发送 WebSocket 请求。 输入测试消息并选择“发送”。 完成后,请选择“关闭套接字”。 “通信日志”部分会报告每一个发生的“打开”、“发送”和“关闭”操作。

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

本文介绍 ASP.NET Core 中 WebSocket 的入门方法。 WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道。 它用于从快速实时通信中获益的应用,如聊天、仪表板和游戏应用。

查看或下载示例代码如何下载)。 如何运行

SignalR

ASP.NET Core SignalR 是一个库,可用于简化向应用添加实时 Web 功能。 它会尽可能地使用 WebSocket。

对于大多数应用程序,我们建议使用 SignalR,而不是原始 WebSocket。 SignalR 可为 WebSocket 不可用的环境提供传输回退。 它还可提供基本的远程过程调用应用模型。 并且在大多数情况下,与使用原始 WebSocket 相比,SignalR 没有显著的性能缺点。

对于某些应用,.NET 上的 gRPC 提供了 WebSocket 的替代方法。

先决条件

  • 支持 ASP.NET Core 的任何操作系统:
    • Windows 7/Windows Server 2008 或更高版本
    • Linux
    • macOS
  • 如果应用在安装了 IIS 的 Windows 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
    • IIS 8 / IIS 8 Express
    • 必须启用 WebSocket。 请参阅 IIS/IIS Express 支持部分。
  • 如果应用在 HTTP.sys 上运行:
    • Windows 8 / Windows Server 2012 及更高版本
  • 有关支持的浏览器,请参阅能否使用

配置中间件

Startup 类的 Configure 方法中添加 WebSocket 中间件:

app.UseWebSockets();

注意

如果您想要接受控制器中的 WebSocket 请求,必须在 app.UseEndpoints 之前调用 app.UseWebSockets

可配置以下设置:

  • KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 默认值为 2 分钟。
  • AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。 默认情况下,允许使用所有源。 有关详细信息,请参阅以下“WebSocket 源限制”。
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

接受 WebSocket 请求

在请求生命周期后期(例如在 Configure 方法或操作方法的后期),检查它是否是 WebSocket 请求并接受 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。 在请求期间对 Task 执行 await,如下面的示例所示:

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.WaitTask.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 时,请求停止在管道中推进。 循环结束且套接字关闭时,请求继续回到管道。

处理客户端连接断开

当客户端由于失去连接而断开连接时,不会自动向服务器发送通知。 服务器只有在客户端发送通知时才会收到断开连接消息,而此操作无法在失去 Internet 连接的情况下进行。 如果想要在发生此情况时采取某个操作,在特定时间范围内未收到来自客户端的任何消息后设置超时。

如果客户端并非总是发送消息,并且你不希望仅由于连接进入空闲状态就设置超时,则让客户端使用一个计时器并每隔 X 秒发送一条 ping 消息。 在服务器上,如果某条消息在上一条消息发出后的 2*X 秒内尚未到达,则终止连接并报告客户端已断开连接。 等待两次预测的时间间隔,以便为可能延迟 ping 消息的网络延迟提供额外的时间。

注意

如果 KeepAliveInterval 选项(默认为 30 秒 (TimeSpan.FromSeconds(30)))大于零,则内部 ManagedWebSocket 将隐式处理 Ping/Pong 帧,以使连接保持活动状态。

WebSocket 源限制

CORS 提供的保护不适用于 WebSocket。 浏览器不会:

  • 执行 CORS 预检请求。
  • 在发出 WebSocket 请求时,遵守 Access-Control 标头中指定的限制。

但是,浏览器在发出 WebSocket 请求时会发送 Origin 标头。 应将应用程序配置为验证这些标头,以确保只允许来自预期来源的 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);

注意

Referer 标头一样,Origin 标头由客户端控制,并可以伪造。 请勿将这些标头用作身份验证机制。

IIS/IIS Express 支持

安装了 IIS/IIS Express 8 或更高版本的 Windows Server 2012 或更高版本以及 Windows 8 或更高版本支持 WebSocket 协议。

注意

使用 IIS Express 时始终启用 WebSocket。

在 IIS 上启用 Websocket

在 Windows Server 2012 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 通过“管理”菜单或“服务器管理器”中的链接使用“添加角色和功能”向导。
  2. 选择“基于角色或基于功能的安装”。 选择“下一步” 。
  3. 选择适当的服务器(默认情况下选择本地服务器)。 选择“下一步” 。
  4. 在“角色”树中展开“Web 服务器 (IIS)”、然后依次展开“Web 服务器”和“应用程序开发” 。
  5. 选择“WebSocket 协议”。 选择“下一步” 。
  6. 如果无需其他功能,请选择“下一步”。
  7. 选择“安装” 。
  8. 安装完成后,选择“关闭”以退出向导。

在 Windows 8 或更高版本上启用对 WebSocket 协议的支持:

注意

使用 IIS Express 时无需执行这些步骤

  1. 导航到“控制面板”>“程序”>“程序和功能”>“启用或禁用 Windows 功能”(位于屏幕左侧) 。
  2. 打开以下节点:“Internet Information Services”>“万维网服务”>“应用程序开发功能” 。
  3. 选择“WebSocket 协议”功能。 选择“确定”。

在 Node.js 上使用 socket.io 时禁用 WebSocket

如果在 Node.jssocket.io 中使用 WebSocket 支持,请使用 web.config 或 applicationHost.config 中的 webSocket 元素禁用默认的 IIS WebSocket 模块 。如果不执行此步骤,IIS WebSocket 模块将尝试处理 WebSocket 通信而不是 Node.js 和应用。

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

示例应用

本文附带的示例应用是一个 echo 应用。 它有一个可建立 WebSocket 连接的网页,且服务器将其收到的消息都重新发回到客户端。 示例应用未配置为使用 IIS Express 从 Visual Studio 运行,因此请在命令行界面中使用 dotnet run 运行应用,并在浏览器中导航到 http://localhost:5000。 该网页显示连接状态:

Initial state of webpage before WebSockets connection

选择“连接”,向显示的 URL 发送 WebSocket 请求。 输入测试消息并选择“发送”。 完成后,请选择“关闭套接字”。 “通信日志”部分会报告每一个发生的“打开”、“发送”和“关闭”操作。

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