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 IPAddress – hỗ trợ làm việc với địa chỉ IP
Ngay từ những bài học đầu tiên về lập trình socket chúng ta đã sử dụng lớp IPAddress. Đây là lớp đối tượng bắt buộc phải cung cấp làm tham số để thực hiện truyền dữ liệu trong UDP socket, hoặc để tạo kết nối trong TCP socket.
Lớp IPAddress trong .NET là một lớp cung cấp các phương thức để đại diện cho một địa chỉ IP (Internet Protocol) và thực hiện các phương thức để làm việc với địa chỉ IP đó.
Lớp IPAddress có các thuộc tính và phương thức sau đây:
– Address: Thuộc tính này trả về một mảng byte đại diện cho địa chỉ IP. Ví dụ: “192.168.1.1” sẽ trả về một mảng byte với giá trị [192, 168, 1, 1].
– Parse: Phương thức này cho phép tạo một đối tượng IPAddress từ một chuỗi đại diện cho địa chỉ IP.
– TryParse: Phương thức này tương tự như phương thức Parse, nhưng nó trả về kết quả thông qua tham số đầu ra và trả về một giá trị bool để chỉ ra xem quá trình chuyển đổi có thành công hay không.
– IsLoopback: Phương thức này kiểm tra xem địa chỉ IP có phải là địa chỉ loopback hay không. Địa chỉ loopback (127.0.0.1) được sử dụng để định danh cho máy tính mà chương trình đang chạy.
Lớp IPAddress cũng cung cấp các hằng số để đại diện cho các địa chỉ IP quan trọng:
- Any: Đại diện cho tất cả các địa chỉ IP.
- Broadcast: Đại diện cho địa chỉ broadcast trong mạng.
- Loopback: Đại diện cho địa chỉ loopback (127.0.0.1).
- None: Đại diện cho địa chỉ IP không được chỉ định.
- IPv6Any: Đại diện cho tất cả các địa chỉ IPv6.
- IPv6Loopback: Đại diện cho địa chỉ IPv6 loopback (::1).
- IPv6None: Đại diện cho địa chỉ IPv6 không được chỉ định.
Lớp IPAddress cung cấp hai phương thức tĩnh rất hữu ích khi cần truyền dữ liệu nhiều byte giữa các hệ thống đầu cuối khác biệt (như giữa Windows – Linux, Windows – router):
Phương thức HostToNetworkOrder(): chuyển đổi một giá trị số (short, int, long) từ dạng biểu diễn của hệ điều hành đang sử dụng sang dạng Big Endian (sử dụng bởi mạng).
Phương thức NetworkToHostOrder(): chuyển đổi một giá trị số (short, int, long) từ dạng Big Endian về dạng biểu diễn của hệ điều hành.
Hai phương thức này chúng ta đã gặp trong bài thực hành xây dựng chương trình Ping với giao thức ICMP và socket IP.
Lớp Dns – chuyển đổi tên miền và địa chỉ Ip
Tên miền (domain name) là một chuỗi ký tự được sử dụng để định danh một trang web hoặc một địa chỉ email trên Internet. Tên miền thường được sử dụng để thay thế cho địa chỉ IP (Internet Protocol) của một trang web hoặc một thiết bị kết nối với mạng Internet.
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.
Lớp Dns trong .NET là một lớp có sẵn trong .NET Framework và .NET Core, cung cấp các phương thức để truy xuất thông tin DNS (Domain Name System). Lớp Dns nằm trong không gian tên System.Net.
Lớp Dns trong .NET cung cấp các phương thức để thực hiện các tác vụ sau:
– Xác định địa chỉ IP của một tên miền: Phương thức GetHostAddresses() được sử dụng để trả về một mảng các địa chỉ IP liên kết với một tên miền cụ thể.
– Xác định tên miền của một địa chỉ IP: Phương thức GetHostEntry() được sử dụng để trả về một đối tượng HostEntry chứa thông tin về một địa chỉ IP cụ thể, bao gồm tên miền liên quan.
– Xác định tên miền được sử dụng bởi một ứng dụng: Phương thức GetHostName() được sử dụng để trả về tên miền được sử dụng bởi ứng dụng đang chạy.
Lớp Dns trong .NET cũng hỗ trợ các phương thức để thực hiện các tác vụ phức tạp hơn liên quan đến DNS như truy vấn các bản ghi DNS khác nhau và xác thực các bản ghi DNS. Việc sử dụng lớp Dns trong .NET rất hữu ích khi cần xử lý các tác vụ liên quan đến DNS trong ứng dụng.
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(); } } }
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.
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).
Chuyển đổi dữ liệu nhị phân: Lớp BitConverter
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ợ.
STT | Tên kiểu C# | Tên kiểu .NET | Giải thích |
1 | bool | System.Boolean | |
2 | char | System.Char | |
3 | double | System.Double | |
4 | int | System.Int32 | |
5 | long | System.Int64 | |
6 | short | System.Int16 | |
7 | uint | System.UInt32 | |
8 | ulong | System.UInt64 | |
9 | ushort | System.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.
Chuyển đổi chuỗi ký tự: lớp Encoding
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.
+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạ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.
Cho em hỏi là đoạn code này có ý nghĩa như thế nào ạ? và số ( 0, 0) là sao ạ
Đây là lệnh tạo một “điểm cuối” – tổ hợp của IP : port – dùng để đánh địa chỉ cho một tiến trình trên liên mạng. Trong lập trình với socket UDP, cụ thể là trong phương thức nhận dữ liệu ReceiveForm(), đòi hỏi cung cấp một tham số có kiểu EndPoint nhằm lưu lại thông tin về tiến trình đối tác. Thông tin này sẽ được sử dụng để gửi lại phản hồi (nếu có) vì UDP không tạo ra liên kết giữa các tiến trình khi trao đổi dữ liệu. Nghĩa là, trước khi gói tin UDP… Đọc tiếp »