ASP.NET Core 中的 WebSockets 支援

本文說明如何在 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 WebSockets,而且您可以使用 network.http.spdy.websockets 旗標以在 FireFox 的 about:config 頁面上將其啟用。

WebSockets 原本是針對 HTTP/1.1 所設計,但此後已改編為透過 HTTP/2 運作。 (RFC 8441)

SignalR

ASP.NET Core SignalR 是程式庫,可簡化將即時 Web 功能新增至應用程式的程序。 它會盡可能使用 WebSockets。

針對大部分的應用程式,建議使用 SignalR,而不是原始 WebSockets。 SignalR:

  • 針對未提供 WebSockets 的環境,提供傳輸後援。
  • 提供基本遠端程序呼叫應用程式模型。
  • 在大部分的情況下,相較於使用原始 WebSockets,沒有顯著的效能缺點。

支援透過 HTTP/2 的 WebSockets 用於:

  • ASP.NET Core SignalR JavaScript 用戶端
  • 具有 Blazor WebAssembly 的 ASP.NET Core SignalR

針對某些應用程式,.NET 上的 gRPC 提供 WebSockets 的替代方案。

必要條件

  • 支援 ASP.NET Core 的任何作業系統:
    • Windows 7/Windows Server 2008 或更新版本
    • Linux
    • macOS
  • 如果應用程式在 Windows 上與 IIS 搭配執行:
    • Windows 8 / Windows Server 2012 或更新版本
    • IIS 8 / IIS 8 Express
    • 必須啟用 WebSockets。 請參閱 IIS/IIS Express 支援小節。
  • 如果應用程式在 HTTP.sys 上執行:
    • Windows 8 / Windows Server 2012 或更新版本
  • 如需所支援的瀏覽器,請參閱我是否可以使用

設定中介軟體

Program.cs 中,新增 WebSockets 中介軟體:

app.UseWebSockets();

您可以設定下列設定:

  • KeepAliveInterval - 要將 "ping" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。 預設為兩分鐘。
  • 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。 然後,在要求期間,awaitTask 屬性,如下列範例所示:

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 後,要求就會在管線中停止向前移動。 當迴圈完成且通訊端關閉時,要求會繼續備份管線。

處理用戶端中斷連線問題

用戶端因連線中斷而中斷連線時,不會自動通知伺服器。 只有當用戶端傳送通知時,伺服器才會收到連線中斷訊息,而在網際網路連線中斷的情況下無法傳送通知。 若想要在發生此情況時採取一些動作,請設定逾時,在指定的時間內未從用戶端收到通知之後觸發逾時。

如果用戶端並非總是會傳送訊息,而且您不想讓系統只是因為連線閒置就觸發逾時,則請讓用戶端使用計時器每隔 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 清單,以讓 WebSockets 進行驗證。

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 支援

搭配 IIS/IIS Express 8 或更新版本的 Windows Server 2012 或更新版本和 Windows 8 或更新版本支援 WebSocket 通訊協定,但不支援透過 HTTP/2 的 WebSockets。

注意

使用 IIS Express 時一律會啟用 WebSockets。

在 IIS 上啟用 WebSockets

若要在 Windows Server 2012 或更新版本中啟用 WebSocket 通訊協定的支援:

注意

使用 IIS Express 時,不需要這些步驟

  1. 使用來自 [管理] 功能表的 [新增角色及功能] 精靈,或是 [伺服器管理員] 中的連結。
  2. 選取 [角色型或功能型安裝]。 選取 [下一步] 。
  3. 選取適當的伺服器 (預設會選取本機伺服器)。 選取 [下一步] 。
  4. 展開 [角色] 樹狀目錄中的 [網頁伺服器 (IIS)],展開 [網頁伺服器],然後展開 [應用程式開發]
  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.configapplicationHost.config 中使用 webSocket 項目,以停用預設的 IIS WebSocket 模組。如果未執行此步驟,IIS WebSocket 模組會嘗試處理 WebSocket 通訊,而非 Node.js 和應用程式。

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

範例應用程式

本文附帶的範例應用程式是回應應用程式。 其具有一個進行 WebSocket 連線的網頁,而伺服器會將其所接收的任何訊息都重新傳送回用戶端。 使用 .NET 7 或更新版本的目標架構時,此範例應用程式支援透過 HTTP/2 的 WebSockets。

執行應用程式:

  • 若要在 Visual Studio 中執行應用程式:在 Visual Studio 中開啟範例專案,然後按 Ctrl+F5 來執行,而不需要偵錯工具。
  • 若要在命令殼層中執行應用程式:執行命令 dotnet run,並在瀏覽器中導覽至 http://localhost:<port>

網頁會顯示連線狀態:

Initial state of webpage before WebSockets connection

選取 [連線] 將 WebSocket 要求傳送到顯示的 URL。 輸入測試訊息,然後選取 [傳送]。 完成後,請選取 [關閉通訊端]。 [通訊記錄檔] 區段會報告每次進行的開啟、傳送和關閉動作。

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 功能新增至應用程式的程序。 它會盡可能使用 WebSockets。

針對大部分的應用程式,建議使用透過原始 WebSockets 的 SignalR。 SignalR 針對未提供 WebSockets 的環境,提供傳輸後援。 其也提供基本的遠端程序呼叫應用程式模型。 而且,在大部分的情況下,相較於使用原始 WebSockets,SignalR 沒有顯著的效能缺點。

針對某些應用程式,.NET 上的 gRPC 提供 WebSockets 的替代方案。

必要條件

  • 支援 ASP.NET Core 的任何作業系統:
    • Windows 7/Windows Server 2008 或更新版本
    • Linux
    • macOS
  • 如果應用程式在 Windows 上與 IIS 搭配執行:
    • Windows 8 / Windows Server 2012 或更新版本
    • IIS 8 / IIS 8 Express
    • 必須啟用 WebSockets。 請參閱 IIS/IIS Express 支援小節。
  • 如果應用程式在 HTTP.sys 上執行:
    • Windows 8 / Windows Server 2012 或更新版本
  • 如需所支援的瀏覽器,請參閱我是否可以使用

設定中介軟體

Program.cs 中,新增 WebSockets 中介軟體:

app.UseWebSockets();

您可以設定下列設定:

  • KeepAliveInterval - 要將 "ping" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。 預設為兩分鐘。
  • 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。 然後,在要求期間,awaitTask 屬性,如下列範例所示:

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 後,要求就會在管線中停止向前移動。 當迴圈完成且通訊端關閉時,要求會繼續備份管線。

處理用戶端中斷連線問題

用戶端因連線中斷而中斷連線時,不會自動通知伺服器。 只有當用戶端傳送通知時,伺服器才會收到連線中斷訊息,而在網際網路連線中斷的情況下無法傳送通知。 若想要在發生此情況時採取一些動作,請設定逾時,在指定的時間內未從用戶端收到通知之後觸發逾時。

如果用戶端並非總是會傳送訊息,而且您不想讓系統只是因為連線閒置就觸發逾時,則請讓用戶端使用計時器每隔 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 清單,以讓 WebSockets 進行驗證。

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 支援

搭配 IIS/IIS Express 8 或更新版本的 Windows Server 2012 或更新版本和 Windows 8 或更新版本具有 WebSocket 通訊協定的支援。

注意

使用 IIS Express 時一律會啟用 WebSockets。

在 IIS 上啟用 WebSockets

若要在 Windows Server 2012 或更新版本中啟用 WebSocket 通訊協定的支援:

注意

使用 IIS Express 時,不需要這些步驟

  1. 使用來自 [管理] 功能表的 [新增角色及功能] 精靈,或是 [伺服器管理員] 中的連結。
  2. 選取 [角色型或功能型安裝]。 選取 [下一步] 。
  3. 選取適當的伺服器 (預設會選取本機伺服器)。 選取 [下一步] 。
  4. 展開 [角色] 樹狀目錄中的 [網頁伺服器 (IIS)],展開 [網頁伺服器],然後展開 [應用程式開發]
  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.configapplicationHost.config 中使用 webSocket 項目,以停用預設的 IIS WebSocket 模組。如果未執行此步驟,IIS WebSocket 模組會嘗試處理 WebSocket 通訊,而非 Node.js 和應用程式。

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

範例應用程式

本文附帶的範例應用程式是回應應用程式。 其具有一個進行 WebSocket 連線的網頁,而伺服器會將其所接收的任何訊息都重新傳送回用戶端。 範例應用程式未設定為使用 IIS Express 以從 Visual Studio 執行,因此會使用 dotnet run 以在命令殼層中執行應用程式,並在瀏覽器中導覽至 http://localhost:<port>。 網頁會顯示連線狀態:

Initial state of webpage before WebSockets connection

選取 [連線] 將 WebSocket 要求傳送到顯示的 URL。 輸入測試訊息,然後選取 [傳送]。 完成後,請選取 [關閉通訊端]。 [通訊記錄檔] 區段會報告每次進行的開啟、傳送和關閉動作。

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 功能新增至應用程式的程序。 它會盡可能使用 WebSockets。

針對大部分的應用程式,建議使用透過原始 WebSockets 的 SignalR。 SignalR 針對未提供 WebSockets 的環境,提供傳輸後援。 其也提供基本的遠端程序呼叫應用程式模型。 而且,在大部分的情況下,相較於使用原始 WebSockets,SignalR 沒有顯著的效能缺點。

針對某些應用程式,.NET 上的 gRPC 提供 WebSockets 的替代方案。

必要條件

  • 支援 ASP.NET Core 的任何作業系統:
    • Windows 7/Windows Server 2008 或更新版本
    • Linux
    • macOS
  • 如果應用程式在 Windows 上與 IIS 搭配執行:
    • Windows 8 / Windows Server 2012 或更新版本
    • IIS 8 / IIS 8 Express
    • 必須啟用 WebSockets。 請參閱 IIS/IIS Express 支援小節。
  • 如果應用程式在 HTTP.sys 上執行:
    • Windows 8 / Windows Server 2012 或更新版本
  • 如需所支援的瀏覽器,請參閱我是否可以使用

設定中介軟體

Startup 類別的 Configure 方法中新增 WebSocket 中介軟體:

app.UseWebSockets();

注意

如果您想要接受控制器中的 WebSocket 要求,則必須在 app.UseEndpoints 之前呼叫 app.UseWebSockets

您可以設定下列設定:

  • KeepAliveInterval - 要將 "ping" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。 預設為兩分鐘。
  • AllowedOrigins - WebSocket 要求之允許 Origin 標頭值的清單。 根據預設,會允許所有來源。 如需詳細資訊,請參閱下方的 "WebSocket origin restriction" (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。 然後,在要求期間,awaitTask 屬性,如下列範例所示:

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 後,要求就會在管線中停止向前移動。 當迴圈完成且通訊端關閉時,要求會繼續備份管線。

處理用戶端中斷連線問題

用戶端因連線中斷而中斷連線時,不會自動通知伺服器。 只有當用戶端傳送通知時,伺服器才會收到連線中斷訊息,而在網際網路連線中斷的情況下無法傳送通知。 若想要在發生此情況時採取一些動作,請設定逾時,在指定的時間內未從用戶端收到通知之後觸發逾時。

如果用戶端並非總是會傳送訊息,而且您不想讓系統只是因為連線閒置就觸發逾時,則請讓用戶端使用計時器每隔 X 秒傳送一次 Ping 訊息。 在伺服器上,如果訊息未在收到前一個訊息後的 2*X 秒內抵達,則終止連線,並回報用戶端已中斷連線。 等候預期時間間隔兩倍的時間,為網路延遲保留足夠的額外時間,看看能不能收到耽誤的 Ping 訊息。

注意

如果選項大於零,內部 ManagedWebSocket 會隱含處理 Ping/Pong 畫面格,以保持連線運作 KeepAliveInterval ,預設為 30 秒(TimeSpan.FromSeconds(30))。

WebSocket 來源限制

CORS 所提供的保護不套用至 WebSocket。 瀏覽器會:

  • 執行 CORS 的事前要求。
  • 進行 WebSocket 要求時,採用 Access-Control 標頭中所指定的限制。

不過,瀏覽器會在發出 WebSocket 要求時,傳送 Origin 標頭。 應設定應用程式驗證這些標頭,以確保只允許來自預期來源的 WebSocket。

如果您在 "https://server.com"" 上裝載伺服器,並在 "https://client.com"" 上裝載用戶端,則請將 "https://client.com"" 新增至 AllowedOrigins 清單,以讓 WebSockets 進行驗證。

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 支援

搭配 IIS/IIS Express 8 或更新版本的 Windows Server 2012 或更新版本和 Windows 8 或更新版本具有 WebSocket 通訊協定的支援。

注意

使用 IIS Express 時一律會啟用 WebSockets。

在 IIS 上啟用 WebSockets

若要在 Windows Server 2012 或更新版本中啟用 WebSocket 通訊協定的支援:

注意

使用 IIS Express 時,不需要這些步驟

  1. 使用來自 [管理] 功能表的 [新增角色及功能] 精靈,或是 [伺服器管理員] 中的連結。
  2. 選取 [角色型或功能型安裝]。 選取 [下一步] 。
  3. 選取適當的伺服器 (預設會選取本機伺服器)。 選取 [下一步] 。
  4. 展開 [角色] 樹狀目錄中的 [網頁伺服器 (IIS)],展開 [網頁伺服器],然後展開 [應用程式開發]
  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.configapplicationHost.config 中使用 webSocket 項目,以停用預設的 IIS WebSocket 模組。如果未執行此步驟,IIS WebSocket 模組會嘗試處理 WebSocket 通訊,而非 Node.js 和應用程式。

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

範例應用程式

本文附帶的範例應用程式是回應應用程式。 其具有一個進行 WebSocket 連線的網頁,而伺服器會將其所接收的任何訊息都重新傳送回用戶端。 範例應用程式未設定為使用 IIS Express 以從 Visual Studio 執行,因此會使用 dotnet run 以在命令殼層中執行應用程式,並在瀏覽器中導覽至 http://localhost:5000。 網頁會顯示連線狀態:

Initial state of webpage before WebSockets connection

選取 [連線] 將 WebSocket 要求傳送到顯示的 URL。 輸入測試訊息,然後選取 [傳送]。 完成後,請選取 [關閉通訊端]。 [通訊記錄檔] 區段會報告每次進行的開啟、傳送和關閉動作。

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