Triển khai WebSocket trong .NET 6 với cơ chế giữ kết nối hai chiều

WebSocket là giao thức truyền thông mạng cho phép thiết lập kênh kết nối bền vững, toàn song công giữa máy chủ và trình duyệt. Khác với mô hình yêu cầu–phản hồi của HTTP, WebSocket duy trì một kết nối TCP duy nhất, qua đó cả hai phía có thể gửi/nhận dữ liệu đồng thời — điều kiện cần cho các ứng dụng thời gian thực như bảng điều khiển giám sát, trò chơi đa người hoặc hệ thống thông báo tức thì.

Thiết lập máy chủ WebSocket bằng Minimal API (.NET 6+)

Dưới đây là triển khai máy chủ sử dụng WebApplication.CreateBuilder, tích hợp xử lý kết nối, gửi tin nhắn định kỳ và phản hồi tương tác từ phía client:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:5000");

var app = builder.Build();
app.UseWebSockets();

app.Map("/chat", async context =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        return;
    }

    using var socket = await context.WebSockets.AcceptWebSocketAsync();
    var cancellation = new CancellationTokenSource();

    // Luồng gửi thông báo thời gian thực mỗi 10 giây
    _ = Task.Run(async () =>
    {
        while (!cancellation.Token.IsCancellationRequested)
        {
            var payload = $"[SERVER] Thời điểm cập nhật: {DateTime.Now:HH:mm:ss}";
            var bytes = Encoding.UTF8.GetBytes(payload);
            await socket.SendAsync(
                new ArraySegment<byte>(bytes),
                WebSocketMessageType.Text,
                endOfMessage: true,
                cancellationToken: cancellation.Token);
            await Task.Delay(10_000, cancellation.Token);
        }
    }, cancellation.Token);

    // Xử lý nhận và phản hồi tin nhắn từ client
    var buffer = new byte[4096];
    try
    {
        WebSocketReceiveResult result;
        do
        {
            result = await socket.ReceiveAsync(
                new ArraySegment<byte>(buffer),
                cancellation.Token);

            if (result.MessageType == WebSocketMessageType.Text && result.Count > 0)
            {
                var input = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
                Console.WriteLine($"Nhận từ client: '{input}'");

                string response;
                if (input.Equals("ping", StringComparison.OrdinalIgnoreCase))
                {
                    response = "pong";
                }
                else if (input.Equals("pong", StringComparison.OrdinalIgnoreCase))
                {
                    response = "ack: heartbeat confirmed";
                }
                else
                {
                    response = $"[SERVER] Đã nhận: '{input}'";
                }

                var replyBytes = Encoding.UTF8.GetBytes(response);
                await socket.SendAsync(
                    new ArraySegment<byte>(replyBytes),
                    WebSocketMessageType.Text,
                    endOfMessage: true,
                    cancellationToken: cancellation.Token);
            }
        } while (!result.CloseStatus.HasValue);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Kết nối bị hủy do timeout.");
    }
    finally
    {
        await socket.CloseAsync(
            WebSocketCloseStatus.NormalClosure,
            "Đóng kết nối theo yêu cầu",
            cancellation.Token);
        cancellation.Cancel();
        Console.WriteLine("Kết nối WebSocket đã ngắt.");
    }
});

await app.RunAsync();

Client HTML đơn giản với hỗ trợ heartbeat

Trang HTML sau bao gồm giao diện nhập tin nhắn, hiển thị lịch sử hội thoại và cơ chế kiểm tra độ ổn định kết nối dựa trên chu kỳ ping/pong:

<!DOCTYPE html>
<html>
<head><title>WebSocket Chat</title></head>
<body>
  <h2>Kết nối WebSocket – Máy chủ thời gian thực</h2>
  <input id="txtInput" type="text" placeholder="Nhập tin nhắn..." />
  <button id="btnSend">Gửi</button>
  <ul id="log"></ul>

  <script>
    const ws = new WebSocket('ws://localhost:5000/chat');
    let lastPong = Date.now();
    let pingInterval;

    const appendLog = (from, msg) => {
      const li = document.createElement('li');
      li.innerHTML = `[${from}] ${msg}`;
      document.getElementById('log').appendChild(li);
      document.getElementById('log').scrollTop = document.getElementById('log').scrollHeight;
    };

    ws.onopen = () => {
      appendLog('HỆ THỐNG', 'Kết nối thành công');
      startHeartbeat();
    };

    ws.onmessage = (e) => {
      const data = e.data;
      appendLog('MÁY CHỦ', data);
      if (data === 'pong') {
        lastPong = Date.now();
      }
    };

    ws.onclose = () => {
      appendLog('HỆ THỐNG', 'Kết nối đã đóng');
      stopHeartbeat();
    };

    ws.onerror = (err) => {
      appendLog('LỖI', 'Kết nối gặp sự cố');
      stopHeartbeat();
    };

    document.getElementById('btnSend').onclick = () => {
      const input = document.getElementById('txtInput');
      if (input.value.trim()) {
        ws.send(input.value.trim());
        appendLog('TÔI', input.value.trim());
        input.value = '';
      }
    };

    function startHeartbeat() {
      pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send('ping');
          appendLog('TÔI', 'ping');
        }
        // Kiểm tra mất kết nối nếu không nhận pong trong 60s
        if (Date.now() - lastPong > 60_000) {
          appendLog('HỆ THỐNG', 'Không nhận được pong → đóng kết nối');
          ws.close();
          stopHeartbeat();
        }
      }, 15_000);
    }

    function stopHeartbeat() {
      if (pingInterval) clearInterval(pingInterval);
    }
  </script>
</body>
</html>

Cơ chế duy trì kết nối hai chiều

Để tránh ngắt kết nối do timeout từ proxy/firewall hoặc mất gói tin, cả máy chủ và client đều thực hiện:

  • Máy chủ: Gửi "ping" mỗi 15 giây, đóng kết nối nếu không nhận "pong" trong vòng 60 giây.
  • Client: Gửi "ping" định kỳ, cập nhật dấu thời gian khi nhận "pong", tự ngắt nếu quá hạn chờ.

Kết hợp cả hai phía giúp đảm bảo tính khả dụng cao và phát hiện sớm sự cố kết nối — đặc biệt hữu ích trong môi trường mạng không ổn định hoặc có gateway trung gian.

Thẻ: WebSocket .net6 minimal-api real-time-communication heartbeat-mechanism

Đăng vào ngày 24 tháng 5 lúc 03:30