- SignalR là gì và khi nào cần sử dụng?
- Hub là gì và cách tạo Hub?
- Client-server communication hoạt động ra sao?
- Groups và Users trong SignalR khác nhau như thế nào?
- Scale SignalR với Redis backplane ra sao?
┌─────────────────────────────────────────────────────────────────┐
│ SIGNALR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │◀──▶│ Hub │◀──▶│ Client │ │
│ │ (Web) │ │ (Server)│ │ (Web) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ └────────▶│ Client │◀──────────┘ │
│ │ (Mobile)│ │
│ └──────────┘ │
│ │
│ - Real-time communication │
│ - Server push to clients │
│ - WebSocket, Server-Sent Events, Long Polling │
│ - Chat, notifications, live updates │
│ │
└─────────────────────────────────────────────────────────────────┘
dotnet add package Microsoft.AspNetCore.SignalR
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");
app.Run();
public class ChatHub : Hub
{
// Client gọi method này
public async Task SendMessage(string user, string message)
{
// Server gọi method trên tất cả clients
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// Gửi cho người gọi
public async Task SendMessageToCaller(string user, string message)
{
await Clients.Caller.SendAsync("ReceiveMessage", user, message);
}
// Gửi cho người khác (trừ người gọi)
public async Task SendMessageToOthers(string user, string message)
{
await Clients.Others.SendAsync("ReceiveMessage", user, message);
}
}
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
private readonly IChatService _chatService;
public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
{
_logger = logger;
_chatService = chatService;
}
public async Task SendMessage(string user, string message)
{
_logger.LogInformation("Message from {User}: {Message}", user, message);
// Save to database
await _chatService.SaveMessage(user, message);
// Broadcast
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
public class NotificationHub : Hub
{
// Gửi cho tất cả
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("OnBroadcast", message);
}
// Gửi cho client cụ thể (theo ConnectionId)
public async Task SendToClient(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("OnMessage", message);
}
// Gửi cho nhóm
public async Task SendToGroup(string group, string message)
{
await Clients.Group(group).SendAsync("OnMessage", message);
}
// Gửi cho user cụ thể (theo UserId)
public async Task SendToUser(string userId, string message)
{
await Clients.User(userId).SendAsync("OnMessage", message);
}
}
// JavaScript Client
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
// Receive message from server
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user}: ${message}`);
});
// Send message to server
connection.invoke("SendMessage", "John", "Hello!").catch(err => console.error(err));
// Start connection
connection.start().catch(err => console.error(err));
public class ChatHub : Hub
{
// Join group
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync(
"OnUserJoined",
$"{Context.ConnectionId} joined {groupName}");
}
// Leave group
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync(
"OnUserLeft",
$"{Context.ConnectionId} left {groupName}");
}
// Send to group
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("OnMessage", message);
}
}
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "General");
await Clients.Caller.SendAsync("OnConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "General");
await Clients.Others.SendAsync(
"OnDisconnected",
Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
// Define client methods interface
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string user);
Task UserLeft(string user);
}
// Strongly-typed Hub
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// Type-safe client calls
await Clients.All.ReceiveMessage(user, message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).UserJoined(Context.ConnectionId);
}
}
// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
options.Transports = HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents;
});
// Hub với authorization
[Authorize]
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier; // From JWT claim
var userName = Context.User?.Identity?.Name;
await base.OnConnectedAsync();
}
}
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379");
dotnet add package Microsoft.Azure.SignalR
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString =
builder.Configuration["Azure:SignalR:ConnectionString"];
});
// Map hub
app.MapHub<ChatHub>("/chat");
// ✅ Good - handle connection events
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
// ✅ Good - use groups for targeted messages
public async Task SendToRoom(string roomId, string message)
{
await Clients.Group($"room-{roomId}").SendAsync("OnMessage", message);
}
// ❌ Bad - send to all when only room needs it
public async Task SendToRoom(string roomId, string message)
{
await Clients.All.SendAsync("OnMessage", message); // All clients receive!
}
// JavaScript Client - handle reconnection
connection.onclose(async () => {
await start();
});
async function start() {
try {
await connection.start();
console.log("SignalR connected");
} catch (err) {
console.log("SignalR connection failed");
setTimeout(() => start(), 5000);
}
}