ASP.NET Core 中的 WebSockets 支援

作者:Tom DykstraAndrew Stanton-Nurse

本文說明如何在 ASP.NET Core 中開始使用 WebSocket。 WebSocket (RFC 6455) 為通訊協定,其可在 TCP 連線下啟用雙向的持續性通訊通道。 它用於受益於快速且即時通訊的應用程式,例如聊天、儀表板和遊戲應用程式。

檢視或下載範例程式碼 (如何下載)。 如何執行

SignalR

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

針對大部分的應用程式,我們建議您不要透過 SignalR 原始 websocket。 SignalR 針對無法使用 Websocket 的環境提供傳輸回復。 它也會提供基本的遠端程序呼叫應用程式模型。 在大部分的情況下, SignalR 相較于使用原始 websocket,沒有顯著的效能缺點。

針對某些應用程式, gRPC on .net 提供 websocket 的替代方案。

先決條件

  • 支援 ASP.NET Core 的任何作業系統:
    • Windows 7/Windows Server 2008 或更新版本
    • Linux
    • macOS
  • 如果應用程式在 Windows 上與 IIS 搭配執行:
    • Windows 8 / Windows Server 2012 或更新版本
    • IIS 8 / IIS 8 Express
    • 必須啟用 Websocket。 請參閱IIS/IIS Express 支援一節。
  • 如果應用程式在 HTTP.sys 上執行:
    • Windows 8 / Windows Server 2012 或更新版本
  • 如需支援的瀏覽器,請請參閱 https://caniuse.com/#feat=websockets。

設定中介軟體

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

app.UseWebSockets();

注意

如果您想要接受控制器中的 WebSocket 要求, app.UseWebSockets 必須先進行的呼叫 app.UseEndpoints

您可以設定下列設定:

  • KeepAliveInterval - 要將 "ping" 框架傳送到用戶端,以確保 Proxy 保持連線開啟的頻率。 預設為兩分鐘。

您可以設定下列設定:

  • 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 WebSocket webSocket = await 
                               HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(HttpContext, webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
    }

使用 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

壓縮

警告

啟用加密連線的壓縮可能會使應用程式遭受犯罪/缺口的攻擊。 如果傳送機密資訊,請避免在呼叫時啟用壓縮或使用 WebSocketMessageFlags.DisableCompression WebSocket.SendAsync 。 這適用于 WebSocket 的雙方。 請注意,瀏覽器中的 Websocket API 沒有針對每個傳送停用壓縮的設定。

如果需要透過 Websocket 壓縮訊息,則接受程式碼必須指定允許壓縮,如下所示:

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

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.DisableServerContextTakeover 是控制壓縮運作方式的 advanced 選項。

第一次建立連線時,會在用戶端與伺服器之間協商壓縮。 您可以深入瞭解 WEBSOCKET RFC 的壓縮延伸模組中的協商。

注意

如果伺服器或用戶端未接受壓縮協商,仍會建立連接。 不過,連接在傳送和接收訊息時不會使用壓縮。

傳送及接收訊息

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 訊息。

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 > World Wide Web 服務 > 應用程式開發功能
  3. 選取 [WebSocket 通訊協定] 功能。 選取 [確定]。

在 Node.js 上使用 socket.io 時停用 WebSocket

如果在 Node.js上使用 Socket.io中的 WebSocket 支援,請使用 webSocket web.configapplicationHost.config 中的元素,停用預設的 IIS WebSocket 模組。如果未執行此步驟,IIS WebSocket 模組會嘗試處理 WebSocket 通訊,而不是 Node.js 和應用程式。

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

範例應用程式

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

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

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

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