Hỗ trợ WebSockets trong 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.
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ư:
- Nén header.
- Multiplexing (ghép kênh), giúp giảm thời gian và tài nguyên cần thiết khi thực hiện nhiều request đến server.
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:
- Cung cấp transport fallback (dự phòng transport) cho các môi trường mà WebSockets không khả dụng.
- Cung cấp mô hình ứng dụng remote procedure call cơ bản.
- Không có nhược điểm hiệu suất đáng kể so với việc sử dụng raw WebSockets trong hầu hết các tình huống.
WebSockets qua HTTP/2 được hỗ trợ cho:
- ASP.NET Core SignalR JavaScript client
- ASP.NET Core SignalR với Blazor WebAssembly
Đố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
- Bất kỳ hệ điều hành nào hỗ trợ ASP.NET Core:
- Windows 7 / Windows Server 2008 trở lên
- Linux
- macOS
- Nếu ứng dụng chạy trên Windows với IIS:
- Windows 8 / Windows Server 2012 trở lên
- IIS 8 / IIS 8 Express
- WebSockets phải được bật. Xem phần hỗ trợ IIS/IIS Express.
- Nếu ứng dụng chạy trên HTTP.sys:
- Windows 8 / Windows Server 2012 trở lên
- Đối với các trình duyệt được hỗ trợ, xem Can I use.
Cấu hình middleware (phần mềm trung gian)
Thêm WebSockets middleware trong Program.cs:
app.UseWebSockets();
Các cài đặt sau có thể được cấu hình:
- KeepAliveInterval - Tần suất gửi các "ping" frame đến client để đảm bảo proxy giữ kết nối mở. Mặc định là hai phút.
- AllowedOrigins - Danh sách các giá trị Origin header được phép cho WebSocket requests. Theo mặc định, tất cả origins đều được phép. Để biết thêm thông tin, xem phần Giới hạn origin WebSocket trong bài viết này.
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:
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:
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:
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:
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ế.
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:
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
new WebSocketAcceptContext { DangerousEnableCompression = true }))
{
}WebSocketAcceptContext.ServerMaxWindowBits và WebSocketAcceptContext.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:
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:
- Thực hiện CORS pre-flight requests.
- Tôn trọng các giới hạn được chỉ định trong
Access-Controlheaders khi thực hiện WebSocket requests.
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.
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
- Sử dụng wizard Add Roles and Features từ menu Manage hoặc liên kết trong Server Manager.
- Chọn Role-based or Feature-based Installation. Chọn Next.
- Chọn server phù hợp (server local được chọn theo mặc định). Chọn Next.
- Mở rộng Web Server (IIS) trong cây Roles, mở rộng Web Server, và sau đó mở rộng Application Development.
- Chọn WebSocket Protocol. Chọn Next.
- Nếu không cần thêm tính năng nào, chọn Next.
- Chọn Install.
- 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
- Đ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).
- Mở các nút sau: Internet Information Services > World Wide Web Services > Application Development Features.
- 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.
<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:
- Để chạy ứng dụng trong Visual Studio: Mở dự án mẫu trong Visual Studio và nhấn Ctrl+F5 để chạy mà không có debugger.
- Để chạy ứng dụng trong command shell: Chạy lệnh
dotnet runvà điều hướng trong trình duyệt đếnhttp://localhost:<port>.
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.