Như phân tích ở chương trước, .NET framework hỗ trợ lập trình socket bằng cách tạo ra một wrapper xung quanh bộ Socket API của hệ thống, đồng thời cung cấp nhiều công cụ mạnh dùng trong phát triển ứng dụng mạng.
Bắt đầu từ bài này chúng ta sẽ xem xét các kỹ thuật lập trình socket cơ bản trong .NET, bao gồm lập trình với socket Tcp, socket Udp và socket thô. Các kỹ thuật này sử dụng trực tiếp lớp Socket để gửi và nhận dữ liệu. Chúng ta sẽ vận dụng các kỹ thuật lập trình socket cơ bản này để viết các ứng dụng mạng đơn giản theo mô hình client / server.
Trong bài học này chúng ta bắt đầu làm quen với lập trình socket Udp và các lớp hỗ trợ lập trình socket của .NET framework thông qua thực hiện một bài thực hành đơn giản.
Hỗ trợ lập trình socket trong .NET framework
.NET cung cấp không gian tên System.Net
và System.Net.Sockets
chứa các class và các kiểu dữ liệu hỗ trợ quan trọng phục vụ cho lập trình với socket. Các class này nằm trong assembly System (file thư viện System.dll), được sử dụng mặc định trong các project của C#.
Lớp cơ bản nhất, trung tâm và quan trọng nhất trong lập trình socket với C# và .NET là lớp Socket
.
Lớp Socket
đảm nhiệm vai trò của một wrapper xung quanh Windows Socket API. Lớp này giúp người lập trình gọi tới các API này từ chương trình .NET sử dụng các kiểu dữ liệu và cách thức lập trình của .NET mà không cần biết đến các cấu trúc của Windows socket API. Lớp Socket
cung cấp tất cả chức năng cơ bản dành cho lập trình truyền thông với socket cho C# (và các ngôn ngữ .NET khác).
Namespace System.Net.Sockets
cũng chứa một số lớp hỗ trợ lập trình socket cao cấp hơn như TcpClient
, TcpListener
, UdpClient
. Các lớp này đều được xây dựng xung quanh lớp Socket
giúp đơn giản hóa việc lập trình mạng. Các lớp này sẽ được xem xét chi tiết trong các bài sau.
Ngoài ra, để lập trình những ứng dụng mạng phức tạp hơn, .NET framework cung cấp thêm nhiều thư viện hỗ trợ lập trình với luồng mạng (NetworkStream), tuần tự hóa dữ liệu (Serialization), lập trình bất đồng bộ (Asynchronous programming), lập trình đa luồng (Threading), bảo mật socket, mã hóa dữ liệu, v.v.. Các nội dung này sẽ lần lượt được trình bày chi tiết ở các bài sau.
Để thực hiện tất cả bài tập và ví dụ chúng ta sẽ sử dụng Visual Studio 2017 (phiên bản Community, Professional hoặc Ultimate). Có thể tải ở đây.
Thực hành: xây dựng ứng dụng client/server đơn giản sử dụng socket Udp
Chúng ta bắt đầu học lập trình với Udp socket thông qua một ví dụ.
Bài toán
Viết một ứng dụng dòng lệnh đơn giản (console application) theo mô hình client/server đáp ứng các yêu cầu:
- Client cho phép người dùng nhập một chuỗi ký tự từ bàn phím và gửi chuỗi ký tự cho server;
- Server nhận chuỗi ký tự, chuyển đổi tất cả ký tự thành dạng in hoa và gửi chuỗi kết quả lại cho client;
- Client nhận kết quả và hiển thị lại cho người dùng.
Logic của cả hai ứng dụng được trình bày trong sơ đồ dưới đây.
Chuẩn bị solution và project
Bước 1. Tạo một solution trống trong Visual Studio đặt tên là UdpSocket
Bước 2. Tạo hai project kiểu Console Application, một project đặt tên là Client, project kia đặt tên là Server. Sau khi thực hiện, cửa sổ Solution Explorer hiển thị như sau:
Bước 3. Thiết lập để đồng thời chạy debug cả hai chương trình.
Do là một ứng dụng mạng với hai thành phần hoạt động song song, mỗi lần debug chúng ta thường phải chạy cả client và server. Thiết lập dưới đây giúp chúng ta nhanh chóng chạy cả hệ thống để debug bằng cách tự động chạy server trước, ngay sau đó sẽ tự chạy client.
Click phải vào tên solution và chọn Properties. Trong cửa sổ mở ra lựa chọn “Startup Project” (1) => chọn “Multiple startup projects” (2) => chọn giá trị “Start” trong cột “Action” cho từng project (3) => chọn và đẩy project Server lên trên (4). Ấn OK để hoàn tất.
Nếu ấn F5 (chạy debug), chương trình Server sẽ chạy trước, ngay sau đó chương trình Client sẽ chạy theo.
Viết code cho Client và Server
Bước 4. Mở file Program.cs trong project Client và viết code như sau:
Đọc kỹ các comment để nắm được ý nghĩa của từng lệnh
using System; using System.Net; // để sử dụng lớp IPAddress, IPEndPoint using System.Net.Sockets; // để sử dụng lớp Socket using System.Text; // để sử dụng lớp Encoding namespace Client { internal class Program { private static void Main(string[] args) { Console.Title = "Udp Client"; // yêu cầu người dùng nhập ip của server Console.Write("Server IP address: "); var serverIpStr = Console.ReadLine(); // chuyển đổi chuỗi ký tự thành object thuộc kiểu IPAddress var serverIp = IPAddress.Parse(serverIpStr); // yêu cầu người dùng nhập cổng của server Console.Write("Server port: "); var serverPortStr = Console.ReadLine(); // chuyển chuỗi ký tự thành biến kiểu int var serverPort = int.Parse(serverPortStr); // đây là "địa chỉ" của tiến trình server trên mạng // mỗi endpoint chứa ip của host và port của tiến trình var serverEndpoint = new IPEndPoint(serverIp, serverPort); var size = 1024; // kích thước của bộ đệm var receiveBuffer = new byte[size]; // mảng byte làm bộ đệm while (true) { // yêu cầu người dùng nhập một chuỗi Console.ForegroundColor = ConsoleColor.Green; Console.Write("# Text >>> "); Console.ResetColor(); var text = Console.ReadLine(); // khởi tạo object của lớp socket để sử dụng dịch vụ Udp // lưu ý SocketType của Udp là Dgram (datagram) var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); // biến đổi chuỗi thành mảng byte var sendBuffer = Encoding.ASCII.GetBytes(text); // gửi mảng byte trên đến tiến trình server socket.SendTo(sendBuffer, serverEndpoint); // endpoint này chỉ dùng khi nhận dữ liệu EndPoint dummyEndpoint = new IPEndPoint(IPAddress.Any, 0); // nhận mảng byte từ dịch vụ Udp và lưu vào bộ đệm // biến dummyEndpoint có nhiệm vụ lưu lại địa chỉ của tiến trình nguồn // tuy nhiên, ở đây chúng ta đã biết tiến trình nguồn là Server // do đó dummyEndpoint không có giá trị sử dụng var length = socket.ReceiveFrom(receiveBuffer, ref dummyEndpoint); // chuyển đổi mảng byte về chuỗi var result = Encoding.ASCII.GetString(receiveBuffer, 0, length); // xóa bộ đệm (để lần sau sử dụng cho yên tâm) Array.Clear(receiveBuffer, 0, size); // đóng socket và giải phóng tài nguyên socket.Close(); // in kết quả ra màn hình Console.WriteLine($">>> {result}"); } } } }
Bước 5. Mở file Program.cs trong project Server và viết code như sau:
using System; using System.Net; using System.Net.Sockets; using System.Text; namespace Server { internal class Program { private static void Main(string[] args) { Console.Title = "Udp Server"; // giá trị Any của IPAddress tương ứng với Ip của tất cả các giao diện mạng trên máy var localIp = IPAddress.Any; // tiến trình server sẽ sử dụng cổng 1308 var localPort = 1308; // biến này sẽ chứa "địa chỉ" của tiến trình server trên mạng var localEndPoint = new IPEndPoint(localIp, localPort); // yêu cầu hệ điều hành cho phép chiếm dụng cổng 1308 // server sẽ nghe trên tất cả các mạng mà máy tính này kết nối tới // chỉ cần gói tin udp đến cổng 1308, tiến trình server sẽ nhận được // một overload khác của hàm tạo Socket // InterNetwork là họ địa chỉ dành cho IPv4 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.Bind(localEndPoint); Console.WriteLine($"Local socket bind to {localEndPoint}. Waiting for request ..."); var size = 1024; var receiveBuffer = new byte[size]; while (true) { // biến này về sau sẽ chứa địa chỉ của tiến trình client nào gửi gói tin tới EndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, 0); // khi nhận được gói tin nào sẽ lưu lại địa chỉ của tiến trình client var length = socket.ReceiveFrom(receiveBuffer, ref remoteEndpoint); var text = Encoding.ASCII.GetString(receiveBuffer, 0, length); Console.WriteLine($"Received from {remoteEndpoint}: {text}"); // chuyển chuỗi thành dạng in hoa var result = text.ToUpper(); var sendBuffer = Encoding.ASCII.GetBytes(result); // gửi kết quả lại cho client socket.SendTo(sendBuffer, remoteEndpoint); Array.Clear(receiveBuffer, 0, size); } } } }
Dịch và chạy thử
Bước 6. Chạy thử ứng dụng (F5) Chương trình server sẽ chạy trước, chương trình client sẽ chạy ngay sau server.
Server sẽ viết ra thông báo về IP và giá trị cổng mà nó đang chờ request. Client sẽ yêu cầu người dùng nhập địa chỉ IP và số cổng của server. Vì server đang nghe tất cả các giao diện mạng và client đang chạy trên cùng máy vật lý với server, client có thể sử dụng địa chỉ loopback (127.0.0.1) và số cổng 1308.
Nhập bất kỳ chuỗi ký tự nào vào dấu nhắc lệnh của client. Ngay sau đó sẽ nhận được kết quả là chuỗi ký tự đã chuyển thành chữ hoa. Để ý rằng mỗi lần Client phát đi một chuỗi ký tự, giá trị cổng của nó lại thay đổi (ở trên là 63004, 63005, 63006).
Lưu ý, khi Server hoạt động lần điều tiên, hệ điều hành sẽ yêu cầu người dùng cấp phép cho Server chiếm dụng cổng 1308. Nếu chạy Server từ một tài khoản bị hạn chế quyền sẽ không thực hiện được yêu cầu này, và Server sẽ bị đóng lại ngay sau đó. Từ lần hoạt động sau sẽ không cần trả lời cấp phép nữa.
[wpdm_package id=’10368′]
Severity Code Description Project File Line Suppression State
Warning CS0028 ‘Program.Main(string[])’ has the wrong signature to be an entry point Client F:\NAV\com\vidaibao\learning\csharp\UdpSocket\Client\Program.cs 10
Severity Code Description Project File Line Suppression State
Error CS5001 Program does not contain a static ‘Main’ method suitable for an entry point Client F:\NAV\com\vidaibao\learning\csharp\UdpSocket\Client\CSC 1