Hỗ trợ lập trình socket: UdpClient, TcpClient, TcpListener, BitConverter, Encoding

    2

    Trong bài học này chúng ta sẽ xem xét một số lớp của C# hỗ trợ lập trình socket (UdpClient, TcpClient, TcpListener, BitConverter, Encoding). Các lớp này giúp đơn giản hóa lập trình socket hoặc hỗ trợ thực hiện các nhiệm vụ đặc biệt của lập trình mạng.

    Các lớp hỗ trợ lập trình socket

    Lớp UdpClient

    UdpClient là một lớp hỗ trợ lập trình với socket Udp. Lớp này được xây dựng trên lớp Socket nhưng có che bớt một số chi tiết của Socket và giúp đơn giản hóa việc gửi nhận dữ liệu.

    Hãy cùng thực hiện lại ví dụ trong bài thực hành Udp căn bản nhưng sử dụng lớp UdpClient.

    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    namespace Client
    {
        internal class Program
        {
            private static void Main()
            {            
                Console.Write("Server Ip: ");
                var ip = IPAddress.Parse(Console.ReadLine());
                var client = new UdpClient();
                //lệnh Connect này chỉ giúp lưu lại thông tin về đối tác trong object client chứ không phải thực hiện kết nối như trong tcp socket
                client.Connect(ip, 1308);
                while (true)
                {
                    Console.Write("# Text >>> ");
                    var text = Console.ReadLine();
                    var buffer = Encoding.ASCII.GetBytes(text);
                    client.Send(buffer, buffer.Length);
                    var dumpEp = new IPEndPoint(0, 0);
                    buffer = client.Receive(ref dumpEp);
                    var response = Encoding.ASCII.GetString(buffer);
                    Console.WriteLine(response);
                }
            }
        }
    }
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    namespace Server
    {
        internal class Program
        {
            private static void Main()
            {
                var server = new UdpClient(1308); // tự động bind với cổng
                while (true)
                {
                    var remoteEp = new IPEndPoint(0, 0);
                    var buffer = server.Receive(ref remoteEp);
                    var text = Encoding.ASCII.GetString(buffer);
                    var response = text.ToUpper();
                    buffer = Encoding.ASCII.GetBytes(response);
                    server.Send(buffer, buffer.Length, remoteEp);
                }
            }
        }
    }

    Qua ví dụ trên chúng ta có thể thấy việc khởi tạo udp socket và gửi/nhận dữ liệu đã được UdpClient đơn giản hóa đi đáng kể.

    Lớp TcpListener và TcpClient

    Tương tự như UdpClient, đối với lập trình socket Tcp, .NET xây dựng hai lớp hỗ trợ: TcpListener và TcpClient. Do khi lập trình tcp socket chúng ta phải tạo ra hai socket, .NET tạo ra lớp tương ứng: TcpListener dùng cho socket tiếp nhận kết nối; TcpClient dành cho socket gửi/nhận dữ liệu.

    Chúng ta hãy cùng làm lại bài thực hành Tcp cơ bản nhưng sử dụng TcpClient và TcpListener.

    using System;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    namespace Client
    {
        internal class Program
        {
            private static void Main()
            {
                Console.Write("Server Ip: ");
                var ip = IPAddress.Parse(Console.ReadLine());
                while (true)
                {
                    Console.Write("# Text >>> ");
                    var text = Console.ReadLine();
                    var client = new TcpClient();
                    client.Connect(ip, 1308);
                    var stream = client.GetStream();
                    var writer = new StreamWriter(stream) { AutoFlush = true };
                    var reader = new StreamReader(stream);
                    writer.WriteLine(text);
                    var response = reader.ReadLine();
                    client.Close();
                    Console.WriteLine(response);
                }
            }
        }
    }
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.IO;
    namespace Server
    {
        internal class Program
        {
            private static void Main()
            {
                var listener = new TcpListener(IPAddress.Any, 1308);
                listener.Start(10);
                while (true)
                {
                    var client = listener.AcceptTcpClient();
                    var stream = client.GetStream();
                    var reader = new StreamReader(stream);
                    var writer = new StreamWriter(stream) { AutoFlush = true };
                    var text = reader.ReadLine();
                    var response = text.ToUpper();
                    writer.WriteLine(response);
                    client.Close();
                }
            }
        }
    }

    Qua ví dụ trên có thể nhận xét:

    • Lớp TcpListener giúp tạo ra socket theo dõi yêu cầu kết nối;
    • Lớp TcpClient giúp tạo ra socket gửi/nhận dữ liệu;
    • TcpClient hoàn toàn ẩn đi các phương thức gửi/nhận dữ liệu trực tiếp của Socket;
    • TcpClient yêu cầu việc gửi/nhận dữ liệu phải tiến hành qua giao diện luồng;
    • Hai class này giúp việc lập trình socket được đơn giản hóa rất nhiều so với trực tiếp sử dụng Socket.

    Luồng dữ liệu là một công cụ rất mạnh để hỗ trợ lập trình mạng. Chúng ta sẽ xem xét chi tiết kỹ thuật làm việc với luồng trong bài học tiếp theo.

    Lớp Dns – chuyển đổi tên miền và địa chỉ Ip

    Dns là một class hỗ trợ thực hiện các chức năng liên quan đến truy vấn các thông tin qua tên miền. Lớp Dns nằm trong không gian tên System.Net.

    Hãy cùng thực hiện ví dụ sau để hiểu rõ hoạt động và các phương thức chính của Dns.

    using System;
    using System.Net;
    namespace ConsoleApp
    {
        class Program
        {
            static void Main(string[] args)
            {
                // lấy tên máy cục bộ
                var hostName = Dns.GetHostName();
                Console.WriteLine($"Local host name: {hostName}");
                // Lấy các địa chỉ Ip mà tên miền google.com trỏ tới
                var addresses = Dns.GetHostAddresses("google.com");
                Console.WriteLine("Addresses:");
                foreach (var a in addresses)
                {
                    Console.WriteLine(a);
                }
                // lấy tất cả thông tin về google.com
                var entry = Dns.GetHostEntry("google.com");
                Console.WriteLine("HostEntry of google.com");
                Console.WriteLine($"Host name: {entry.HostName}");
                Console.WriteLine("Addresses:");
                foreach (var a in entry.AddressList)
                {
                    Console.WriteLine(a);
                }
                Console.WriteLine("Aliases");
                foreach (var s in entry.Aliases)
                {
                    Console.WriteLine(s);
                }
                Console.ReadLine();
            }
        }
    }
    Kết quả thực hiện một số phương thức của lớp Dns
    Kết quả thực hiện một số phương thức của lớp Dns

    Lớp Socket yêu cầu làm việc trực tiếp với địa chỉ Ip. Nếu ứng dụng chạy trong LAN, làm việc trực tiếp với địa chỉ Ip đơn giản và nhanh chóng hơn. Tuy nhiên, để ứng dụng hoạt động trên Internet thì dùng địa chỉ Ip lại có nhiều nhược điểm.

    Nhờ có lớp Dns chúng ta có thể lấy địa chỉ Ip thông qua tên miền. Qua đó, đơn giản hóa việc lập trình ứng dụng hoạt động trên Internet.

    Các lớp hỗ trợ đã xem xét ở phần trên (UdpClient, TcpClient, TcpListener) đều hỗ trợ khả năng làm việc trực tiếp với hostname. Các lớp này tự mình gọi tới Dns để lấy thông tin.

    Chuyển đổi kiểu dữ liệu

    Như chúng ta đã biết, các loại socket đều chỉ nhận gửi đi các chuỗi byte chứ không làm việc với các kiểu dữ liệu thông thường của ngôn ngữ lập trình. Ở phía nhận, socket cũng chỉ trả về một chuỗi byte thô. Chương trình phải tự mình chuyển đổi chuỗi byte về kiểu dữ liệu mà chương trình cần.

    Để hỗ trợ việc chuyển đổi thành chuỗi byte (và ngược lại), .NET cung cấp hai class: Bitconverter để chuyển đổi các kiểu dữ liệu cơ sở; Encoding để chuyển đổi chuỗi ký tự.

    Để chuyển đổi các kiểu dữ liệu phức tạp hơn (như class), chúng ta cần đến công cụ Serialization (sẽ học chi tiết ở bài này).

    Lớp BitConverter, kiểu của C# và kiểu của .NET

    Các kiểu dữ liệu có sẵn (built-in types) của C# (cũng như của các ngôn ngữ .NET khác) đều là các “biệt danh” (alias) của các kiểu dữ liệu (class) được định nghĩa trong .NET framework. Ví dụ, kiểu int (C#) là biệt danh của System.Int32 (.NET). Khi biên dịch mã nguồn C#, tất cả kiểu của C# đều được quy về kiểu tương ứng của .NET. Tuy nhiên, C# khuyến khích sử dụng các kiểu built-in thay vì dùng kiểu của .NET.

    Khi làm việc với BitConverter chúng ta sẽ thấy rõ vấn đề này.

    BitConverter sử dụng phương thức GetBytes để chuyển đổi một biến kiểu cơ sở về một mảng byte. Phương thức này có 9 overload, tương ứng với 9 kiểu dữ liệu cơ sở của C#: bool, char, double, float, int, long, short, uint, ulong, ushort.

    Ở chiều ngược lại, để chuyển đổi mảng byte về kiểu dữ liệu cơ sở, BitConverter cung cấp một loạt phương thức có dạng ToX, trong đó X là tên kiểu .NET tương ứng.

    Bảng dưới đây liệt kê các kiểu tương ứng của C# và .NET được BitConverter hỗ trợ.

    STTTên kiểu C#Tên kiểu .NETGiải thích
    1bool
    System.Boolean
    2charSystem.Char
    3doubleSystem.Double
    4intSystem.Int32
    5longSystem.Int64
    6shortSystem.Int16
    7uintSystem.UInt32
    8ulongSystem.UInt64
    9ushortSystem.UInt16

    Trong code minh họa dưới đây chúng ta sẽ truyền các loại số từ client đến server.

    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Sockets;
    namespace Client
    {
        class Program
        {
            static void Main(string[] args)
            {
                while (true)
                {
                    Console.Write("Press enter to start sending");
                    Console.ReadLine();
                    var client = new Socket(SocketType.Stream, ProtocolType.Tcp);
                    client.Connect(IPAddress.Loopback, 1308);
                    bool aBool = true;
                    char aChar = 'A';
                    double aDouble = 10.01;
                    int anInt = 100;
                    long aLong = 1000000;
                    short aShort = 256;
                    var bytes = new List<byte>();
                    bytes.AddRange(BitConverter.GetBytes(aBool));
                    Console.WriteLine($"bool: {aBool}, {sizeof(bool)} byte(s)");
                    bytes.AddRange(BitConverter.GetBytes(aChar));
                    Console.WriteLine($"char: {aChar}, {sizeof(char)} byte(s)");
                    bytes.AddRange(BitConverter.GetBytes(aDouble));
                    Console.WriteLine($"double: {aDouble}, {sizeof(double)} byte(s)");
                    bytes.AddRange(BitConverter.GetBytes(anInt));
                    Console.WriteLine($"int: {anInt}, {sizeof(int)} byte(s)");
                    bytes.AddRange(BitConverter.GetBytes(aLong));
                    Console.WriteLine($"long: {aLong}, {sizeof(long)} byte(s)");
                    bytes.AddRange(BitConverter.GetBytes(aShort));
                    Console.WriteLine($"short: {aShort}, {sizeof(short)} byte(s)");
                    client.Send(bytes.ToArray());
                    client.Close();
                }
            }
        }
    }
    using System;
    using System.Net;
    using System.Net.Sockets;
    namespace Server
    {
        class Program
        {
            static void Main(string[] args)
            {
                var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
                listener.Bind(new IPEndPoint(IPAddress.Any, 1308));
                listener.Listen(10);
                while (true)
                {
                    var client = listener.Accept();
                    var buffer = new byte[8];
                    // đọc 1 byte đầu tiên và chuyển thành biến bool
                    client.Receive(buffer, 1, SocketFlags.None);
                    var aBool = BitConverter.ToBoolean(buffer, 0);
                    Console.WriteLine($"bool: {aBool}");
                    // đọc 2 byte tiếp theo chuyển thành char
                    client.Receive(buffer, 2, SocketFlags.None);
                    var aChar = BitConverter.ToChar(buffer, 0);
                    Console.WriteLine($"char: {aChar}");
                    // đọc 8 byte tiếp theo chuyển thành double
                    client.Receive(buffer, 8, SocketFlags.None);
                    var aDouble = BitConverter.ToDouble(buffer, 0);
                    Console.WriteLine($"double: {aDouble}");
                    // đọc 4 byte tiếp theo chuyển thành int
                    client.Receive(buffer, 4, SocketFlags.None);
                    var anInt = BitConverter.ToInt32(buffer, 0);
                    Console.WriteLine($"int: {anInt}");
                    // đọc 8 byte tiếp theo chuyển thành long
                    client.Receive(buffer, 8, SocketFlags.None);
                    var aLong = BitConverter.ToInt64(buffer, 0);
                    Console.WriteLine($"long: {aLong}");
                    // đọc 2 byte tiếp theo chuyển thành short
                    client.Receive(buffer, 2, SocketFlags.None);
                    var aShort = BitConverter.ToInt16(buffer, 0);
                    Console.WriteLine($"short: {aShort}");
                    client.Close();
                }
            }
        }
    }

    Lớp BitConverter được sử dụng khi cần chuyển đổi object của một class về dạng chuỗi ký tự trước khi gửi đi hoặc sau khi nhận về. Khi đó, chúng ta phải thực hiện biến đổi từng trường thuộc kiểu cơ sở mà BitConverter hỗ trợ.

    Tất cả các phương thức chuyển đổi từ chuỗi byte về kiểu tương ứng của C# .NET đều yêu cầu cung cấp một mảng byte và số thứ tự của byte trong mảng đó nơi bắt đầu của dãy byte cần biến đổi. BitConverter tự xác định cần đọc bao nhiêu byte từ vị trí đó và lấy ra đúng chừng ấy byte. Chuỗi byte con này sẽ được chuyển thành giá trị thuộc kiểu chúng ta yêu cầu.

    Encoding, chuyển đổi chuỗi ký tự

    Tương tự như số, để truyền các chuỗi ký tự qua mạng chúng ta cũng phải chuyển đổi nó về một chuỗi byte. Tuy nhiên, việc chuyển đổi chuỗi ký tự về chuỗi byte (và ngược lại) rất khác biệt so với chuyển đổi số và có nhiều cách thức khác nhau. Quy tắc chuyển đổi qua lại giữa chuỗi ký tự và chuỗi byte được gọi chung là encoding (mã hóa).

    .NET framework hỗ trợ các loại mã hóa sau: ASCII, Unicode, UTF-7, UTF-8, UTF-32. Tự bản thân kiểu dữ liệu string (System.String) sử dụng mã hóa Unicode. Nói cách khác, tất cả chuỗi ký tự được xử lý bởi C# đều là Unicode. Để lưu trữ (vào file) hoặc truyền đi, chúng ta có thể chuyển đổi các chuỗi Unicode của C# về các loại mã hóa khác nhau.

    Trong bài học sau chúng ta sẽ đi chi tiết hơn về nguyên lý chuyển đổi số và chuỗi ký tự sang chuỗi byte.

    Để thực hiện mã hóa, chúng ta không sử dụng được lớp BitConverter ở trên mà phải dùng các lớp Encoding. Tất cả các lớp này đều có hai phương thức chung: GetBytes dùng để chuyển đổi chuỗi ký tự của C# (vốn sử dụng mã hóa Unicode) về chuỗi byte theo dạng mã hóa tương ứng; GetString chuyển đổi từ chuỗi byte (thuộc một dạng mã hóa khác) về chuỗi ký tự của C#. Các lớp này đều nằm trong không gian tên System.Text.

    • Lớp ASCIIEncoding thực hiện mã hóa chuỗi ký tự theo bảng mã ASCII sử dụng 7 bit cho mỗi ký tự. Lớp này chỉ hỗ trợ các ký tự nằm trong dải U+0000 tới U+007F.
    • Lớp UnicodeEncoding mã hóa mỗi ký tự unicode sử dụng hai byte với trật tự byte little-endian (code page 1200) và big-endian (code page 1201).
    • Lớp UTF7Encoding chuyển mã Unicode sang UTF-7. UTF-7 hỗ trợ tất cả giá trị Unicode và sử dụng code page 65000.
    • Lớp UTF8Encoding chuyển mã Unicode sang UTF-8 sử dụng code page 65001.

    Để tiện lợi cho việc sử dụng các lớp này, .NET cung cấp lớp Encoding chứa các thuộc tính tĩnh tương đương với các lớp ở trên.

    Kết luận

    Trong bài học này chúng ta đã cùng xem xét một số class hỗ trợ lập trình socket trong .NET framework.

    Đặc điểm chung của các class này là chúng đều được xây dựng bao quanh lớp socket cơ bản mà chúng ta đã xem xét ở các phần trước. Mục đích chính của các class này là che bớt đi những điểm phức tạp của Socket, cũng như bổ sung thêm một số chức năng hỗ trợ lập trình socket.

    Chúng ta cũng xem xét một số class hỗ trợ chuyển đổi kiểu dữ liệu. Mặc dù các class này không được xây dựng riêng cho socket nhưng chúng đóng vai trò quan trọng khi lập trình socket trong .NET framework.

    Theo dõi
    Thông báo của
    guest

    2 Thảo luận
    Cũ nhất
    Mới nhất
    Phản hồi nội tuyến
    Xem tất cả bình luận
    Cần Giải Đáp
    var dumpEp = new IPEndPoint(0, 0);  
    

    Cho em hỏi là đoạn code này có ý nghĩa như thế nào ạ? và số ( 0, 0) là sao ạ