本文介绍 ASP.NET Core 中 WebSocket 的入门方法。 WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道。 它用于从快速实时通信中获益的应用,如聊天、仪表板和游戏应用。
支持 Http/2 WebSocket
使用基于 HTTP/2 的 WebSockets 可利用以下新功能:
- 标头压缩。
- 多路复用,可减少向服务器发出多个请求时所需的时间和资源。
所有支持 HTTP/2 的平台上的 Kestrel 中都提供了这些受支持的功能。 版本协商在浏览器和 Kestrel 中是自动的,因此不需要新的 API。
.NET 7 为 Kestrel(SignalR JavaScript 客户端)和包含 SignalR 的 Blazor WebAssembly 引入了基于 HTTP/2 的 Websocket 支持。
说明
HTTP/2 WebSockets 使用 CONNECT 请求而不是 GET,因此可能需要更新你自己的路由和控制器。 有关详细信息,请参阅本文中的为现有控制器添加 HTTP/2 WebSockets 支持。
默认情况下,Chrome、Edge 和 Firefox(版本 128 及更高版本)已启用 HTTP/2 WebSocket。 可以通过在 Firefox 中打开about:config 并查找network.http.http2.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 的替代方法。
Prerequisites
- 支持 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 分钟。
-
KeepAliveTimeout – 发送“ping”帧后,等待客户端返回“pong”帧的时间长度。 如果超过超时时间,WebSocket 连接将被中止,并且
WebSocket.ReceiveAsync会引发异常。 默认值为Timeout.InfiniteTimeSpan(禁用)。 有关详细信息,请参阅本文中的 “处理客户端断开连接 ”。 - 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.Wait、Task.Result 或类似的阻塞调用来等待套接字操作完成,因为这可能导致严重的线程问题。 请始终使用 await。
为现有控制器添加 HTTP/2 WebSockets 支持
.NET 7 为 Kestrel(SignalR JavaScript 客户端)和包含 SignalR 的 Blazor WebAssembly 引入了基于 HTTP/2 的 Websocket 支持。 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;
}
}
Compression
警告
通过加密连接启用压缩可能会使应用受到 CRIME/BREACH 攻击。
如果发送敏感信息,请避免在调用 WebSocketMessageFlags.DisableCompression 时启用压缩或使用 WebSocket.SendAsync。
这适用于 WebSocket 的两端。 请注意,浏览器中的 WebSockets API 没有为每次发送禁用压缩的配置。
如果需要通过 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 连接被接受后,请求就不会继续沿管道向前传递。 循环结束且套接字关闭时,请求继续回到管道。
处理客户端断开连接
当客户端由于失去连接而断开连接时,不会自动向服务器发送通知。 服务器只有在客户端发送通知时才会收到断开连接消息,而此操作无法在失去 Internet 连接的情况下进行。 如果想要在发生此情况时采取某个操作,在特定时间范围内未收到来自客户端的任何消息后设置超时。
如果客户端并非总是发送消息,并且你不希望仅由于连接进入空闲状态就设置超时,则让客户端使用一个计时器并每隔 X 秒发送一条 ping 消息。 在服务器上,如果某条消息在上一条消息发出后的 2*X 秒内尚未到达,则终止连接并报告客户端已断开连接。 等待预期时间间隔的两倍,为可能导致 ping 消息延后的网络延迟预留额外时间。
保持活动状态超时
WebSockets 中间件可配置保持活动超时。 当同时满足以下两个条件时,保持活动超时会终止 WebSocket 连接,并从 WebSocket.ReceiveAsync 抛出异常:
- 服务器使用 WebSocket 协议发送 ping 帧。
- 客户端未在指定的超时时间内以 pong 帧进行响应。
服务器会自动发送 ping 帧并配置其频率 KeepAliveInterval。
保持活动超时设置有助于检测可能速度较慢或非正常断开的连接。 默认值为 Timeout.InfiniteTimeSpan禁用超时。
可以为 WebSocket 中间件全局配置保持活动超时:
app.UseWebSockets(new WebSocketOptions { KeepAliveTimeout = TimeSpan.FromSeconds(15) });
或按接受的 WebSocket 进行配置:
using var webSocket = await context.WebSockets.AcceptWebSocketAsync(
new WebSocketAcceptContext { KeepAliveTimeout = TimeSpan.FromSeconds(15) });
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);
说明
与 Origin 标头一样,Referer 标头由客户端控制,并可以伪造。 请勿将这些标头用作身份验证机制。
IIS/IIS Express 支持
Windows Server 2012 或更高版本以及 Windows 8 或更高版本在配合 IIS/IIS Express 8 或更高版本时,均支持 WebSocket 协议,但不支持通过 HTTP/2 使用的 WebSocket。
说明
使用 IIS Express 时始终启用 WebSocket。
在 IIS 上启用 Websocket
在 Windows Server 2012 或更高版本上启用对 WebSocket 协议的支持:
说明
使用 IIS Express 时无需执行这些步骤
- 通过“管理”菜单或“服务器管理器”中的链接使用“添加角色和功能”向导。
- 选择“基于角色或基于功能的安装”。 选择“下一步”。
- 选择适当的服务器(默认情况下选择本地服务器)。 选择“下一步”。
- 在“角色”树中展开“Web 服务器 (IIS)”,展开“Web 服务器”,然后再展开“应用程序开发”。
- 选择“WebSocket 协议”。 选择“下一步”。
- 如果无需其他功能,请选择“下一步”。
- 选择“安装”。
- 安装完成后,选择“关闭”以退出向导。
在 Windows 8 或更高版本上启用对 WebSocket 协议的支持:
说明
使用 IIS Express 时无需执行这些步骤
- 导航到“控制面板”“程序”>“程序和功能”“启用或禁用 Windows 功能”(位于屏幕左侧) 。
- 打开以下节点:“Internet Information Services”“万维网服务”>“应用程序开发功能” 。
- 选择“WebSocket 协议”功能。 选择“确定”。
在 Node.js 上使用 socket.io 时禁用 WebSocket
如果在 Node.js上的 socket.io 中使用 WebSocket 支持,请使用 webSocket 或 applicationHost.config中的 元素禁用默认 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>。
该网页显示连接状态:
选择“连接”,向显示的 URL 发送 WebSocket 请求。 输入测试消息并选择“发送”。 完成后,请选择 关闭套接字。 “通信日志”部分会报告每一个发生的“打开”、“发送”和“关闭”操作。
原文:ASP.NET Core 中的 WebSocket 支持。作者/来源:Microsoft Learn 文档贡献者。本文依据所列原文整理为中文,代码、命令与配置示例保留原文。











暂无评论内容