Chương 3: Lập Trình Sockets

1. Socket là gì?

Lập trình socket là nền tảng của lập trình mạng. Mọi ứng dụng giao tiếp qua mạng — từ trình duyệt web, email, đến game online — đều hoạt động dựa trên cơ chế socket ở tầng thấp.

Socket có thể:

  • Ở chế độ mở (đang kết nối) hoặc đóng (ngắt kết nối)
  • Gửinhận dữ liệu
  • Dữ liệu được truyền theo từng khối (packet), thường vài KB mỗi lần

Mô hình hoạt động

[Tiến trình A] <---> [Socket A] <====Internet====> [Socket B] <---> [Tiến trình B]
                      (Cửa ra vào)                  (Cửa ra vào)
                      Do hệ điều hành quản lý

Khởi tạo Socket trong C#

// Tạo một socket gửi/nhận dữ liệu qua kết nối TCP
Socket s = new Socket(
    AddressFamily.InterNetwork,  // IPv4
    SocketType.Stream,           // Kiểu kết nối liên tục (TCP)
    ProtocolType.Tcp             // Giao thức TCP
);

2. Địa chỉ và Cổng (IP & Port)

Vấn đề cần giải quyết

Một máy tính có thể chạy hàng chục ứng dụng cùng lúc (trình duyệt, email client, game, …). Tuy nhiên, máy tính chỉ có một đường truyền vật lý ra Internet.

Máy A (192.168.1.1)                     Máy B (192.168.1.2)
┌──────────────────┐                     ┌────────────────────────┐
│ App Chrome       │ ──────────────────► │ Port 80  → Web Server  │
│ App Email Client │ ──────────────────► │ Port 25  → SMTP Server │
│ App FTP Tool     │ ──────────────────► │ Port 21  → FTP Server  │
└──────────────────┘                     └────────────────────────┘

Các cổng thường gặp

PortGiao thứcMô tả
20FTP DataTruyền dữ liệu FTP
21FTPĐiều khiển FTP
22SSHKết nối shell bảo mật
23TelnetKết nối terminal từ xa
25SMTPGửi email
53DNSPhân giải tên miền
80HTTPWeb không mã hóa
110POP3Nhận email
443HTTPSWeb có mã hóa SSL/TLS

Phân loại cổng

Khoảng PortTên gọiMục đích
0 – 1023Well-known PortsDành cho các ứng dụng hệ thống quan trọng (HTTP, FTP, SSH, …)
1024 – 49151Registered PortsCho lập trình viên đăng ký sử dụng (khuyến cáo dùng khoảng này)
49152 – 65535Dynamic/Private PortsDự trữ, dùng tự động bởi hệ điều hành

3. Lớp IPAddress

Giới thiệu

Mỗi thiết bị trên Internet có một địa chỉ IP duy nhất — gồm 4 số (mỗi số từ 0–255), ví dụ: 192.168.1.1.

Địa chỉ IP có thể biểu diễn dưới nhiều dạng:

DạngVí dụ
Tên máyMay01, Server
Chuỗi string"192.168.1.1", "127.0.0.1"
Mảng 4 byte{192, 168, 1, 1}
Số nguyên16885952 (dạng long 4 byte)

Chuyển đổi địa chỉ sang số nguyên

Thuộc tính quan trọng

Thuộc tínhMô tả
AnyĐịa chỉ đặc biệt, lắng nghe trên tất cả các interface mạng
Loopback127.0.0.1 — địa chỉ vòng lặp nội bộ (localhost)
BroadcastĐịa chỉ gửi cho tất cả các máy trong mạng

Phương thức quan trọng

Phương thức / ConstructorMô tả
IPAddress(long)Tạo địa chỉ IP từ một số kiểu long
IPAddress(byte[])Tạo địa chỉ IP từ mảng 4 byte
IPAddress.Parse(string)Chuyển chuỗi "192.168.1.1" thành đối tượng IPAddress
IPAddress.TryParse(string, out ip)Kiểm tra và chuyển — trả về bool, không ném ngoại lệ
GetAddressBytes()Trả về địa chỉ dưới dạng mảng byte
IsLoopback(ip)Kiểm tra có phải địa chỉ loopback không
AddressFamilyTrả về họ địa chỉ (IPv4: InterNetwork)

Ví dụ: Các cách tạo địa chỉ IP

// Cách 1: Dùng mảng byte
byte[] b = new byte[4];
b[0] = 192; b[1] = 168; b[2] = 1; b[3] = 1;
IPAddress ip1 = new IPAddress(b);

// Cách 2: Dùng số nguyên long
IPAddress ip2 = new IPAddress(16885952);

// Cách 3: Parse từ chuỗi (phổ biến nhất)
IPAddress ip3 = IPAddress.Parse("172.16.0.1");

// Cách 4: Tính toán thủ công
long so = 192 * (long)Math.Pow(256, 0)
        + 168 * (long)Math.Pow(256, 1)
        +   1 * (long)Math.Pow(256, 2)
        +   1 * (long)Math.Pow(256, 3);
IPAddress ip4 = new IPAddress(so);

Ví dụ: Kiểm tra tính hợp lệ của địa chỉ

private void KiemTra()
{
    IPAddress ip;
    string ip4 = "127.0.0.1";  // Hợp lệ
    string ip5 = "999.0.0.1";  // Không hợp lệ (999 > 255)

    // TryParse trả về true/false thay vì ném exception
    MessageBox.Show(ip4 + ": " + IPAddress.TryParse(ip4, out ip)); // True
    MessageBox.Show(ip5 + ": " + IPAddress.TryParse(ip5, out ip)); // False
}

Ví dụ: Chuyển địa chỉ ra mảng byte

void ConvertToIPArray()
{
    IPAddress ip = new IPAddress(16885952);
    byte[] b = ip.GetAddressBytes();

    // In ra dạng 192.168.1.1
    MessageBox.Show($"Address: {b[0]}.{b[1]}.{b[2]}.{b[3]}");
}

4. Lớp IPEndPoint

Giới thiệu

IPAddress chỉ cung cấp địa chỉ IP. Để xác định đầy đủ một điểm kết nối mạng, ta cần cả số cổng. Đó là vai trò của IPEndPoint.

IPEndPoint = IPAddress + Port Number

Thuộc tính và phương thức

TênMô tả
AddressLấy/thiết lập địa chỉ IP
PortLấy/thiết lập số cổng
IPEndPoint(Int64, Int32)Constructor: tạo từ số long và port
IPEndPoint(IPAddress, Int32)Constructor: tạo từ đối tượng IPAddress và port

Ví dụ

private void CreateEndpoint()
{
    // Tạo địa chỉ IP
    IPAddress ipAdd = IPAddress.Parse("127.0.0.1");

    // Tạo endpoint = IP + Port
    IPEndPoint ipEp = new IPEndPoint(ipAdd, 10000);

    Console.WriteLine(ipEp.Address); // 127.0.0.1
    Console.WriteLine(ipEp.Port);    // 10000
}

5. Lớp IPHostEntry

Giới thiệu

IPHostEntry là một container chứa thông tin địa chỉ của một máy trạm trên Internet. Thường được dùng kết hợp với lớp DNS.

Thuộc tính

Thuộc tínhMô tả
AddressListDanh sách các địa chỉ IP của máy
AliasesDanh sách tên bí danh của máy
HostNameTên máy chủ chính

6. Lớp DNS

Giới thiệu

DNS (Domain Name Service) giúp phân giải tên miền thành địa chỉ IP và ngược lại.

"google.com"  ──DNS──►  "142.250.185.78"
"MY-PC"       ──DNS──►  "192.168.1.5"

Phương thức (đều là static)

Phương thứcMô tả
Dns.GetHostEntry(string)Trả về IPHostEntry cho tên máy hoặc địa chỉ IP
Dns.GetHostAddresses(string)Trả về mảng IPAddress[] của một host
Dns.GetHostName()Trả về tên máy hiện tại

Ví dụ: Lấy tất cả địa chỉ IP của máy

private void ShowIPs()
{
    // Lấy tất cả địa chỉ IP của máy có tên "MY-PC"
    IPAddress[] addresses = Dns.GetHostAddresses("MY-PC");

    foreach (IPAddress ip in addresses)
    {
        MessageBox.Show(ip.ToString());
    }
}

7. Lớp UdpClient

Giao thức UDP là gì?

UDP (User Datagram Protocol) là giao thức phi kết nối (connectionless) — dữ liệu được gửi đi mà không cần thiết lập kết nối trước.

Đặc điểmGiải thích
Phi kết nốiKhông cần bắt tay (handshake) trước khi gửi
Không tin cậyGói tin có thể bị mất, trùng lặp, hoặc đến sai thứ tự
Tốc độ nhanhOverhead thấp, phù hợp với ứng dụng thời gian thực
Hỗ trợ BroadcastCó thể gửi đến nhiều máy cùng lúc

Trình tự kết nối UDP

sequenceDiagram participant Server participant Client Server->>Server: socket() Server->>Server: bind(port) Server->>Server: recvfrom() [chờ] Client->>Server: sendto(data) Server->>Client: sendto(response) Client->>Client: recvfrom() Client->>Client: close() Server->>Server: close()

Constructor và Phương thức quan trọng

// Tạo UDP client không gắn port (dùng để gửi)
UdpClient client = new UdpClient();

// Tạo UDP server gắn vào port cố định (dùng để nhận)
UdpClient server = new UdpClient(8080);

// Tạo từ endpoint
UdpClient ep = new UdpClient(new IPEndPoint(IPAddress.Any, 9000));
Phương thứcMô tả
Send(byte[], int, IPEndPoint)Gửi dữ liệu đến endpoint chỉ định
Receive(ref IPEndPoint)Nhận dữ liệu (chặn đến khi có dữ liệu đến)
BeginReceive(...)Nhận bất đồng bộ (non-blocking)
Close()Đóng kết nối, giải phóng tài nguyên

Ví dụ ứng dụng Chat UDP

Phía Client (gửi tin nhắn)

private void ClientSend_Click(object sender, EventArgs e)
{
    UdpClient udpClient = new UdpClient();

    // Lấy IP và Port từ giao diện
    IPAddress ipadd = IPAddress.Parse(textBox1.Text); // VD: "127.0.0.1"
    int port = Convert.ToInt32(textBox2.Text);         // VD: 8080
    IPEndPoint ipEnd = new IPEndPoint(ipadd, port);

    // Chuyển chuỗi thành mảng byte (UTF-8 để hỗ trợ tiếng Việt)
    byte[] sendBytes = Encoding.UTF8.GetBytes(richTextBox1.Text);

    // Gửi dữ liệu
    udpClient.Send(sendBytes, sendBytes.Length, ipEnd);

    // Xóa ô nhập sau khi gửi
    richTextBox1.Text = "";
}

Phía Server (nhận tin nhắn)

public void ServerThread()
{
    int port = Convert.ToInt32(textBox1.Text); // VD: 8080
    UdpClient udpServer = new UdpClient(port);

    while (true)
    {
        // IPAddress.Any + port 0: chấp nhận từ bất kỳ địa chỉ nào
        IPEndPoint remoteEnd = new IPEndPoint(IPAddress.Any, 0);

        // Chờ và nhận dữ liệu (blocking call)
        byte[] recvBytes = udpServer.Receive(ref remoteEnd);

        // Chuyển byte về chuỗi
        string data = Encoding.UTF8.GetString(recvBytes);

        // Hiển thị thông tin người gửi + nội dung
        string message = $"{remoteEnd.Address}:{remoteEnd.Port} → {data}";
        InfoMessage(message); // Gọi delegate để update UI an toàn
    }
}

Tổng kết quy trình UDP

graph TD A[Bước 1: Chuyển string → byte array
Encoding.UTF8.GetBytes] --> B[Bước 2: Gửi
udpClient.Send byte array, endpoint] C[Nhận dữ liệu
udpClient.Receive ref endpoint] --> D[Chuyển byte → string
Encoding.UTF8.GetString]

8. Lớp TcpClientTcpListener

Giao thức TCP là gì?

TCP (Transmission Control Protocol) là giao thức có kết nối (connection-oriented) — đảm bảo dữ liệu đến đích đầy đủ, đúng thứ tự.

So sánh TCP và UDP

Tiêu chíTCPUDP
Kết nốiCó (handshake 3 bước)Không
Độ tin cậyCao (đảm bảo gửi đến)Thấp (có thể mất gói)
Thứ tự gói tinĐảm bảo đúng thứ tựKhông đảm bảo
Tốc độChậm hơnNhanh hơn
OverheadCaoThấp
Ứng dụngWeb, email, FTPGame, stream, DNS

Trình tự kết nối TCP

sequenceDiagram participant Client participant Server Server->>Server: TcpListener.Start() Server->>Server: AcceptTcpClient() [chờ kết nối] Client->>Server: TcpClient.Connect() Server-->>Client: Chấp nhận kết nối Client->>Server: Send(data) Server->>Client: Receive(data) Client->>Server: Close() Server->>Server: Close()

Lớp TcpClient

// Tạo đối tượng TcpClient chưa kết nối
TcpClient client = new TcpClient();

// Kết nối đến server
client.Connect("192.168.1.1", 8080);
// Hoặc
client.Connect(new IPEndPoint(IPAddress.Parse("192.168.1.1"), 8080));
Thuộc tính / Phương thứcMô tả
AvailableSố byte đã nhận nhưng chưa đọc
ConnectedKiểm tra có đang kết nối không
GetStream()Lấy NetworkStream để đọc/ghi dữ liệu
Connect(host, port)Kết nối đến server
Close()Đóng kết nối

Lớp TcpListener

TcpListener được dùng phía server để lắng nghe và chấp nhận kết nối từ client.

// Tạo listener trên port 8080
TcpListener listener = new TcpListener(IPAddress.Any, 8080);

// Bắt đầu lắng nghe
listener.Start();

// Chờ và chấp nhận kết nối (blocking)
TcpClient client = listener.AcceptTcpClient();
Phương thứcMô tả
TcpListener(Port)Tạo listener trên port chỉ định
TcpListener(IPEndPoint)Tạo listener trên endpoint cụ thể
Start()Bắt đầu lắng nghe kết nối
Stop()Dừng lắng nghe
AcceptTcpClient()Chờ và trả về TcpClient khi có kết nối đến (blocking)
AcceptSocket()Tương tự nhưng trả về Socket thay vì TcpClient

Ví dụ: Ứng dụng TCP đơn giản

Server

public void StartServer()
{
    TcpListener listener = new TcpListener(IPAddress.Any, 8080);
    listener.Start();
    Console.WriteLine("Server đang lắng nghe...");

    while (true)
    {
        TcpClient client = listener.AcceptTcpClient();
        Console.WriteLine("Client kết nối: " + 
            ((IPEndPoint)client.Client.RemoteEndPoint).Address);

        // Xử lý client trên thread riêng
        Thread t = new Thread(() => HandleClient(client));
        t.Start();
    }
}

private void HandleClient(TcpClient client)
{
    NetworkStream stream = client.GetStream();
    byte[] buffer = new byte[1024];
    int bytesRead = stream.Read(buffer, 0, buffer.Length);
    string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
    Console.WriteLine("Nhận: " + message);

    // Gửi phản hồi
    byte[] response = Encoding.UTF8.GetBytes("Đã nhận: " + message);
    stream.Write(response, 0, response.Length);

    client.Close();
}

Client

public void SendMessage(string message)
{
    TcpClient client = new TcpClient();
    client.Connect("127.0.0.1", 8080);

    NetworkStream stream = client.GetStream();

    // Gửi dữ liệu
    byte[] data = Encoding.UTF8.GetBytes(message);
    stream.Write(data, 0, data.Length);

    // Nhận phản hồi
    byte[] buffer = new byte[1024];
    int bytesRead = stream.Read(buffer, 0, buffer.Length);
    Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));

    client.Close();
}

9. Debugging Network Code

Nguyên tắc debug ứng dụng mạng

Dùng try/catch cho mọi thao tác mạng

try
{
    serverSocket.Bind(ipepServer);
    serverSocket.Listen(10);
    // ...
}
catch (SocketException ex)
{
    Console.WriteLine("Lỗi Socket: " + ex.Message);
    Console.WriteLine("Error Code: " + ex.SocketErrorCode);
}
catch (Exception ex)
{
    Console.WriteLine("Lỗi chung: " + ex.Message);
}
finally
{
    // Luôn giải phóng tài nguyên
    serverSocket?.Close();
}

Dùng Trace cho ứng dụng đa luồng

using System.Diagnostics;

// Ghi log thread-safe
Trace.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] Nhận kết nối từ {client.Client.RemoteEndPoint}");

// Hoặc đơn giản hơn
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Server started on port 8080");

Sơ đồ tổng quan kiến trúc

graph TD %% Định nghĩa các node chính DNS[DNS
Phân giải tên miền] IPAddress[IPAddress
Địa chỉ IP IPv4/IPv6] IPHostEntry[IPHostEntry
Thông tin máy trạm] IPEndPoint[IPEndPoint
IP + Port number] UdpClient[UdpClient
Gửi/nhận UDP] TcpClient[TcpClient
Kết nối TCP phía client] TcpListener[TcpListener
Lắng nghe kết nối TCP] UDP_Final[UDP — phi kết nối] TCP_Final[TCP — có kết nối, tin cậy] %% Định nghĩa các mối quan hệ DNS --> IPAddress DNS --> IPHostEntry IPAddress -->|tạo| IPEndPoint IPEndPoint --> UdpClient IPEndPoint --> TcpClient IPEndPoint --> TcpListener UdpClient --> UDP_Final TcpClient --> TCP_Final TcpListener --> TCP_Final

Tóm tắt nhanh