Nguon: Microsoft Learn · .NET 8.0

Hỗ trợ WebSockets trong ASP.NET Core

Nguồn: WebSockets support in ASP.NET Core

Bài viết này giải thích cách bắt đầu với WebSockets trong ASP.NET Core. WebSocket (RFC 6455) là một protocol (giao thức) cho phép các kênh giao tiếp hai chiều liên tục qua kết nối TCP. Nó được sử dụng trong các ứng dụng hưởng lợi từ giao tiếp nhanh, thời gian thực như chat, dashboard và game.

Xem hoặc tải xuống mã mẫu

Hỗ trợ WebSockets qua HTTP/2

Sử dụng WebSockets qua HTTP/2 tận dụng các tính năng mới như:

Các tính năng được hỗ trợ này có sẵn trong Kestrel trên tất cả các nền tảng HTTP/2 được bật. Việc thương lượng phiên bản là tự động trong các trình duyệt và Kestrel, vì vậy không cần API mới nào.

.NET 7 đã giới thiệu hỗ trợ WebSockets qua HTTP/2 cho Kestrel, SignalR JavaScript client, và SignalR với Blazor WebAssembly.

Lưu ý: WebSockets qua HTTP/2 sử dụng CONNECT requests thay vì GET, vì vậy routes và controllers của bạn có thể cần cập nhật. Để biết thêm thông tin, xem phần Thêm hỗ trợ HTTP/2 WebSockets cho các controller hiện có trong bài viết này.

Chrome, Edge và Firefox (phiên bản 128 trở lên) đã bật HTTP/2 WebSockets theo mặc định. Bạn có thể kiểm tra hoặc thay đổi cài đặt này trong Firefox bằng cách mở about:config và tìm tùy chọn network.http.http2.websockets.

WebSockets ban đầu được thiết kế cho HTTP/1.1 nhưng từ đó đã được điều chỉnh để hoạt động qua HTTP/2. (RFC 8441)

SignalR

ASP.NET Core SignalR là thư viện đơn giản hóa việc thêm chức năng web thời gian thực vào ứng dụng. Nó sử dụng WebSockets bất cứ khi nào có thể.

Đối với hầu hết các ứng dụng, chúng tôi khuyến nghị dùng SignalR thay vì raw WebSockets. SignalR:

WebSockets qua HTTP/2 được hỗ trợ cho:

Đối với một số ứng dụng, gRPC trên .NET cung cấp một giải pháp thay thế cho WebSockets.

Điều kiện tiên quyết

Cấu hình middleware (phần mềm trung gian)

Thêm WebSockets middleware trong Program.cs:

csharp
app.UseWebSockets();

Các cài đặt sau có thể được cấu hình:

csharp
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Chấp nhận WebSocket requests

Ở một nơi nào đó sau trong vòng đời request (sau trong Program.cs hoặc trong action method, ví dụ) hãy kiểm tra xem đó có phải là WebSocket request không và chấp nhận WebSocket request đó.

Ví dụ sau là từ phần sau của Program.cs:

csharp
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);
    }

});

Một WebSocket request có thể đến trên bất kỳ URL nào, nhưng mã mẫu này chỉ chấp nhận các request cho /ws.

Cách tiếp cận tương tự có thể được thực hiện trong controller method:

csharp
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;
        }
    }

Khi sử dụng WebSocket, bạn phải giữ middleware pipeline chạy trong suốt thời gian kết nối. Nếu bạn cố gắng gửi hoặc nhận WebSocket message sau khi middleware pipeline kết thúc, bạn có thể gặp ngoại lệ như sau:

code
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'.

Nếu bạn đang sử dụng background service để ghi dữ liệu vào WebSocket, hãy đảm bảo bạn giữ middleware pipeline chạy. Thực hiện điều này bằng cách sử dụng TaskCompletionSource<TResult>. Truyền TaskCompletionSource vào background service và để nó gọi TrySetResult khi bạn kết thúc với WebSocket. Sau đó await thuộc tính Task trong suốt request, như ví dụ sau:

csharp
app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

Ngoại lệ WebSocket closed cũng có thể xảy ra khi trả về quá sớm từ action method. Khi chấp nhận socket trong action method, hãy chờ đoạn code sử dụng socket hoàn thành trước khi trả về từ action method.

Không bao giờ sử dụng Task.Wait, Task.Result hoặc các blocking call tương tự để chờ socket hoàn thành, vì điều đó có thể gây ra các vấn đề threading nghiêm trọng. Luôn sử dụng await.

Thêm hỗ trợ HTTP/2 WebSockets cho các controller hiện có

.NET 7 đã giới thiệu hỗ trợ WebSockets qua HTTP/2 cho Kestrel, SignalR JavaScript client và SignalR với Blazor WebAssembly. HTTP/2 WebSockets sử dụng CONNECT requests thay vì GET. Nếu trước đây bạn sử dụng [HttpGet("/path")] trên controller action method cho Websocket requests, hãy cập nhật để sử dụng [Route("/path")] thay thế.

csharp
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;
        }
    }

Nén (Compression)

Cảnh báo: Bật nén qua các kết nối được mã hóa có thể khiến ứng dụng dễ bị tấn công CRIME/BREACH. Nếu gửi thông tin nhạy cảm, hãy tránh bật nén hoặc sử dụng WebSocketMessageFlags.DisableCompression khi gọi WebSocket.SendAsync. Điều này áp dụng cho cả hai phía của WebSocket. Lưu ý rằng WebSockets API trong trình duyệt không có cấu hình để tắt nén theo từng lần gửi.

Nếu muốn nén các message qua WebSockets, mã chấp nhận phải chỉ định rằng nó cho phép nén như sau:

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

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.DisableServerContextTakeover là các tùy chọn nâng cao kiểm soát cách thức hoạt động của quá trình nén.

Quá trình nén được thương lượng giữa client và server khi lần đầu thiết lập kết nối. Bạn có thể đọc thêm về thương lượng trong Compression Extensions for WebSocket RFC.

Lưu ý: Nếu việc thương lượng nén không được chấp nhận bởi server hoặc client, kết nối vẫn được thiết lập. Tuy nhiên, kết nối không sử dụng nén khi gửi và nhận các message.

Gửi và nhận message

Phương thức AcceptWebSocketAsync nâng cấp kết nối TCP thành kết nối WebSocket và cung cấp đối tượng WebSocket. Sử dụng đối tượng WebSocket để gửi và nhận message.

Mã hiển thị trước đó chấp nhận WebSocket request truyền đối tượng WebSocket đến phương thức Echo. Mã nhận một message và ngay lập tức gửi lại cùng một message. Message được gửi và nhận trong một vòng lặp cho đến khi client đóng kết nối:

csharp
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);
}

Khi chấp nhận kết nối WebSocket trước khi bắt đầu vòng lặp, middleware pipeline kết thúc. Khi đóng socket, pipeline unwinds (hủy cuộn). Nghĩa là, request dừng tiến về phía trước trong pipeline khi WebSocket được chấp nhận. Khi vòng lặp hoàn thành và socket được đóng, request tiếp tục đi ngược lại pipeline.

Xử lý client ngắt kết nối

Server không được thông báo tự động khi client ngắt kết nối do mất kết nối. Server chỉ nhận được thông điệp ngắt kết nối nếu client gửi nó, điều này không thể thực hiện nếu kết nối internet bị mất. Nếu bạn muốn thực hiện một hành động nào đó khi điều đó xảy ra, hãy đặt timeout sau khi không nhận được gì từ client trong một khoảng thời gian nhất định.

Nếu client không luôn gửi message và bạn không muốn timeout chỉ vì kết nối trở nên nhàn rỗi, hãy để client sử dụng timer để gửi ping message mỗi X giây. Trên server, nếu message chưa đến trong 2\*X giây sau message trước đó, hãy chấm dứt kết nối và báo cáo rằng client đã ngắt kết nối. Chờ gấp đôi khoảng thời gian dự kiến để dành thêm thời gian cho độ trễ mạng có thể giữ ping message.

Giới hạn origin WebSocket

Các biện pháp bảo vệ do CORS cung cấp không áp dụng cho WebSockets. Các trình duyệt không:

Tuy nhiên, trình duyệt gửi header Origin khi phát hành WebSocket requests. Các ứng dụng nên được cấu hình để xác thực các header này để đảm bảo chỉ các WebSocket đến từ các origin dự kiến mới được phép.

Nếu bạn đang host server của mình trên "https://server.com" và host client trên "https://client.com", hãy thêm "https://client.com" vào danh sách AllowedOrigins cho WebSockets để xác minh.

csharp
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Lưu ý: Header Origin được kiểm soát bởi client và, giống như header Referer, có thể bị làm giả. Không sử dụng các header này như một cơ chế xác thực.

Hỗ trợ IIS/IIS Express

Windows Server 2012 trở lên và Windows 8 trở lên với IIS/IIS Express 8 trở lên có hỗ trợ WebSocket protocol, nhưng không có WebSockets qua HTTP/2.

Lưu ý: WebSockets luôn được bật khi sử dụng IIS Express.

Bật WebSockets trên IIS

Để bật hỗ trợ WebSocket protocol trên Windows Server 2012 trở lên:

Lưu ý: Các bước này không bắt buộc khi sử dụng IIS Express

  1. Sử dụng wizard Add Roles and Features từ menu Manage hoặc liên kết trong Server Manager.
  2. Chọn Role-based or Feature-based Installation. Chọn Next.
  3. Chọn server phù hợp (server local được chọn theo mặc định). Chọn Next.
  4. Mở rộng Web Server (IIS) trong cây Roles, mở rộng Web Server, và sau đó mở rộng Application Development.
  5. Chọn WebSocket Protocol. Chọn Next.
  6. Nếu không cần thêm tính năng nào, chọn Next.
  7. Chọn Install.
  8. Khi quá trình cài đặt hoàn tất, chọn Close để thoát wizard.

Để bật hỗ trợ WebSocket protocol trên Windows 8 trở lên:

Lưu ý: Các bước này không bắt buộc khi sử dụng IIS Express

  1. Điều hướng đến Control Panel > Programs > Programs and Features > Turn Windows features on or off (phía bên trái màn hình).
  2. Mở các nút sau: Internet Information Services > World Wide Web Services > Application Development Features.
  3. Chọn tính năng WebSocket Protocol. Chọn OK.

Tắt WebSocket khi sử dụng socket.io trên Node.js

Nếu sử dụng hỗ trợ WebSocket trong socket.io trên Node.js, hãy tắt module IIS WebSocket mặc định bằng cách sử dụng phần tử webSocket trong web.config hoặc applicationHost.config. Nếu bước này không được thực hiện, module IIS WebSocket cố gắng xử lý giao tiếp WebSocket thay vì Node.js và ứng dụng.

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

Ứng dụng mẫu

Ứng dụng mẫu đi kèm với bài viết này là một ứng dụng echo. Nó có một trang web thực hiện kết nối WebSocket, và server gửi lại bất kỳ message nào nó nhận được về cho client. Ứng dụng mẫu hỗ trợ WebSockets qua HTTP/2 khi sử dụng framework .NET 7 trở lên.

Chạy ứng dụng:

Trang web hiển thị trạng thái kết nối. Chọn Connect để gửi WebSocket request đến URL được hiển thị. Nhập test message và chọn Send. Khi hoàn tất, chọn Close Socket. Phần Communication Log báo cáo mỗi hành động mở, gửi và đóng khi nó xảy ra.