ASP.NET Core 中的 WebSockets 支援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.

SignalR

ASP.NET Core SignalR 是可簡化將即時 web 功能新增至應用程式的程式庫。ASP.NET Core SignalR is a library that simplifies adding real-time web functionality to apps. 它會盡可能使用 WebSockets。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 basic remote procedure call app model. 在大部分的情況下, SignalR 相較于使用原始 websocket,沒有顯著的效能缺點。And in most scenarios, SignalR has no significant performance disadvantage compared to using raw WebSockets.

針對某些應用程式, gRPC on .net 提供 websocket 的替代方案。For some apps, gRPC on .NET provides an alternative to WebSockets.

先決條件Prerequisites

  • 支援 ASP.NET Core 的任何作業系統:Any OS that supports ASP.NET Core:
    • Windows 7/Windows Server 2008 或更新版本Windows 7 / Windows Server 2008 or later
    • LinuxLinux
    • macOSmacOS
  • 如果應用程式在 Windows 上與 IIS 搭配執行: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。WebSockets must be enabled. 請參閱 IIS/IIS Express 支援 一節。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.

設定中介軟體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" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。KeepAliveInterval - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. 預設為兩分鐘。The default is two minutes.

您可以設定下列設定:The following settings can be configured:

  • KeepAliveInterval - 要將 "ping" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。KeepAliveInterval - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. 預設為兩分鐘。The default is two minutes.
  • AllowedOrigins - WebSocket 要求之允許 Origin 標頭值的清單。AllowedOrigins - A list of allowed Origin header values for WebSocket requests. 根據預設,會允許所有來源。By default, all origins are allowed. 如需詳細資訊,請參閱下方的 "WebSocket origin restriction" (WebSocket 來源限制)。See "WebSocket origin restriction" below for details.
var webSocketOptions = new WebSocketOptions() 
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

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)
        {
            using (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. 然後,在要求期間,awaitTask 屬性,如下列範例所示:Then await the Task property during the request, as shown in the following example:

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 封閉式例外狀況。The WebSocket closed exception can also happen when returning too soon from an action method. 在動作方法中接受通訊端時,請等候使用通訊端的程式碼完成後,再從動作方法返回。When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

絕不要使用 Task.WaitTask.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. 只有當用戶端傳送通知時,伺服器才會收到連線中斷訊息,而在網際網路連線中斷的情況下無法傳送通知。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 清單中,以讓 WebSockets 進行驗證。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),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

注意

因為 Origin 由用戶端控制,所以和 Referer 標頭一樣可能受到偽造。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 時一律會啟用 WebSockets。WebSockets are always enabled when using IIS Express.

在 IIS 上啟用 WebSocketsEnabling 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. 展開 [角色] 樹狀目錄中的 [網頁伺服器 (IIS)],展開 [網頁伺服器],然後展開 [應用程式開發]。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 > World Wide Web 服務 > 應用程式開發功能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.js上使用 Socket.io中的 WebSocket 支援,請使用 webSocket web.configapplicationHost.config 中的元素,停用預設的 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

本文附帶的範例應用程式是回應應用程式。The sample app that accompanies this article is an echo app. 它有一個可進行 WebSocket 連線的網頁,而伺服器會將其接收的任何訊息重新傳送回用戶端。It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. 範例應用程式未設定為從具有 IIS Express 的 Visual Studio 執行,因此請在的命令 shell 中執行應用程式, dotnet run 並在瀏覽器中流覽至 http://localhost:5000The sample app isn't configured to run from Visual Studio with IIS Express, so run the app in a command shell with dotnet run and navigate in a browser to http://localhost:5000. 網頁會顯示連接狀態:The webpage shows the connection status:

在 Websocket 連接之前網頁的初始狀態

選取 [連線] 將 WebSocket 要求傳送到顯示的 URL。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.

傳送和接收 Websocket 連接和測試訊息之後的網頁最終狀態