Sử dụng Hub trong ASP.NET Core SignalR
Tác giả: Rachel Appel và Kevin Griffin
SignalR Hubs API (giao diện lập trình ứng dụng Hub) cho phép các client đã kết nối gọi các phương thức trên server, tạo điều kiện cho giao tiếp thời gian thực. Server định nghĩa các phương thức được client gọi, và client định nghĩa các phương thức được server gọi. SignalR cũng cho phép giao tiếp gián tiếp giữa các client, trong đó SignalR Hub đóng vai trò trung gian, cho phép gửi tin nhắn giữa các client riêng lẻ, nhóm, hoặc đến tất cả client đã kết nối. SignalR đảm nhiệm mọi thứ cần thiết để thực hiện giao tiếp thời gian thực giữa client và server cũng như giữa server và client.
Bài viết này mô tả cách cấu hình hub, gửi tin nhắn đến client và cho phép server xử lý kết quả từ client.
Cấu hình SignalR hubs
Đăng ký các dịch vụ cần thiết cho SignalR hub bằng cách gọi phương thức AddSignalR trong file Program.cs:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR();
Cấu hình các endpoint (điểm cuối) SignalR bằng cách gọi phương thức MapHub trong file Program.cs:
app.MapRazorPages();
app.MapHub<ChatHub>("/Chat");
app.Run();Lưu ý: Các assembly (tập hợp) phía server của ASP.NET Core SignalR hiện đã được cài đặt cùng với .NET Core SDK.
Tạo và sử dụng Hub
Tạo một hub bằng cách khai báo một class kế thừa từ Hub. Thêm các phương thức public vào class để chúng có thể được gọi từ client:
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}Lưu ý: Hub có vòng đời transient (tạm thời):
- Không lưu trữ state (trạng thái) trong một thuộc tính của class hub. Mỗi lần gọi phương thức hub được thực thi trên một instance hub mới.
- Không khởi tạo hub trực tiếp thông qua dependency injection (tiêm phụ thuộc). Để gửi tin nhắn đến client từ nơi khác trong ứng dụng, hãy sử dụng IHubContext.
- Sử dụng await khi gọi các phương thức bất đồng bộ phụ thuộc vào hub còn hoạt động. Ví dụ, nếu gọi một phương thức như Clients.All.SendAsync(...) mà không sử dụng await, lời gọi có thể thất bại và phương thức hub hoàn thành trước khi SendAsync kết thúc.
Sử dụng các thuộc tính và phương thức của đối tượng 'Context'
Class Hub bao gồm thuộc tính Context chứa các thuộc tính sau về thông tin kết nối:
| Thuộc tính | Mô tả |
|---|---|
| ConnectionId | Lấy ID duy nhất cho kết nối, được SignalR gán. Mỗi kết nối có một connection ID. |
| UserIdentifier | Lấy định danh người dùng. Mặc định, SignalR sử dụng thuộc tính ClaimTypes.NameIdentifier từ ClaimsPrincipal được liên kết với kết nối làm định danh người dùng. |
| User | Lấy ClaimsPrincipal liên kết với người dùng hiện tại. |
| Items | Lấy bộ sưu tập key/value có thể được dùng để chia sẻ dữ liệu trong phạm vi kết nối này. Dữ liệu có thể được lưu trong bộ sưu tập này và tồn tại xuyên suốt các lần gọi phương thức hub khác nhau. |
| Features | Lấy bộ sưu tập các tính năng có sẵn trên kết nối. |
| ConnectionAborted | Lấy CancellationToken thông báo khi kết nối bị hủy bỏ. |
Thuộc tính Hub.Context cũng chứa các phương thức sau:
| Phương thức | Mô tả |
|---|---|
| GetHttpContext | Trả về HttpContext cho kết nối, hoặc null nếu kết nối không liên kết với một HTTP request. Đối với kết nối HTTP, dùng phương thức này để lấy thông tin như HTTP headers và query strings. |
| Abort | Hủy bỏ kết nối. |
Sử dụng các thuộc tính và phương thức của đối tượng 'Clients'
Class Hub bao gồm thuộc tính Clients chứa các thuộc tính sau cho giao tiếp giữa server và client:
| Thuộc tính | Mô tả |
|---|---|
| All | Gọi một phương thức trên tất cả client đã kết nối. |
| Caller | Gọi một phương thức trên client đã gọi phương thức hub. |
| Others | Gọi một phương thức trên tất cả client đã kết nối ngoại trừ client đã gọi phương thức. |
Thuộc tính Hub.Clients cũng chứa các phương thức sau:
| Phương thức | Mô tả |
|---|---|
| AllExcept | Gọi một phương thức trên tất cả client đã kết nối ngoại trừ các kết nối được chỉ định. |
| Client | Gọi một phương thức trên một client cụ thể đã kết nối. |
| Clients | Gọi một phương thức trên các client cụ thể đã kết nối. |
| Group | Gọi một phương thức trên tất cả kết nối trong nhóm được chỉ định. |
| GroupExcept | Gọi một phương thức trên tất cả kết nối trong nhóm được chỉ định, ngoại trừ các kết nối được chỉ định. |
| Groups | Gọi một phương thức trên nhiều nhóm kết nối. |
| OthersInGroup | Gọi một phương thức trên một nhóm kết nối, không bao gồm client đã gọi phương thức hub. |
| User | Gọi một phương thức trên tất cả kết nối liên kết với một người dùng cụ thể. |
| Users | Gọi một phương thức trên tất cả kết nối liên kết với các người dùng được chỉ định. |
Mỗi thuộc tính hoặc phương thức trả về một đối tượng có phương thức SendAsync. Phương thức SendAsync nhận tên của phương thức client cần gọi và bất kỳ tham số nào.
Đối tượng được trả về bởi các phương thức Client và Caller cũng chứa phương thức InvokeAsync, có thể được dùng để chờ kết quả từ client.
Gửi tin nhắn đến client
Để thực hiện cuộc gọi đến các client cụ thể, sử dụng các thuộc tính của đối tượng Clients. Trong ví dụ sau, có ba phương thức hub:
- Phương thức
SendMessagegửi tin nhắn đến tất cả client đã kết nối bằng thuộc tínhClients.All. - Phương thức
SendMessageToCallergửi tin nhắn trở lại cho người gọi bằng thuộc tínhClients.Caller. - Phương thức
SendMessageToGroupgửi tin nhắn đến tất cả client trong nhómSignalR Users.
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.SendAsync("ReceiveMessage", user, message);
public async Task SendMessageToGroup(string user, string message)
=> await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);Sử dụng strongly typed hubs (hub có kiểu mạnh)
Một nhược điểm của việc dùng phương thức SendAsync là nó dựa vào một chuỗi để chỉ định phương thức client cần gọi. Điều này khiến mã dễ gặp lỗi runtime (thời gian chạy) nếu tên phương thức bị sai chính tả hoặc thiếu ở phía client.
Một lựa chọn thay thế là sử dụng strongly typed (có kiểu mạnh) cho class Hub với Hub<T>. Trong ví dụ sau, phương thức client của ChatHub được tách ra thành một interface có tên IChatClient:
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}Interface này có thể được dùng để tái cấu trúc ví dụ ChatHub trước đó thành strongly typed:
public class StronglyTypedChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
=> await Clients.All.ReceiveMessage(user, message);
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.ReceiveMessage(user, message);
public async Task SendMessageToGroup(string user, string message)
=> await Clients.Group("SignalR Users").ReceiveMessage(user, message);
}Việc sử dụng Hub<IChatClient> cho phép kiểm tra tại thời điểm biên dịch (compile-time) các phương thức client. Cách này ngăn ngừa các vấn đề do sử dụng chuỗi vì Hub<T> chỉ có thể cung cấp quyền truy cập vào các phương thức được định nghĩa trong interface. Sử dụng strongly typed Hub<T> sẽ vô hiệu hóa khả năng sử dụng phương thức SendAsync.
Lưu ý: Hậu tố Async không bị loại bỏ khỏi tên phương thức. Trừ khi phương thức client được định nghĩa với .on('MyMethodAsync'), đừng sử dụng MyMethodAsync làm tên.
Yêu cầu kết quả từ client
Ngoài việc thực hiện cuộc gọi đến client, server cũng có thể yêu cầu kết quả từ client. Trong trường hợp này, server sử dụng phương thức ISingleClientProxy.InvokeAsync và client trả về kết quả từ handler .On của nó.
Có hai cách sử dụng API trên server:
Cách thứ nhất là gọi Client(...) hoặc Caller trên thuộc tính Clients trong phương thức Hub:
public class ChatHub : Hub
{
public async Task<string> WaitForMessage(string connectionId)
{
var message = await Clients.Client(connectionId).InvokeAsync<string>(
"GetMessage");
return message;
}
}Cách thứ hai là gọi Client(...) trên một instance của IHubContext<T>:
async Task SomeMethod(IHubContext<MyHub> context)
{
string result = await context.Clients.Client(connectionID).InvokeAsync<string>(
"GetMessage");
}Strongly typed hub cũng có thể trả về giá trị từ các phương thức interface:
public interface IClient
{
Task<string> GetMessage();
}
public class ChatHub : Hub<IClient>
{
public async Task<string> WaitForMessage(string connectionId)
{
string message = await Clients.Client(connectionId).GetMessage();
return message;
}
}Client trả về kết quả trong các handler .On(...) của chúng, như được minh họa trong các phần sau.
Client .NET
hubConnection.On("GetMessage", async () =>
{
Console.WriteLine("Enter message:");
var message = await Console.In.ReadLineAsync();
return message;
});Client TypeScript
hubConnection.on("GetMessage", async () => {
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("message");
}, 100);
});
return promise;
});Client Java
hubConnection.onWithResult("GetMessage", () -> {
return Single.just("message");
});Thay đổi tên của phương thức hub
Mặc định, tên phương thức hub phía server là tên của phương thức .NET. Để thay đổi hành vi mặc định này cho một phương thức cụ thể, sử dụng attribute HubMethodName. Client nên sử dụng tên này thay vì tên phương thức .NET khi gọi phương thức:
[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
=> await Clients.User(user).SendAsync("ReceiveMessage", user, message);Inject (tiêm) dịch vụ vào Hub
Constructor (hàm khởi tạo) của Hub có thể nhận các dịch vụ từ dependency injection (DI - tiêm phụ thuộc) như các tham số, có thể được lưu trong các thuộc tính của class để sử dụng trong phương thức hub.
Khi inject nhiều dịch vụ cho các phương thức hub khác nhau, các phương thức hub cũng có thể nhận dịch vụ từ DI. Mặc định, các tham số của phương thức hub được kiểm tra và giải quyết từ DI nếu có thể.
services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
// ...
public class ChatHub : Hub
{
public Task SendMessage(string user, string message, IDatabaseService dbService)
{
var userName = dbService.GetUserName(user);
return Clients.All.SendAsync("ReceiveMessage", userName, message);
}
}Nếu không muốn giải quyết tham số ngầm định từ dịch vụ, bạn có thể vô hiệu hóa hành vi này với tùy chọn DisableImplicitFromServicesParameters. Để chỉ định rõ ràng các tham số nào được giải quyết từ DI trong phương thức hub, sử dụng attribute [FromServices] hoặc attribute tùy chỉnh triển khai IFromServiceMetadata trên các tham số phương thức hub:
services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
services.AddSignalR(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
// ...
public class ChatHub : Hub
{
public Task SendMessage(string user, string message,
[FromServices] IDatabaseService dbService)
{
var userName = dbService.GetUserName(user);
return Clients.All.SendAsync("ReceiveMessage", userName, message);
}
}Hỗ trợ keyed services trong dependency injection
Cơ chế keyed services (dịch vụ có khóa) cho phép bạn đăng ký và truy xuất các dịch vụ DI bằng cách sử dụng khóa. Một dịch vụ được liên kết với một khóa bằng cách gọi phương thức AddKeyedSingleton để đăng ký nó.
Bạn truy cập dịch vụ đã đăng ký bằng cách chỉ định khóa với attribute [FromKeyedServices]. Đoạn mã sau minh họa cách sử dụng keyed services:
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
app.MapRazorPages();
app.MapHub<MyHub>("/myHub");
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
public class MyHub : Hub
{
public void SmallCacheMethod([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
public void BigCacheMethod([FromKeyedServices("big")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}Xử lý sự kiện cho kết nối
SignalR Hubs API cung cấp các phương thức virtual (ảo) OnConnectedAsync và OnDisconnectedAsync để quản lý và theo dõi kết nối. Override (ghi đè) phương thức virtual OnConnectedAsync để thực hiện các hành động khi client kết nối đến hub, ví dụ như thêm vào một nhóm:
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}Override phương thức virtual OnDisconnectedAsync để thực hiện các hành động khi client ngắt kết nối. Nếu client ngắt kết nối có chủ ý, chẳng hạn bằng cách gọi connection.stop(), tham số exception được đặt thành null. Tuy nhiên, nếu client ngắt kết nối do lỗi, chẳng hạn sự cố mạng, tham số exception chứa một ngoại lệ mô tả lỗi:
public override async Task OnDisconnectedAsync(Exception? exception)
{
await base.OnDisconnectedAsync(exception);
}Không cần gọi phương thức RemoveFromGroupAsync trong OnDisconnectedAsync vì nó được xử lý tự động.
Xử lý lỗi
Các ngoại lệ (exception) được ném ra trong phương thức hub được gửi đến client đã gọi phương thức đó. Trên client JavaScript, phương thức invoke trả về một Promise object. Client có thể đính kèm handler catch vào promise được trả về hoặc sử dụng try/catch với async/await để xử lý ngoại lệ:
try {
await connection.invoke("SendMessage", user, message);
} catch (err) {
console.error(err);
}Kết nối không bị đóng khi hub ném ra một ngoại lệ. Mặc định, SignalR trả về một thông báo lỗi chung cho client, như trong ví dụ sau:
Microsoft.AspNetCore.SignalR.HubException: An unexpected error occurred invoking 'SendMessage' on the server.
Các ngoại lệ bất ngờ thường chứa thông tin nhạy cảm, chẳng hạn như tên của database server trong một ngoại lệ được kích hoạt khi kết nối database thất bại. Vì lý do bảo mật, SignalR không tiết lộ các thông báo lỗi chi tiết này theo mặc định.
Nếu cần truyền một điều kiện ngoại lệ đến client, hãy sử dụng class HubException. Nếu một HubException được ném ra trong phương thức hub, SignalR gửi toàn bộ thông báo ngoại lệ đến client ở dạng không thay đổi:
public Task ThrowException()
=> throw new HubException("This error will be sent to the client!");Lưu ý: SignalR chỉ gửi thuộc tính Message của ngoại lệ đến client. Stack trace (dấu vết ngăn xếp) và các thuộc tính khác trên ngoại lệ không có sẵn cho client.