Xây dựng kết nối WebSocket giữa trình duyệt và ứng dụng C#

Khi triển khai giao tiếp thời gian thực giữa trang web và ứng dụng desktop C#, WebSocket là giải pháp hiệu quả. Có hai phương pháp phổ biến để xây dựng phía server bằng C#: sử dụng thư viện bên thứ ba như Fleck, hoặc tự triển khai giao thức WebSocket thông qua Socket thuần.

Mẫu mã phía trình duyệt

Phần này chứa giao diện HTML và xử lý JavaScript để thiết lập kết nối với server:

<!DOCTYPE html>
<html lang="vi-VN">
<head>
    <meta charset="UTF-8">
    <title>Hệ thống truyền tải dữ liệu học tập</title>
    <script type="text/javascript">
        let socketConnection = null;
        
        function handleFileDispatch(actionBtn) {
            const currentState = actionBtn.textContent;
            
            if (currentState === 'Truyền') {
                actionBtn.textContent = 'Hủy';
                
                try {
                    if (socketConnection) socketConnection.close();
                } catch (exception) {
                    console.error('Lỗi khi đóng kết nối:', exception);
                }
                
                // Khởi tạo đối tượng WebSocket mới
                socketConnection = new WebSocket('ws://localhost:9090/ws');
                
                socketConnection.onopen = () => {
                    console.log('Kết nối đã được thiết lập:', socketConnection.readyState);
                    // Gửi yêu cầu nhận tệp về máy khách
                    const payloadMessage = 'action:getFileDownload;sourceLink:http://localhost:8080/dataset.xlsx;';
                    socketConnection.send(payloadMessage);
                    console.log('Đã gửi thông điệp đến server');
                };

                socketConnection.onmessage = (eventData) => {
                    console.log('Nhận phản hồi từ server:', eventData.data);
                    // Đóng kết nối sau khi nhận xác nhận
                    if (eventData.data === 'ACK_OK') {
                        socketConnection.close();
                    }
                };

                socketConnection.onclose = () => {
                    console.log('WebSocket đã đóng:', socketConnection.readyState);
                };

                socketConnection.onerror = (err) => {
                    console.error('Xảy ra lỗi kết nối:', err);
                };

            } else if (currentState === 'Hủy') {
                try {
                    if (socketConnection) socketConnection.close();
                } catch (exception) {
                    console.error(exception);
                }
                actionBtn.textContent = 'Truyền';
            }
        }
    </script>
</head>
<body>
    <div class="data-table-container">
        <table border="1" cellpadding="10">
            <tr>
                <th>ID Bài Kiểm Tra</th>
                <th>Tiêu Đề</th>
                <th>Mô Tả</th>
                <th>Hành Động</th>
            </tr>
            <tr>
                <td>EXAM_001</td>
                <td>Bài Tập Cơ Bản</td>
                <td>Thiết lập tài khoản, định kỳ kế toán, nhân viên ngân hàng</td>
                <td><button onclick="handleFileDispatch(this)">Truyền</button></td>
            </tr>
            <tr>
                <td>EXAM_002</td>
                <td>Thực Hành Nâng Cao</td>
                <td>Cấu hình chi tiết hệ thống và báo cáo</td>
                <td><button onclick="handleFileDispatch(this)">Truyền</button></td>
            </tr>
        </table>
    </div>
</body>
</html>

Triển khai Server bằng C#

Dưới đây là ví dụ tự xây dựng giao thức WebSocket sử dụng .NET Socket API:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace WebSocketServerApp
{
    public class TcpSocketHandler
    {
        private static int LISTEN_PORT = 9090;
        private TcpListener listener;
        private Thread serverThread;
        private bool isRunning = true;

        public void StartService()
        {
            listener = new TcpListener(IPAddress.Any, LISTEN_PORT);
            listener.Start();
            Console.WriteLine($"Dịch vụ đang lắng nghe trên cổng {LISTEN_PORT}...");

            serverThread = new Thread(ListenForClients);
            serverThread.IsBackground = true;
            serverThread.Start();
        }

        private void ListenForClients()
        {
            while (isRunning)
            {
                Socket peerConnection = listener.AcceptSocket();
                Console.WriteLine($"Khách hàng mới tham gia: {peerConnection.RemoteEndPoint}");

                HandleClientCommunication(peerConnection);
            }
        }

        private void HandleClientCommunication(Socket peerSocket)
        {
            byte[] buffer = new byte[2048];
            int bytesRead = peerSocket.Receive(buffer);
            string handshakeRequest = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Yêu cầu bắt tay HTTP:\n" + handshakeRequest);

            // Tìm Sec-WebSocket-Key trong request
            string wsKey = ExtractWebSocketKey(handshakeRequest);
            if (string.IsNullOrEmpty(wsKey)) return;

            // Tạo giá trị Sec-WebSocket-Accept theo chuẩn RFC6455
            string magicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
            byte[] sha1Hash = SHA1.HashData(Encoding.ASCII.GetBytes(wsKey + magicGuid));
            string acceptResponse = Convert.ToBase64String(sha1Hash);

            // Gửi phản hồi hoàn tất bắt tay
            string upgradeHeader = 
                "HTTP/1.1 101 Switching Protocols\r\n" +
                "Upgrade: websocket\r\n" +
                "Connection: Upgrade\r\n" +
                $"Sec-WebSocket-Accept: {acceptResponse}\r\n\r\n";
            
            byte[] headerBytes = Encoding.UTF8.GetBytes(upgradeHeader);
            peerSocket.Send(headerBytes);

            ProcessMessages(peerSocket);
        }

        private string ExtractWebSocketKey(string httpRequest)
        {
            var lines = httpRequest.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines)
            {
                if (line.StartsWith("Sec-WebSocket-Key:", StringComparison.OrdinalIgnoreCase))
                {
                    return line.Substring(line.IndexOf(':') + 1).Trim();
                }
            }
            return string.Empty;
        }

        private void ProcessMessages(Socket peerSocket)
        {
            try
            {
                while (peerSocket.Connected)
                {
                    byte[] dataBuffer = new byte[1024];
                    int receivedLength = peerSocket.Receive(dataBuffer);

                    if (receivedLength == 0) break;

                    string decodedText = DecodeWebSocketFrame(dataBuffer, receivedLength);
                    
                    if (!string.IsNullOrEmpty(decodedText))
                    {
                        Console.WriteLine($"Nhận được: {decodedText}");
                        var commandParams = ParseCommandStructure(decodedText);

                        if (commandParams.ContainsKey("action") && commandParams["action"] == "getFileDownload")
                        {
                            string fileLocation = commandParams.ContainsKey("sourceLink") ? commandParams["sourceLink"] : "";
                            Console.WriteLine($"URL tệp cần tải: {fileLocation}");
                            
                            peerSocket.Send(EncodeWebSocketMessage("ACK_OK"));
                            break;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Xảy ra lỗi trong luồng nhận tin: {ex.Message}");
            }
            finally
            {
                peerSocket.Close();
            }
        }

        private Dictionary ParseCommandStructure(string rawText)
        {
            var keyValuePairs = new Dictionary();
            
            if (rawText.EndsWith(";"))
                rawText = rawText[..^1];

            string[] segments = rawText.Split(';');
            
            foreach (var segment in segments)
            {
                if (segment.Contains(":"))
                {
                    string[] parts = segment.Split(':', 2);
                    if (parts.Length == 2)
                        keyValuePairs[parts[0]] = parts[1];
                }
            }
            
            return keyValuePairs;
        }

        private string DecodeWebSocketFrame(byte[] frameBytes, int length)
        {
            if (length < 6) return string.Empty;

            bool finalFrame = (frameBytes[0] & 0x80) != 0;
            bool maskPresent = (frameBytes[1] & 0x80) != 0;

            if (!finalFrame || !maskPresent) return string.Empty;

            int payloadSize = frameBytes[1] & 0x7F;
            byte[] maskingKey = new byte[4];
            byte[] actualData;

            int headerOffset = 2;

            if (payloadSize == 126)
            {
                headerOffset = 4;
                Array.Copy(frameBytes, 2, maskingKey, 0, 4);
                payloadSize = BitConverter.ToUInt16(frameBytes, 4);
            }
            else if (payloadSize == 127)
            {
                headerOffset = 10;
                Array.Copy(frameBytes, 6, maskingKey, 0, 4);
                payloadSize = (int)BitConverter.ToUInt64(frameBytes, 14);
            }
            else
            {
                Array.Copy(frameBytes, 2, maskingKey, 0, 4);
            }

            actualData = new byte[payloadSize];
            Array.Copy(frameBytes, headerOffset + 4, actualData, 0, payloadSize);

            for (int i = 0; i < payloadSize; i++)
            {
                actualData[i] ^= maskingKey[i % 4];
            }

            return Encoding.UTF8.GetString(actualData);
        }

        private byte[] EncodeWebSocketMessage(string textContent)
        {
            byte[] utf8Bytes = Encoding.UTF8.GetBytes(textContent);
            int dataLen = utf8Bytes.Length;

            byte[] result;

            if (dataLen < 126)
            {
                result = new byte[dataLen + 2];
                result[0] = 0x81;
                result[1] = (byte)dataLen;
                Array.Copy(utf8Bytes, 0, result, 2, dataLen);
            }
            else if (dataLen < 65536)
            {
                result = new byte[dataLen + 4];
                result[0] = 0x81;
                result[1] = 126;
                result[2] = (byte)(dataLen >> 8);
                result[3] = (byte)dataLen;
                Array.Copy(utf8Bytes, 0, result, 4, dataLen);
            }
            else
            {
                throw new ArgumentException("Thông điệp quá dài");
            }

            return result;
        }

        public void StopService()
        {
            isRunning = false;
            listener?.Stop();
        }
    }
}

Cơ chế đóng kết nối WebSocket

  • Cả client và server đều có khả năng chủ động ngắt kết nối bất cứ lúc nào.
  • Khi client đóng, server sẽ nhận được một khung dữ liệu có độ dài bằng 0.
  • Khi server đóng, sự kiện onclose sẽ được trigger ở phía client kèm theo trạng thái kết nối.
  • Luôn đảm bảo cả hai bên xử lý sự kiện đóng một cách đồng bộ để tránh rò rỉ tài nguyên.

Thẻ: WebSocket C# JavaScript TcpListener .NET Framework

Đăng vào ngày 12 tháng 8 lúc 02:23