ASP.NET Core 中的 WebSocket 支持WebSockets support in ASP.NET Core

作者:Tom DykstraAndrew Stanton-NurseBy Tom Dykstra and Andrew Stanton-Nurse

本文介绍 ASP.NET Core 中 WebSocket 的入门方法。This article explains how to get started with WebSockets in ASP.NET Core. WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道。WebSocket (RFC 6455) is a protocol that enables two-way persistent communication channels over TCP connections. 它用于从快速实时通信中获益的应用,如聊天、仪表板和游戏应用。It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

查看或下载示例代码如何下载)。View or download sample code (how to download). 如何运行How to run.

SignalRSignalR

ASP.NET Core SignalR 是一个库,可用于简化向应用添加实时 Web 功能。ASP.NET Core SignalR is a library that simplifies adding real-time web functionality to apps. 它会尽可能地使用 WebSocket。It uses WebSockets whenever possible.

对于大多数应用程序,我们建议使用 SignalR,而不是原始 Websocket。For most applications, we recommend SignalR over raw WebSockets. SignalR 可为 Websocket 不可用的环境提供传输回退。SignalR provides transport fallback for environments where WebSockets is not available. 它还可提供简单的远程过程调用应用模型。It also provides a simple remote procedure call app model. 并且在大多数情况下,与使用原始 Websocket 相比,SignalR 没有显著的性能缺点。And in most scenarios, SignalR has no significant performance disadvantage compared to using raw WebSockets.

系统必备Prerequisites

  • ASP.NET Core 1.1 或更高版本ASP.NET Core 1.1 or later

  • 支持 ASP.NET Core 的任何操作系统:Any OS that supports ASP.NET Core:

    • Windows 7/Windows Server 2008 或更高版本Windows 7 / Windows Server 2008 or later
    • LinuxLinux
    • macOSmacOS
  • 如果应用在安装了 IIS 的 Windows 上运行:If the app runs on Windows with IIS:

    • Windows 8 / Windows Server 2012 及更高版本Windows 8 / Windows Server 2012 or later
    • IIS 8 / IIS 8 ExpressIIS 8 / IIS 8 Express
    • 必须启用 WebSocket(请参阅 IIS/IIS Express 支持部分。)。WebSockets must be enabled (See the IIS/IIS Express support section.).
  • 如果应用在 HTTP.sys 上运行:If the app runs on HTTP.sys:

    • Windows 8 / Windows Server 2012 及更高版本Windows 8 / Windows Server 2012 or later
  • 有关支持的浏览器,请参阅 https://caniuse.com/#feat=websockets。For supported browsers, see https://caniuse.com/#feat=websockets.

NuGet 程序包NuGet package

安装 Microsoft.AspNetCore.WebSockets 包。Install the Microsoft.AspNetCore.WebSockets package.

配置中间件Configure the middleware

Startup 类的 Configure 方法中添加 WebSocket 中间件:Add the WebSockets middleware in the Configure method of the Startup class:

app.UseWebSockets();

可配置以下设置:The following settings can be configured:

  • KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。KeepAliveInterval - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. 默认值为 2 分钟。The default is two minutes.
  • ReceiveBufferSize - 用于接收数据的缓冲区的大小。ReceiveBufferSize - The size of the buffer used to receive data. 高级用户可能需要对其进行更改,以便根据数据大小调整性能。Advanced users may need to change this for performance tuning based on the size of the data. 默认值为 4 KB。The default is 4 KB.

可配置以下设置:The following settings can be configured:

  • KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。KeepAliveInterval - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. 默认值为 2 分钟。The default is two minutes.
  • ReceiveBufferSize - 用于接收数据的缓冲区的大小。ReceiveBufferSize - The size of the buffer used to receive data. 高级用户可能需要对其进行更改,以便根据数据大小调整性能。Advanced users may need to change this for performance tuning based on the size of the data. 默认值为 4 KB。The default is 4 KB.
  • AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。AllowedOrigins - A list of allowed Origin header values for WebSocket requests. 默认情况下,允许使用所有源。By default, all origins are allowed. 有关详细信息,请参阅以下“WebSocket 源限制”。See "WebSocket origin restriction" below for details.
var webSocketOptions = new WebSocketOptions() 
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
    ReceiveBufferSize = 4 * 1024
};

app.UseWebSockets(webSocketOptions);

接受 WebSocket 请求Accept WebSocket requests

在请求生命周期后期(例如在 Configure 方法或操作方法的后期),检查它是否是 WebSocket 请求并接受 WebSocket 请求。Somewhere later in the request life cycle (later in the Configure method or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

以下示例来自 Configure 方法的后期:The following example is from later in the Configure method:

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

});

WebSocket 请求可以来自任何 URL,但此示例代码只接受 /ws 的请求。A WebSocket request could come in on any URL, but this sample code only accepts requests for /ws.

使用 WebSocket 时,“必须” 在连接期间保持中间件管道运行。When using a WebSocket, you must keep the middleware pipeline running for the duration of the connection. 如果在中间件管道结束后尝试发送或接收 WebSocket 消息,可能会遇到以下异常情况:If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

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,请确保保持中间件管道运行。If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. 通过使用 TaskCompletionSource<TResult> 执行此操作。Do this by using a TaskCompletionSource<TResult>. 传递 TaskCompletionSource 到背景服务,并在通过 WebSocket 完成时让其调用 TrySetResultPass the TaskCompletionSource to your background service and have it call TrySetResult when you finish with the WebSocket. 在请求期间对 Task 执行 await,如下面的示例所示:Then await the Task property during the request, as shown in the following example:

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

    BackgroundSocketProcessor.AddSocket(socket, socketFinishedTcs); 

    await socketFinishedTcs.Task;
});

如果从操作方法返回过快,则还可能发生 WebSocket 关闭异常。The WebSocket closed exception can also happen if you return too soon from an action method. 如果接受操作方法中的套接字,请等待使用套接字的代码完成运行,然后再从操作方法返回。If you accept a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

坚决不要使用 Task.Wait()Task.Result 或类似阻塞调用来等待套接字完成,因为这可能导致严重的线程处理问题。Never use Task.Wait(), Task.Result, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. 请始终使用 awaitAlways use await.

发送和接收消息Send and receive messages

AcceptWebSocketAsync 方法将 TCP 连接升级到 WebSocket 连接,并提供 WebSocket 对象。The AcceptWebSocketAsync method upgrades the TCP connection to a WebSocket connection and provides a WebSocket object. 使用 WebSocket 对象发送和接收消息。Use the WebSocket object to send and receive messages.

之前显示的接受 WebSocket 请求的代码将 WebSocket 对象传递给 Echo 方法。The code shown earlier that accepts the WebSocket request passes the WebSocket object to an Echo method. 代码接收消息并立即发回相同的消息。The code receives a message and immediately sends back the same message. 循环发送和接收消息,直到客户端关闭连接:Messages are sent and received in a loop until the client closes the connection:

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 连接,中间件管道会结束。When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. 关闭套接字后,管道展开。Upon closing the socket, the pipeline unwinds. 即接受 WebSocket 时,请求停止在管道中推进。That is, the request stops moving forward in the pipeline when the WebSocket is accepted. 循环结束且套接字关闭时,请求继续回到管道。When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

处理客户端连接断开Handle client disconnects

当客户端由于失去连接而断开连接时不会自动向服务器发送通知。The server is not automatically informed when the client disconnects due to loss of connectivity. 服务器只有在客户端发送通知时才会收到断开连接消息,而此操作无法在失去 Internet 连接的情况下进行。The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. 如果想要在发生此情况时采取某个操作,在特定时间范围内未收到来自客户端的任何消息后设置超时。If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

如果客户端并非总是发送消息且不希望仅由于连接进入空闲状态就设置超时,则让客户端使用一个计时器并每隔 X 秒发送一条 ping 消息。If the client isn't always sending messages and you don't want to timeout just because the connection goes idle, have the client use a timer to send a ping message every X seconds. 在服务器上,如果某条消息在上一条消息发出后的 2*X 秒内尚未到达,则终止连接并报告客户端已断开连接。On the server, if a message hasn't arrived within 2*X seconds after the previous one, terminate the connection and report that the client disconnected. 等待两次预测的时间间隔,以便为可能延迟 ping 消息的网络延迟提供额外的时间。Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

WebSocket 源限制WebSocket origin restriction

CORS 提供的保护不适用于 WebSocket。The protections provided by CORS don't apply to WebSockets. 浏览器不会 :Browsers do not:

  • 执行 CORS 预检请求。Perform CORS pre-flight requests.
  • 在发出 WebSocket 请求时,遵守 Access-Control 标头中指定的限制。Respect the restrictions specified in Access-Control headers when making WebSocket requests.

但是,浏览器在发出 WebSocket 请求时会发送 Origin 标头。However, browsers do send the Origin header when issuing WebSocket requests. 应将应用程序配置为验证这些标头,以确保只允许来自预期来源的 WebSocket。Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

如果在“https://server.com”上托管服务器并在“https://client.com”上托管客户端,请将“https://client.com”添加到 AllowedOrigins 列表以验证 WebSocket。If you're hosting your server on "https://server.com" and hosting your client on "https://client.com", add "https://client.com" to the AllowedOrigins list for WebSockets to verify.

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

app.UseWebSockets(webSocketOptions);

备注

Referer 标头一样,Origin 标头由客户端控制,并可以伪造。The Origin header is controlled by the client and, like the Referer header, can be faked. 请勿将这些标头用作身份验证机制。Do not use these headers as an authentication mechanism.

IIS/IIS Express 支持IIS/IIS Express support

安装了 IIS/IIS Express 8 或更高版本的 Windows Server 2012 或更高版本以及 Windows 8 或更高版本支持 WebSocket 协议。Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol.

备注

使用 IIS Express 时始终启用 WebSocket。WebSockets are always enabled when using IIS Express.

在 IIS 上启用 WebsocketEnabling WebSockets on IIS

在 Windows Server 2012 或更高版本上启用对 WebSocket 协议的支持:To enable support for the WebSocket protocol on Windows Server 2012 or later:

备注

使用 IIS Express 时无需执行这些步骤These steps are not required when using IIS Express

  1. 通过“管理” 菜单或“服务器管理器” 中的链接使用“添加角色和功能” 向导。Use the Add Roles and Features wizard from the Manage menu or the link in Server Manager.
  2. 选择“基于角色或基于功能的安装” 。Select Role-based or Feature-based Installation. 选择“下一步” 。Select Next.
  3. 选择适当的服务器(默认情况下选择本地服务器)。Select the appropriate server (the local server is selected by default). 选择“下一步” 。Select Next.
  4. 在“角色”树中展开“Web 服务器 (IIS)”、然后依次展开“Web 服务器”和“应用程序开发” 。Expand Web Server (IIS) in the Roles tree, expand Web Server, and then expand Application Development.
  5. 选择“WebSocket 协议” 。Select WebSocket Protocol. 选择“下一步” 。Select Next.
  6. 如果无需其他功能,请选择“下一步” 。If additional features aren't needed, select Next.
  7. 选择“安装” 。Select Install.
  8. 安装完成后,选择“关闭”以退出向导 。When the installation completes, select Close to exit the wizard.

在 Windows 8 或更高版本上启用对 WebSocket 协议的支持:To enable support for the WebSocket protocol on Windows 8 or later:

备注

使用 IIS Express 时无需执行这些步骤These steps are not required when using IIS Express

  1. 导航到“控制面板” > “程序” > “程序和功能” > “打开或关闭 Windows 功能”(位于屏幕左侧) 。Navigate to Control Panel > Programs > Programs and Features > Turn Windows features on or off (left side of the screen).
  2. 打开以下节点:“Internet Information Services” > “万维网服务” > “应用程序开发功能” 。Open the following nodes: Internet Information Services > World Wide Web Services > Application Development Features.
  3. 选择“WebSocket 协议” 功能。Select the WebSocket Protocol feature. 选择“确定” 。Select OK.

在 Node.js 上使用 socket.io 时禁用 WebSocketDisable WebSocket when using socket.io on Node.js

如果在 Node.jssocket.io 中使用 WebSocket 支持,请使用 web.config 或 applicationHost.config 中的 webSocket 元素禁用默认的 IIS WebSocket 模块 。如果不执行此步骤,IIS WebSocket 模块将尝试处理 WebSocket 通信而不是 Node.js 和应用。If using the WebSocket support in socket.io on Node.js, disable the default IIS WebSocket module using the webSocket element in web.config or applicationHost.config. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

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

示例应用Sample app

本文附带的示例应用是一个 echo 应用。The sample app that accompanies this article is an echo app. 它有一个可建立 WebSocket 连接的网页,且服务器将其收到的消息重新发回到客户端。It has a web page that makes WebSocket connections, and the server resends any messages it receives back to the client. 从命令提示符运行该应用(它未设置为在安装了 IIS Express 的 Visual Studio 中运行)并导航到 http://localhost:5000。Run the app from a command prompt (it's not set up to run from Visual Studio with IIS Express) and navigate to http://localhost:5000. 该网页的左上方显示连接状态:The web page shows the connection status in the upper left:

网页的初始状态

选择“连接”,向显示的 URL 发送 WebSocket 请求 。Select Connect to send a WebSocket request to the URL shown. 输入测试消息并选择“发送” 。Enter a test message and select Send. 完成后,请选择“关闭套接字” 。When done, select Close Socket. “通信日志” 部分会报告每一个发生的“打开”、“发送”和“关闭”操作。The Communication Log section reports each open, send, and close action as it happens.

网页的初始状态