ASP.NET Core 9: Tương tác real-time với SignalR (cho người mới bắt đầu)

Trong bài này, chúng ta sẽ tìm hiểu về SignalR, bao gồm các tiêu chuẩn, quy ước, và thực hành tốt nhất để triển khai giải pháp thời gian thực.

Chúng ta sẽ tạo ứng dụng quản lý công việc theo thời gian thực, áp dụng các kỹ thuật sẵn có của .NET và SignalR để hiểu khái niệm stream (luồng dữ liệu), đồng thời tìm hiểu cách triển khai SignalR trên máy chủ.

Các chủ đề chính

  • SignalR là gì
  • Hiểu về khái niệm ServerClient
  • Làm việc với Streaming
  • Triển khai ứng dụng SignalR ASP.NET Core
  • Yêu cầu kỹ thuật

SignalR là gì?

Trong các ứng dụng web, ta có hai phần chính: client (thường là trình duyệt) và server.
Client gửi yêu cầu, server xử lý rồi phản hồi lại — theo mô hình request/response tuần tự.
Tuy nhiên, có nhiều tình huống đòi hỏi giao tiếp thời gian thực, nơi dữ liệu liên tục được cập nhật — ví dụ:

  • Ứng dụng bản đồ hiển thị tình trạng giao thông ngay lập tức
  • Trò chơi trực tuyến
  • Mạng xã hội
  • Ứng dụng cộng tác (soạn thảo văn bản, bảng tính…)

Trong những trường hợp đó, client và server cần kênh kết nối liên tục để trao đổi dữ liệu.

SignalR là thư viện của .NET giúp tạo các ứng dụng thời gian thực một cách đơn giản, bằng cách duy trì kết nối hai chiều giữa client và server.

Hình – Các thành phần của SignalR

SignalR sử dụng kết nối chủ động, vận chuyển thông điệp JSON hoặc nhị phân (binary).
Mặc định, nó dùng WebSockets, nhưng nếu môi trường không hỗ trợ, sẽ tự động chuyển sang:

  1. Server-Sent Events – kết nối một chiều từ server đến client
  2. Long Polling – client liên tục gửi yêu cầu và chờ phản hồi

Thứ tự ưu tiên là: WebSockets → Server-Sent Events → Long Polling.

Thành phần cốt lõi: Hub

Kết nối giữa client và server được quản lý qua Hub, một lớp đặc biệt của SignalR.
Hub cho phép gọi hàm từ xa (RPC – Remote Procedure Call), tức là server có thể thực thi phương thức trên client và ngược lại.

SignalR ẩn toàn bộ sự phức tạp của việc quản lý kết nối, đồng thời hỗ trợ:

  • Gửi thông báo đến tất cả client
  • Gửi riêng cho từng client
  • Gửi theo nhóm client

SignalR hỗ trợ nhiều nền tảng: .NET, Java, JavaScript, thậm chí cả ứng dụng console.


Hiểu về khái niệm Server và Client

SignalR hoạt động dựa trên hai thành phần: ServerClient, tương tự ứng dụng web thông thường, nhưng với khả năng giao tiếp liên tục.

Chúng ta sẽ tìm hiểu mô hình này qua ví dụ: ứng dụng quản lý công việc (TaskManager).


Ứng dụng quản lý công việc (TaskManager)

Ứng dụng TaskManager (dùng Razor Pages) sẽ có các chức năng:

  • Hoạt động theo thời gian thực
  • Tạo công việc
  • Hoàn thành công việc
  • Hiển thị danh sách công việc đang và đã hoàn thành

Các thành phần của ứng dụng TaskManager

Thành phần chính:

  • Client (Razor Pages) – giao diện người dùng
    • index-page.js: quản lý giao tiếp giữa trang chính và server
    • signalr.js: SDK JavaScript của SignalR
  • Server (Razor Page app) – đóng vai trò điều phối
  • Hub – quản lý giao tiếp thời gian thực

Tạo dự án

Mở terminal, chạy:

dotnet new webapp -o TaskManager
cd TaskManager

Tiếp theo, cài SignalR JavaScript SDK bằng công cụ LibMan (Library Manager CLI):

dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli

Rồi thêm SignalR:

libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js

Tạo Hub

Tạo thư mục Hubs → thêm tệp TaskManagerHub.cs:

public class TaskManagerHub : Hub
{
    public async Task CreateTask(TaskModel taskModel)
    {
        // ..
    }

    public async Task CompleteTask(TaskModel taskModel)
    {
        // ..
    }
}

Hub kế thừa từ lớp Hub trong Microsoft.AspNetCore.SignalR.
Nó cho phép cả client và server gọi hàm của nhau.

Mô hình dữ liệu

Tạo tệp Models/TaskModel.cs:

public class TaskModel
{
    public Guid Id { get; } = Guid.NewGuid();
    public string Name { get; set; }
    public bool IsCompleted { get; set; }

    public TaskModel()
    {
        IsCompleted = false;
    }

    public TaskModel(string name) : this()
    {
        Name = name;
    }

    public TaskModel(string name, bool isCompleted)
    {
        Name = name;
        IsCompleted = isCompleted;
    }
}

Hoàn chỉnh phương thức CreateTask

public async Task CreateTask(TaskModel taskModel)
{
    _taskRepository.Save(taskModel);
    await Clients.All.SendAsync(ClientConstants.NOTIFY_TASK_MANAGER, taskModel);
}

Phương thức này:

  1. Lưu công việc (tạm trong bộ nhớ)
  2. Thông báo tới tất cả client đang kết nối qua Hub

Mẹo: Hãy dùng hằng số (constant) cho tên sự kiện, tránh lỗi chính tả khi gọi chuỗi.


Cấu hình server

Trong Program.cs, thêm SignalR và ánh xạ Hub:

using TaskManager.Hubs;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();
app.MapRazorPages();
app.MapHub<TaskManagerHub>("/taskmanagerhub");
app.Run();

Route /taskmanagerhub chính là endpoint mà client sẽ kết nối đến.


Chuẩn bị Client

Mở Pages/Index.cshtml và thay toàn bộ nội dung:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}
<div class="text-center">
    <h1 class="display-4">SignalR Task Manager</h1>
</div>

<div class="task-form-container">
    <h2>Add a New Task</h2>
    <form method="post" class="task-form">
        <input type="text" id="taskName" placeholder="Enter task name" class="task-input" />
        <input type="button" value="Add Task" id="addTaskButton" class="task-submit" />
    </form>
</div>

<div class="tasks-container">
    <h2>Uncompleted Tasks</h2>
    <div id="uncompletedTaskList"></div>
    <h2>Completed Tasks</h2>
    <div id="completedTaskList"></div>
</div>

@section Scripts {
    <script src="~/js/signalr/dist/browser/signalr.js"></script>
    <script src="~/js/index/index-page.js"></script>
}

HTML này chỉ tạo form nhập tên công việc và hai danh sách hiển thị.


Kết nối đến Hub (index-page.js)

var connection = new signalR.HubConnectionBuilder()
    .withUrl("/taskmanagerhub")
    .build();

connection.on("NotifyTaskManager", updateTaskList);

connection.start().then(function () {
    addTaskButton.disabled = false;
}).catch(function (err) {
    console.error(err.toString());
});

function updateTaskList(taskModel) {
    // Cập nhật danh sách công việc trên giao diện
}
  • connection.on() đăng ký sự kiện khi server gửi thông báo.
  • connection.start() khởi tạo kết nối.
  • updateTaskList() xử lý dữ liệu trả về.

Mẹo:
Dùng đối tượng làm tham số truyền (object-based params) để tránh phải thay đổi chữ ký hàm mỗi khi dữ liệu đổi.


Gửi công việc mới

var addTaskButton = document.getElementById("addTaskButton");

addTaskButton.addEventListener("click", function (event) {
    let taskName = document.getElementById("taskName");
    connection.invoke("CreateTask", { name: taskName.value })
        .catch(function (err) {
            console.error(err.toString());
        });
    taskName.value = "";
    taskName.focus();
    event.preventDefault();
});

Khi người dùng nhấn “Add Task”, client gọi phương thức CreateTask trên server qua kết nối SignalR.


Luồng giao tiếp giữa Client và Server

Luồng giao tiếp SignalR

  1. Người dùng nhập tên công việc → nhấn Add Task
  2. Client gọi CreateTask() trên server
  3. Server lưu công việc, rồi gọi ngược NotifyTaskManager() trên client
  4. Client nhận dữ liệu và cập nhật giao diện

SignalR giúp đồng bộ hóa hai chiều này gần như ngay lập tức.


Làm việc với Streaming

Streaming trong SignalR là hình thức truyền dữ liệu liên tục giữa client và server.
Khác với mô hình request/response truyền thống (chuyển dữ liệu một lần), streaming cho phép dòng dữ liệu chảy liên tục, rất hữu ích cho:

  • Dashboard trực tiếp
  • Ứng dụng chat
  • Bảng tin, số liệu liên tục

Đặc điểm:

  • Dữ liệu gửi đi ngay khi có sẵn
  • Bất đồng bộ (asynchronous)
  • Hai chiều (bidirectional streaming)

Thách thức

  • Phụ thuộc chất lượng mạng
  • Tốn tài nguyên (duy trì kết nối mở)
  • Khó debug
  • Giới hạn trình duyệt cũ
  • Cần chú ý bảo mật và khả năng mở rộng

Triển khai ví dụ Streaming

Tạo dự án mới:

mkdir SignalRStream
cd SignalRStream
dotnet new webapp -o SignalRStreamingApp
cd SignalRStreamingApp
code .

Tạo thư mục Hubs và thêm StreamHub.cs:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Channels;

namespace SignalRStream.Hubs;

public class StreamHub : Hub
{
    public ChannelReader<int> Countdown(int count)
    {
        var channel = Channel.CreateUnbounded<int>();
        _ = WriteItemsAsync(channel.Writer, count);
        return channel.Reader;
    }

    private async Task WriteItemsAsync(ChannelWriter<int> writer, int count)
    {
        for (int i = count; i >= 0; i--)
        {
            await writer.WriteAsync(i);
            await Task.Delay(1000); // mô phỏng độ trễ
        }
        writer.TryComplete();
    }
}

Phương thức Countdown() trả về luồng dữ liệu đếm ngược.


Cập nhật Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();
app.MapRazorPages();
app.MapHub<StreamHub>("/streamHub");
app.Run();

Cài client library:

libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js

Tạo wwwroot/js/index-stream.js:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/streamHub")
    .build();

connection.start().then(function () {
  connection.stream("Countdown", 10).subscribe({
    next: (count) => logStream(count),
    complete: () => logStream("Stream completed"),
    error: (err) => logStream(err)
  });
}).catch(err => logStream(err.toString()));

function logStream(status) {
  let li = document.createElement("li");
  let ul = document.getElementById("ulLog");
  li.textContent = status;
  ul.appendChild(li);
}

HTML (Index.cshtml):

@page
@model IndexModel
<div class="text-center">
  <h1 class="display-4">Stream</h1>
  <ul id="ulLog"></ul>
</div>

@section Scripts {
  <script src="~/js/signalr/dist/browser/signalr.js"></script>
  <script src="~/js/index-stream.js"></script>
}

Chạy ứng dụng:

dotnet run

Ứng dụng SignalR Streaming

Kết quả: hiển thị đếm ngược từ 10 đến 0, sau đó hiển thị “Stream completed”.

Streaming cho phép gửi từng phần dữ liệu nhỏ liên tục, giúp ứng dụng phản hồi nhanh và tự nhiên hơn.


Triển khai ứng dụng SignalR lên máy chủ

Ứng dụng SignalR có thể được host giống ứng dụng ASP.NET Core bình thường —
dù là trên server nội bộ hay cloud (Azure, AWS, GCP).

Các mô hình phổ biến:

  1. Truyền thống (IIS, Nginx, Apache) – đóng vai trò reverse proxy.
  2. Cloud Hosting – ví dụ Azure App Service, hỗ trợ tốt ASP.NET Core.
  3. Containers (Docker, Kubernetes) – dễ triển khai, đảm bảo đồng nhất môi trường.

Các bước cơ bản

  1. Đóng gói ứng dụng: dotnet publish -c Release -o ./Published
  2. Cấu hình server: cài .NET runtime, thiết lập reverse proxy nếu cần.
  3. Cấu hình proxy: để đảm bảo SignalR duy trì kết nối liên tục.
  4. Triển khai: tải lên qua FTP, Web Deploy hoặc CI/CD pipeline.

Lưu ý khi host SignalR

  • Mỗi kết nối đều tốn tài nguyên — cần theo dõi giới hạn kết nối.
  • Dùng sticky sessions nếu có load balancing.
  • Có thể mở rộng bằng cách triển khai nhiều instance và phân phối tải.

Tóm lại, việc host SignalR không khác nhiều so với ứng dụng web thường, chỉ cần lưu ý thêm phần kết nối liên tục và tài nguyên server.


Tóm tắt:

  • Hiểu cách SignalR mang lại khả năng tương tác thời gian thực cho ASP.NET Core 9
  • Xây dựng ứng dụng quản lý công việc dùng SignalR
  • Tìm hiểu mô hình giao tiếp hai chiều giữa client và server
  • Hiểu và triển khai streaming dữ liệu liên tục
  • Nắm được các bước triển khai ứng dụng SignalR lên máy chủ

Để lại một bình luận