Xây dựng Ứng dụng Web Thời gian thực với SignalR Core trong Lộ trình .NET

Chào mừng các bạn quay trở lại với chuỗi bài viết “Lộ trình .NET”. Sau khi đã cùng nhau khám phá nhiều khía cạnh quan trọng từ Tổng quan Lộ trình ASP.NET Core, nền tảng C#, Hệ sinh thái .NET, hiểu về HTTP, làm việc với Cơ sở dữ liệu quan hệ lẫn NoSQL, sử dụng Entity Framework Core, quản lý Cache, hay xây dựng RESTful APIgRPC, hôm nay chúng ta sẽ bước sang một lĩnh vực mới đầy thú vị: Ứng dụng Web thời gian thực (Real-Time Web Apps).

Bạn đã bao giờ dùng một ứng dụng chat web, một bảng tin chứng khoán trực tuyến, hay một dashboard quản lý hiển thị dữ liệu thay đổi liên tục mà không cần nhấn nút refresh? Đó chính là sức mạnh của các ứng dụng thời gian thực. Chúng mang lại trải nghiệm người dùng mượt mà, phản hồi tức thì và cảm giác “sống động” cho ứng dụng web truyền thống.

Vậy làm thế nào để xây dựng được những ứng dụng như vậy trong thế giới .NET? Câu trả lời chính là ASP.NET Core SignalR.

SignalR Core là gì?

SignalR Core là một thư viện mã nguồn mở của Microsoft, cho phép thêm chức năng thời gian thực vào các ứng dụng web một cách dễ dàng. “Thời gian thực” ở đây có nghĩa là server có thể đẩy (push) nội dung xuống client ngay lập tức khi có dữ liệu mới, thay vì client phải liên tục gửi yêu cầu (polling) lên server để kiểm tra xem có gì mới hay không.

Về bản chất, SignalR Core tạo ra một kết nối hai chiều (bi-directional) giữa server và client. Điều này khác biệt hoàn toàn với mô hình HTTP truyền thống chỉ cho phép client gửi yêu cầu và server phản hồi. Với SignalR, server có thể chủ động gửi tin nhắn đến bất kỳ client nào đang kết nối, hoặc thậm chí một nhóm client cụ thể.

SignalR Core tự động chọn phương thức kết nối tối ưu nhất dựa trên khả năng hỗ trợ của server và client. Các phương thức kết nối phổ biến bao gồm:

  1. WebSockets: Phương thức lý tưởng, cung cấp kênh giao tiếp hai chiều full-duplex trên một kết nối TCP duy nhất. Đây là phương thức hiệu quả nhất về độ trễ và tài nguyên.
  2. Server-Sent Events (SSE): Cho phép server đẩy dữ liệu một chiều xuống client qua kết nối HTTP. Client không thể gửi dữ liệu lên server qua kênh này, nhưng vẫn có thể gửi qua các yêu cầu HTTP riêng biệt.
  3. Long Polling: Phương thức “fallback” khi WebSockets và SSE không khả dụng. Client gửi một yêu cầu HTTP đến server và server giữ kết nối mở cho đến khi có dữ liệu mới hoặc hết thời gian chờ. Khi có dữ liệu hoặc hết thời gian, server gửi phản hồi và client ngay lập tức mở một yêu cầu mới. Đây là phương thức kém hiệu quả nhất.

SignalR Core xử lý việc “đàm phán” để chọn phương thức kết nối phù hợp nhất, giúp lập trình viên không cần phải bận tâm quá nhiều đến sự khác biệt giữa các phương thức này. Nó cũng tự động quản lý việc kết nối lại khi kết nối bị ngắt.

Tại sao nên sử dụng SignalR Core?

Có nhiều lý do khiến SignalR Core trở thành lựa chọn hàng đầu để xây dựng ứng dụng thời gian thực trong .NET:

  • Đơn giản hóa phát triển: SignalR cung cấp các abstraction (trừu tượng hóa) mạnh mẽ giúp bạn tập trung vào logic nghiệp vụ thay vì phải xử lý chi tiết phức tạp của việc quản lý kết nối và các giao thức truyền tải khác nhau.
  • Tự động quản lý kết nối: Tự động xử lý việc thiết lập, duy trì và kết nối lại các kết nối.
  • Hỗ trợ đa nền tảng: Cung cấp các client library cho JavaScript, .NET (C#, Blazor), Java, và nhiều ngôn ngữ khác, cho phép kết nối từ nhiều loại ứng dụng khác nhau (web browser, mobile app, desktop app…).
  • Tự động đàm phán phương thức truyền tải: Tự động chọn phương thức tốt nhất (WebSockets, SSE, Long Polling) dựa trên khả năng của cả server và client.
  • Khả năng mở rộng (Scalability): Hỗ trợ scale-out bằng cách sử dụng các backplane như Redis hoặc Azure SignalR Service, cho phép ứng dụng của bạn hoạt động trên nhiều server đồng thời. (Chúng ta đã tìm hiểu về Redis trong ASP.NET Core trước đây, giờ bạn sẽ thấy nó hữu ích ở đây).
  • Tích hợp sâu với ASP.NET Core: Tận dụng sức mạnh của các tính năng khác trong ASP.NET Core như Dependency Injection (DI), routing, authentication, authorization. (Hiểu rõ về DI sẽ giúp bạn cấu hình SignalR dễ dàng hơn).

Các khái niệm cốt lõi trong SignalR Core

Hubs

Hub là trung tâm chính xử lý giao tiếp giữa client và server. Bạn định nghĩa các phương thức trên một lớp kế thừa từ `Microsoft.AspNetCore.SignalR.Hub`. Các phương thức này có thể được gọi từ client, và bạn cũng có thể gọi các phương thức JavaScript (hoặc phương thức trong client code) từ bên trong Hub.

Một Hub cung cấp các đối tượng quan trọng:

  • Clients: Cung cấp các phương thức để gửi tin nhắn đến client. Ví dụ: Clients.All (tất cả client), Clients.Caller (client hiện tại), Clients.Others (tất cả client trừ client hiện tại), Clients.Client(connectionId) (một client cụ thể), Clients.Group(groupName) (một nhóm client).
  • Context: Chứa thông tin về kết nối hiện tại, bao gồm Context.ConnectionId (ID duy nhất của kết nối) và thông tin người dùng đã được xác thực (nếu có).

Kết nối (Connections)

Mỗi client kết nối đến Hub sẽ được gán một ID duy nhất (ConnectionId). ID này hữu ích cho việc quản lý và gửi tin nhắn đến các client cụ thể. SignalR Core tự động quản lý vòng đời của kết nối (kết nối, ngắt kết nối, kết nối lại).

Bạn có thể override các phương thức ảo OnConnectedAsync()OnDisconnectedAsync(Exception exception) trong lớp Hub để xử lý các sự kiện khi client kết nối hoặc ngắt kết nối. Điều này thường được dùng để thêm/bớt client vào các nhóm hoặc thực hiện các logic khởi tạo/dọn dẹp.

Groups

Group là một cách để gửi tin nhắn đến một tập hợp con của các client đang kết nối. Bất kỳ client nào cũng có thể được thêm hoặc xóa khỏi một group bằng cách sử dụng các phương thức AddToGroupAsync()RemoveFromGroupAsync() trong Hub. Groups rất hữu ích cho các kịch bản như phòng chat, theo dõi một chủ đề cụ thể, hoặc thông báo đến một nhóm người dùng.

Bắt đầu với SignalR Core: Một ví dụ đơn giản

Hãy cùng xây dựng một ứng dụng chat web đơn giản sử dụng SignalR Core.

Bước 1: Tạo dự án ASP.NET Core

Sử dụng .NET CLI (chúng ta đã làm quen với .NET CLI):

dotnet new web -o SimpleChatApp
cd SimpleChatApp

Bước 2: Thêm package SignalR

dotnet add package Microsoft.AspNetCore.SignalR

Bước 3: Cấu hình SignalR trên Server

Mở file Program.cs (hoặc Startup.cs nếu bạn dùng .NET 5 trở xuống) và cấu hình các dịch vụ cần thiết và middleware cho SignalR.

// Program.cs (.NET 6+)

var builder = WebApplication.CreateBuilder(args);

// Thêm dịch vụ SignalR vào Dependency Injection container
builder.Services.AddSignalR();

var app = builder.Build();

// Cấu hình middleware cho file tĩnh (index.html)
app.UseDefaultFiles(); // Tìm các file mặc định như index.html
app.UseStaticFiles(); // Cho phép phục vụ các file tĩnh

// Map endpoint cho Hub
// Clients sẽ kết nối đến "/chatHub"
app.MapHub<SimpleChatApp.Hubs.ChatHub>("/chatHub");

// Fallback cho các route không tìm thấy (ví dụ: spa mode)
app.MapFallbackToFile("index.html"); // Nếu không tìm thấy file tĩnh, trả về index.html

app.Run();

Nếu bạn dùng .NET 5 trở xuống với Startup.cs:

// Startup.cs (.NET 5 trở xuống)

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Thêm dịch vụ SignalR
        services.AddSignalR();
        // Các dịch vụ khác nếu cần
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Cấu hình file tĩnh và routing
        app.UseDefaultFiles();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            // Map endpoint cho Hub
            endpoints.MapHub<SimpleChatApp.Hubs.ChatHub>("/chatHub");
            // Fallback
            endpoints.MapFallbackToFile("index.html");
        });
    }
}

Bước 4: Tạo Hub

Tạo một thư mục Hubs trong dự án và thêm một class mới tên là ChatHub.cs:

// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SimpleChatApp.Hubs
{
    public class ChatHub : Hub
    {
        // Phương thức này được Client gọi đến Server
        // Khi client gọi "SendMessage", nó sẽ truyền vào user và message
        public async Task SendMessage(string user, string message)
        {
            // Gửi tin nhắn nhận được từ một client
            // đến TẤT CẢ các client khác đang kết nối
            // "ReceiveMessage" là tên phương thức mà client sẽ lắng nghe
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }

        // Bạn có thể override OnConnectedAsync và OnDisconnectedAsync
        public override async Task OnConnectedAsync()
        {
            // Logic khi client kết nối
            System.Console.WriteLine($"Client connected: {Context.ConnectionId}");
            // await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
            await base.OnConnectedAsync();
        }

        public override async Task OnDisconnectedAsync(Exception exception)
        {
            // Logic khi client ngắt kết nối
             System.Console.WriteLine($"Client disconnected: {Context.ConnectionId}");
            // await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
            await base.OnDisconnectedAsync(exception);
        }
    }
}

Bước 5: Tạo Client (HTML và JavaScript)

Tạo file index.html trong thư mục gốc của dự án (hoặc thư mục wwwroot nếu bạn tạo dự án MVC/Razor Pages). File này sẽ chứa giao diện chat và script kết nối đến SignalR Hub.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Chat App</title>
    <style>
        #messagesList { list-style: none; padding: 0; }
        #messagesList li { margin-bottom: 5px; }
    </style>
</head>
<body>
    <h1>Simple SignalR Chat</h1>

    <div>
        User: <input type="text" id="userInput" /><br>
        Message: <input type="text" id="messageInput" /><br>
        <button id="sendButton">Send Message</button>
    </div>

    <div>
        <strong>Messages:</strong>
        <ul id="messagesList"></ul>
    </div>

    <!-- Include SignalR client script -->
    <script src="https://unpkg.com/@microsoft/signalr@latest/dist/browser/signalr.js"></script>

    <script>
        // Tạo một kết nối mới đến Hub
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub") // URL endpoint của Hub đã map trên server
            .configureLogging(signalR.LogLevel.Information) // Cấu hình log level
            .build();

        // Bắt sự kiện khi nhận được tin nhắn từ server
        // Tên phương thức "ReceiveMessage" phải khớp với tên trong Hub.Clients.All.SendAsync
        connection.on("ReceiveMessage", function (user, message) {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messagesList").appendChild(li);
        });

        // Bắt sự kiện khi kết nối bị ngắt
        connection.onclose(function(error) {
            console.error("SignalR connection closed with error:", error);
            // Có thể thêm logic kết nối lại ở đây
        });

        // Bắt đầu kết nối đến Hub
        connection.start().then(function () {
            console.log("SignalR Connected.");
            // Có thể kích hoạt nút gửi sau khi kết nối thành công
            document.getElementById("sendButton").disabled = false;
        }).catch(function (err) {
            // Xử lý lỗi nếu kết nối không thành công
            console.error("Error starting SignalR connection:", err.toString());
            // Có thể hiển thị thông báo lỗi cho người dùng
        });

        // Xử lý sự kiện khi nút "Send Message" được nhấn
        document.getElementById("sendButton").addEventListener("click", function (event) {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;

            // Gọi phương thức "SendMessage" trên Server Hub
            // Tên phương thức "SendMessage" phải khớp với tên phương thức trong ChatHub
            connection.invoke("SendMessage", user, message).catch(function (err) {
                console.error("Error invoking SendMessage:", err.toString());
                // Có thể hiển thị thông báo lỗi cho người dùng
            });

            // Xóa nội dung ô tin nhắn sau khi gửi (tùy chọn)
            document.getElementById("messageInput").value = "";
            event.preventDefault(); // Ngăn form submit mặc định
        });

        // Vô hiệu hóa nút gửi ban đầu cho đến khi kết nối thành công
        document.getElementById("sendButton").disabled = true;

    </script>
</body>
<!/html>

Bước 6: Chạy ứng dụng

Mở terminal trong thư mục dự án và chạy:

dotnet watch run

Mở trình duyệt và truy cập địa chỉ được hiển thị (thường là https://localhost:7xxx hoặc http://localhost:5xxx). Mở nhiều tab hoặc trình duyệt khác nhau để thử gửi tin nhắn và xem chúng xuất hiện tức thời ở tất cả các cửa sổ.

Các phương thức truyền tải (Transports)

Như đã đề cập, SignalR Core tự động đàm phán để chọn phương thức truyền tải tốt nhất. Dưới đây là bảng tóm tắt các phương thức phổ biến:

Phương thức Mô tả Ưu điểm Nhược điểm Khi nào sử dụng
WebSockets Kênh giao tiếp hai chiều, full-duplex trên một kết nối TCP duy nhất. Hiệu quả nhất, độ trễ thấp nhất, sử dụng ít tài nguyên server nhất. Không được hỗ trợ trên tất cả các trình duyệt cũ hoặc các proxy mạng. Lý tưởng cho hầu hết các trường hợp, là phương thức mặc định và được ưu tiên.
Server-Sent Events (SSE) Server đẩy dữ liệu một chiều xuống client qua HTTP. Dễ triển khai, hỗ trợ kết nối lại tự động. Hoạt động tốt với các proxy HTTP. Chỉ một chiều từ server xuống client. Không hiệu quả bằng WebSockets cho giao tiếp hai chiều. Khi WebSockets không khả dụng và bạn chỉ cần server đẩy dữ liệu (ví dụ: dashboard, cập nhật tin tức). Client vẫn cần HTTP POST để gửi data lên.
Long Polling Client liên tục mở các kết nối HTTP mới lên server. Server giữ kết nối mở đến khi có dữ liệu hoặc hết thời gian chờ. Tương thích rộng rãi nhất, hoạt động trên hầu hết các trình duyệt và proxy. Kém hiệu quả nhất, độ trễ cao hơn, sử dụng nhiều tài nguyên server (mỗi tin nhắn là một yêu cầu/phản hồi mới). Phương thức fallback cuối cùng khi WebSockets và SSE không hoạt động.

Trong hầu hết các trường hợp, bạn chỉ cần dựa vào cơ chế đàm phán tự động của SignalR. Tuy nhiên, việc hiểu rõ các phương thức này giúp bạn debug hoặc tối ưu hóa khi cần thiết.

Mở rộng (Scaling) ứng dụng SignalR

Ứng dụng SignalR đơn giản chạy trên một server hoạt động tốt. Tuy nhiên, khi ứng dụng của bạn phát triển và bạn cần chạy nó trên nhiều server (ví dụ: sử dụng load balancer), các client kết nối đến các server khác nhau sẽ không thể nhận tin nhắn được gửi từ một server khác. Đây là lúc bạn cần một “Backplane”.

Backplane là một dịch vụ dùng chung mà tất cả các server SignalR kết nối đến. Khi một server nhận được tin nhắn từ client, nó sẽ gửi tin nhắn đó đến Backplane. Backplane sau đó phân phối tin nhắn đến tất cả các server khác trong farm, và mỗi server sẽ đẩy tin nhắn đó xuống các client được kết nối với nó.

Các tùy chọn Backplane phổ biến cho SignalR Core bao gồm:

  • Redis Backplane: Sử dụng Redis làm bộ nhớ đệm phân tán để đồng bộ hóa tin nhắn giữa các server. Đây là lựa chọn phổ biến và hiệu quả. Chúng ta đã tìm hiểu về RedisCache phân tán, và đây là một trong những ứng dụng thực tế quan trọng của nó.
  • Azure SignalR Service: Một dịch vụ được quản lý hoàn toàn trên Azure. Nó đóng vai trò như Backplane và thậm chí có thể xử lý các kết nối client trực tiếp, giải phóng tài nguyên trên các server ứng dụng của bạn. Đây là giải pháp tuyệt vời cho các ứng dụng quy mô lớn trên đám mây.

Để cấu hình Backplane, bạn thường chỉ cần thêm một dòng code cấu hình dịch vụ vào Program.cs hoặc Startup.cs:

// Cấu hình Redis Backplane
builder.Services.AddSignalR()
    .AddStackExchangeRedis("your_redis_connection_string");

// Hoặc cấu hình Azure SignalR Service
builder.Services.AddSignalR()
    .AddAzureSignalR("your_azure_signalr_connection_string");

Việc chọn Backplane nào phụ thuộc vào môi trường triển khai và yêu cầu về khả năng mở rộng của bạn.

Bảo mật trong SignalR Core

Vì SignalR Core tích hợp chặt chẽ với ASP.NET Core, bạn có thể sử dụng các cơ chế Authentication (xác thực) và Authorization (phân quyền) sẵn có. Thông tin người dùng đã được xác thực trên kết nối HTTP ban đầu sẽ được truyền sang kết nối SignalR và có sẵn thông qua Context.User trong Hub.

Bạn có thể áp dụng các attribute như [Authorize] trên lớp Hub hoặc các phương thức Hub cụ thể để chỉ cho phép người dùng đã xác thực hoặc có quyền nhất định mới có thể kết nối hoặc gọi các phương thức đó.

[Authorize] // Chỉ cho phép người dùng đã đăng nhập
public class SecureChatHub : Hub
{
    // Phương thức này chỉ có người dùng đã đăng nhập mới gọi được
    public async Task SendSecureMessage(string message)
    {
        var user = Context.User.Identity.Name; // Lấy tên người dùng đã xác thực
        await Clients.All.SendAsync("ReceiveSecureMessage", user, message);
    }

    [Authorize(Roles = "Admin")] // Chỉ Admin mới gọi được phương thức này
    public async Task KickUser(string connectionId)
    {
        // Logic kick user
    }
}

Việc hiểu rõ về Bảo mật trong ASP.NET Core là nền tảng để áp dụng bảo mật cho SignalR Hubs.

SignalR Core so với các công nghệ khác

Trong series về API, chúng ta đã tìm hiểu về RESTful API và gRPC. Vậy khi nào nên dùng SignalR?

  • SignalR vs RESTful API: RESTful API theo mô hình request/response, phù hợp cho việc lấy dữ liệu (GET), tạo (POST), cập nhật (PUT/PATCH), xóa (DELETE). Nó không hiệu quả cho việc server cần chủ động đẩy dữ liệu xuống client ngay lập tức cho nhiều client cùng lúc. SignalR sinh ra để giải quyết bài toán server-push và giao tiếp hai chiều thời gian thực.
  • SignalR vs gRPC: gRPC rất tốt cho giao tiếp hiệu năng cao giữa các service với nhau (service-to-service communication), đặc biệt là gRPC Streaming cho phép giao tiếp hai chiều liên tục. Tuy nhiên, gRPC thường sử dụng HTTP/2 và yêu cầu client/server hỗ trợ gRPC (thường là các client được tạo từ file .proto). SignalR được thiết kế đặc biệt cho giao tiếp giữa server và client web (browser), tận dụng các công nghệ web sẵn có (WebSockets, SSE, Long Polling) và có các client library dễ dùng cho trình duyệt. Nếu bạn cần giao tiếp thời gian thực giữa server và web browser một cách dễ dàng, SignalR thường là lựa chọn tốt hơn. Nếu bạn cần giao tiếp hiệu năng cực cao giữa các microservice, gRPC có thể phù hợp hơn.

Tóm lại, SignalR tập trung vào việc đơn giản hóa việc thêm chức năng thời gian thực vào ứng dụng web, đặc biệt là giao tiếp server-to-client.

Kết luận

SignalR Core là một thư viện mạnh mẽ và hiệu quả để xây dựng các ứng dụng web thời gian thực trong ASP.NET Core. Nó trừu tượng hóa sự phức tạp của các phương thức truyền tải, quản lý kết nối và cung cấp một mô hình lập trình Hub đơn giản, trực quan.

Việc nắm vững SignalR Core là một bước tiến quan trọng trong lộ trình phát triển .NET của bạn, mở ra cánh cửa để xây dựng các ứng dụng hiện đại, tương tác cao và mang lại trải nghiệm tuyệt vời cho người dùng.

Chúng ta đã đi qua các khái niệm cơ bản, cách thiết lập một ứng dụng chat đơn giản, hiểu về các phương thức truyền tải, và cách mở rộng cũng như bảo mật ứng dụng SignalR. Đây chỉ là điểm khởi đầu, còn rất nhiều điều thú vị để khám phá như việc sử dụng MessagePack cho hiệu năng cao hơn, tích hợp với các dịch vụ messaging queue, v.v.

Hãy thử nghiệm với ví dụ chat đơn giản này, tùy chỉnh nó, và suy nghĩ về cách bạn có thể áp dụng SignalR vào các dự án của mình để thêm chức năng thời gian thực!

Hẹn gặp lại các bạn trong những bài viết tiếp theo của series Lộ trình .NET!

Chỉ mục