Java Networking: Xây dựng Chat Server với TCP/UDP Sockets


Lời mở đầu

Chào mọi người! Sau khi học về Socket Programming ở bài trước, hôm nay mình muốn đi sâu vào Java Networking - một trong những điểm mạnh của Java trong việc xây dựng network applications.

Mình sẽ hướng dẫn các bạn xây dựng một Chat Server thực tế sử dụng cả TCP và UDP. Đây là kiến thức nền tảng mà mọi Java developer nên biết!

🎯 Mục tiêu của bài học

Sau bài này, bạn sẽ biết:

  • ✅ Java Socket API cơ bản (Socket, ServerSocket)
  • ✅ Xây dựng TCP Chat Server/Client
  • ✅ Xây dựng UDP Chat Server/Client
  • ✅ So sánh TCP vs UDP trong thực tế
  • ✅ Xử lý multiple clients với threads
  • ✅ Error handling và best practices

🌐 Java Socket API Overview

Package java.net

Java cung cấp package java.net với các class chính:

Cho TCP:

  • Socket - Client-side TCP socket
  • ServerSocket - Server-side TCP socket
  • InetAddress - IP address representation

Cho UDP:

  • DatagramSocket - UDP socket (cả client & server)
  • DatagramPacket - UDP packet (data container)

Utilities:

  • URL, URLConnection - HTTP utilities
  • NetworkInterface - Network interface info

Kiến trúc tổng quan

┌─────────────────────────────────────────────────┐
│                 TCP Architecture                 │
├─────────────────────────────────────────────────┤
│                                                  │
│   Client                        Server           │
│     │                             │              │
│     │  1. new Socket()            │              │
│     │─────────────────────────────▶              │
│     │                             │ new          │
│     │                             │ ServerSocket │
│     │                             │ accept()     │
│     │  2. Connection established  │              │
│     │◀─────────────────────────────              │
│     │                             │              │
│     │  3. InputStream/OutputStream│              │
│     │◀────────────────────────────▶              │
│     │                             │              │
│     │  4. close()                 │              │
│     │─────────────────────────────▶              │
│                                                  │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│                 UDP Architecture                 │
├─────────────────────────────────────────────────┤
│                                                  │
│   Client                        Server           │
│     │                             │              │
│     │  new DatagramSocket()       │ new          │
│     │                             │ DatagramSocket│
│     │                             │              │
│     │  send(DatagramPacket)       │              │
│     │─────────────────────────────▶              │
│     │                             │ receive()    │
│     │  receive(DatagramPacket)    │              │
│     │◀─────────────────────────────              │
│     │                             │              │
│                                                  │
└─────────────────────────────────────────────────┘

💬 Phần 1: TCP Chat Server

1. TCP Server - Phiên bản đơn giản

ChatServer.java:

import java.io.*;
import java.net.*;

public class ChatServer {
    private static final int PORT = 5000;
    
    public static void main(String[] args) {
        System.out.println("Chat Server started on port " + PORT);
        
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                // Chờ client kết nối
                Socket clientSocket = serverSocket.accept();
                System.out.println("New client connected: " + 
                    clientSocket.getInetAddress().getHostAddress());
                
                // Xử lý client (blocking - chỉ 1 client tại 1 thời điểm)
                handleClient(clientSocket);
            }
        } catch (IOException e) {
            System.err.println("Server error: " + e.getMessage());
        }
    }
    
    private static void handleClient(Socket socket) {
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(
                socket.getOutputStream(), true)
        ) {
            out.println("Welcome to Chat Server!");
            
            String message;
            while ((message = in.readLine()) != null) {
                System.out.println("Client: " + message);
                
                // Echo back
                out.println("Server received: " + message);
                
                if (message.equalsIgnoreCase("bye")) {
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("Error handling client: " + e.getMessage());
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Giải thích:

  1. ServerSocket(PORT): Tạo server socket lắng nghe trên port 5000
  2. accept(): Blocking call - đợi client kết nối
  3. getInputStream(): Nhận data từ client
  4. getOutputStream(): Gửi data đến client
  5. BufferedReader/PrintWriter: Đọc/ghi text dễ dàng

Vấn đề:

  • ❌ Chỉ xử lý được 1 client tại 1 thời điểm
  • ❌ Blocking - client khác phải đợi
  • ❌ Không scalable

2. TCP Client

ChatClient.java:

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class ChatClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 5000;
    
    public static void main(String[] args) {
        try (
            Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(
                socket.getOutputStream(), true);
            Scanner scanner = new Scanner(System.in)
        ) {
            System.out.println("Connected to server!");
            
            // Nhận welcome message
            System.out.println(in.readLine());
            
            // Chat loop
            while (true) {
                System.out.print("You: ");
                String message = scanner.nextLine();
                
                // Gửi message
                out.println(message);
                
                // Nhận response
                String response = in.readLine();
                System.out.println(response);
                
                if (message.equalsIgnoreCase("bye")) {
                    break;
                }
            }
            
        } catch (IOException e) {
            System.err.println("Client error: " + e.getMessage());
        }
    }
}

Cách chạy:

# Terminal 1: Start server
javac ChatServer.java
java ChatServer

# Terminal 2: Start client
javac ChatClient.java
java ChatClient

3. Multi-threaded TCP Server

Để xử lý nhiều clients đồng thời, ta dùng threads:

MultiThreadedChatServer.java:

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class MultiThreadedChatServer {
    private static final int PORT = 5000;
    private static Set<ClientHandler> clientHandlers = 
        Collections.synchronizedSet(new HashSet<>());
    
    public static void main(String[] args) {
        System.out.println("Multi-threaded Chat Server started on port " + PORT);
        
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("New client connected: " + 
                    clientSocket.getInetAddress().getHostAddress());
                
                // Tạo thread mới cho mỗi client
                ClientHandler handler = new ClientHandler(clientSocket);
                clientHandlers.add(handler);
                new Thread(handler).start();
            }
        } catch (IOException e) {
            System.err.println("Server error: " + e.getMessage());
        }
    }
    
    // Broadcast message đến tất cả clients
    public static void broadcast(String message, ClientHandler sender) {
        synchronized (clientHandlers) {
            for (ClientHandler client : clientHandlers) {
                if (client != sender) {
                    client.sendMessage(message);
                }
            }
        }
    }
    
    // Xóa client khi disconnect
    public static void removeClient(ClientHandler handler) {
        clientHandlers.remove(handler);
        System.out.println("Client disconnected. Total clients: " + 
            clientHandlers.size());
    }
}

class ClientHandler implements Runnable {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;
    private String username;
    
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            
            // Xin username
            out.println("Enter your username:");
            username = in.readLine();
            
            if (username == null || username.trim().isEmpty()) {
                username = "Anonymous";
            }
            
            System.out.println(username + " joined the chat");
            MultiThreadedChatServer.broadcast(
                username + " joined the chat", this);
            
            // Chat loop
            String message;
            while ((message = in.readLine()) != null) {
                if (message.equalsIgnoreCase("/quit")) {
                    break;
                }
                
                System.out.println(username + ": " + message);
                MultiThreadedChatServer.broadcast(
                    username + ": " + message, this);
            }
            
        } catch (IOException e) {
            System.err.println("Error in ClientHandler: " + e.getMessage());
        } finally {
            cleanup();
        }
    }
    
    public void sendMessage(String message) {
        if (out != null) {
            out.println(message);
        }
    }
    
    private void cleanup() {
        try {
            if (in != null) in.close();
            if (out != null) out.close();
            if (socket != null) socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        MultiThreadedChatServer.removeClient(this);
        MultiThreadedChatServer.broadcast(
            username + " left the chat", this);
    }
}

Cải tiến:

  • ✅ Xử lý nhiều clients đồng thời
  • ✅ Broadcast message đến tất cả clients
  • ✅ Username cho mỗi user
  • ✅ Thông báo join/leave

Multi-threaded Client:

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class MultiThreadedChatClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 5000;
    
    public static void main(String[] args) {
        try (
            Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(
                socket.getOutputStream(), true);
            Scanner scanner = new Scanner(System.in)
        ) {
            System.out.println("Connected to server!");
            
            // Thread để nhận messages
            Thread receiveThread = new Thread(() -> {
                try {
                    String message;
                    while ((message = in.readLine()) != null) {
                        System.out.println(message);
                    }
                } catch (IOException e) {
                    System.out.println("Disconnected from server");
                }
            });
            receiveThread.start();
            
            // Main thread gửi messages
            while (true) {
                String message = scanner.nextLine();
                out.println(message);
                
                if (message.equalsIgnoreCase("/quit")) {
                    break;
                }
            }
            
            receiveThread.join(1000);
            
        } catch (IOException | InterruptedException e) {
            System.err.println("Client error: " + e.getMessage());
        }
    }
}

Cải tiến:

  • ✅ Nhận messages real-time (không chờ input)
  • ✅ Gửi và nhận đồng thời
  • ✅ UX tốt hơn

📦 Phần 2: UDP Chat Server

UDP không có “connection”, chỉ gửi/nhận packets.

1. UDP Server

UDPChatServer.java:

import java.io.*;
import java.net.*;
import java.util.*;

public class UDPChatServer {
    private static final int PORT = 6000;
    private static final int BUFFER_SIZE = 1024;
    private static Set<InetSocketAddress> clients = 
        Collections.synchronizedSet(new HashSet<>());
    
    public static void main(String[] args) {
        System.out.println("UDP Chat Server started on port " + PORT);
        
        try (DatagramSocket socket = new DatagramSocket(PORT)) {
            byte[] buffer = new byte[BUFFER_SIZE];
            
            while (true) {
                // Nhận packet từ client
                DatagramPacket receivePacket = 
                    new DatagramPacket(buffer, buffer.length);
                socket.receive(receivePacket);
                
                // Lấy thông tin client
                InetAddress clientAddress = receivePacket.getAddress();
                int clientPort = receivePacket.getPort();
                InetSocketAddress clientSocket = 
                    new InetSocketAddress(clientAddress, clientPort);
                
                // Decode message
                String message = new String(
                    receivePacket.getData(), 
                    0, 
                    receivePacket.getLength()
                ).trim();
                
                // Thêm client vào danh sách
                if (!clients.contains(clientSocket)) {
                    clients.add(clientSocket);
                    System.out.println("New client: " + clientSocket);
                }
                
                System.out.println("Received: " + message + " from " + clientSocket);
                
                // Broadcast đến tất cả clients (trừ sender)
                broadcast(socket, message, clientSocket);
            }
            
        } catch (IOException e) {
            System.err.println("Server error: " + e.getMessage());
        }
    }
    
    private static void broadcast(
        DatagramSocket socket, 
        String message, 
        InetSocketAddress sender
    ) {
        byte[] buffer = message.getBytes();
        
        synchronized (clients) {
            for (InetSocketAddress client : clients) {
                if (!client.equals(sender)) {
                    try {
                        DatagramPacket packet = new DatagramPacket(
                            buffer,
                            buffer.length,
                            client.getAddress(),
                            client.getPort()
                        );
                        socket.send(packet);
                    } catch (IOException e) {
                        System.err.println("Error sending to " + client);
                    }
                }
            }
        }
    }
}

2. UDP Client

UDPChatClient.java:

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class UDPChatClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 6000;
    private static final int BUFFER_SIZE = 1024;
    
    public static void main(String[] args) {
        try (
            DatagramSocket socket = new DatagramSocket();
            Scanner scanner = new Scanner(System.in)
        ) {
            InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
            
            System.out.print("Enter your username: ");
            String username = scanner.nextLine();
            
            // Thread để nhận messages
            Thread receiveThread = new Thread(() -> {
                byte[] buffer = new byte[BUFFER_SIZE];
                
                while (true) {
                    try {
                        DatagramPacket packet = new DatagramPacket(
                            buffer, 
                            buffer.length
                        );
                        socket.receive(packet);
                        
                        String message = new String(
                            packet.getData(), 
                            0, 
                            packet.getLength()
                        );
                        System.out.println(message);
                        
                    } catch (IOException e) {
                        break;
                    }
                }
            });
            receiveThread.setDaemon(true);
            receiveThread.start();
            
            // Gửi messages
            System.out.println("Connected! Start chatting (type /quit to exit)");
            
            while (true) {
                String message = scanner.nextLine();
                
                if (message.equalsIgnoreCase("/quit")) {
                    break;
                }
                
                String fullMessage = username + ": " + message;
                byte[] buffer = fullMessage.getBytes();
                
                DatagramPacket packet = new DatagramPacket(
                    buffer,
                    buffer.length,
                    serverAddress,
                    SERVER_PORT
                );
                
                socket.send(packet);
            }
            
        } catch (IOException e) {
            System.err.println("Client error: " + e.getMessage());
        }
    }
}

⚖️ TCP vs UDP: So sánh thực tế

Test với nhiều clients

TCP Server:

# 100 concurrent clients
for i in {1..100}; do
    java ChatClient &
done

Kết quả:
- Tất cả clients connect thành công
- Messages đến đúng thứ tự
- Không mất messages
- CPU usage: Cao (100 threads)
- Memory: ~200MB

UDP Server:

# 100 concurrent clients
for i in {1..100}; do
    java UDPChatClient &
done

Kết quả:
- Tất cả clients connect thành công
- Một vài messages bị mất (~2-3%)
- Messages đôi khi sai thứ tự
- CPU usage: Thấp (1 thread)
- Memory: ~50MB

Khi nào dùng TCP vs UDP?

Dùng TCP khi:

  • ✅ Cần đảm bảo delivery 100%
  • ✅ Thứ tự messages quan trọng
  • ✅ Chat applications
  • ✅ File transfer
  • ✅ HTTP/HTTPS
  • ✅ Database connections

Dùng UDP khi:

  • ✅ Speed quan trọng hơn reliability
  • ✅ OK với mất một vài packets
  • ✅ Real-time gaming
  • ✅ Live streaming
  • ✅ VoIP (voice calls)
  • ✅ DNS queries

🎨 Cải tiến nâng cao

1. Thread Pool thay vì tạo thread mới

Vấn đề với new Thread():

  • Tạo thread tốn resources
  • Không giới hạn số threads
  • Server crash nếu quá nhiều clients

Giải pháp: ExecutorService

import java.util.concurrent.*;

public class PooledChatServer {
    private static final int PORT = 5000;
    private static final int THREAD_POOL_SIZE = 10;
    
    private static ExecutorService executorService = 
        Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                
                // Submit task to thread pool
                executorService.submit(
                    new ClientHandler(clientSocket)
                );
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

Lợi ích:

  • ✅ Reuse threads → Giảm overhead
  • ✅ Giới hạn threads → Tránh crash
  • ✅ Queue requests → Xử lý tuần tự khi full

2. NIO (Non-blocking I/O)

Java NIO cho phép 1 thread xử lý nhiều connections:

import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;

public class NIOChatServer {
    private static final int PORT = 5000;
    
    public static void main(String[] args) throws IOException {
        // Selector - theo dõi nhiều channels
        Selector selector = Selector.open();
        
        // Server channel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(PORT));
        serverChannel.configureBlocking(false);
        
        // Register với selector
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        System.out.println("NIO Chat Server started on port " + PORT);
        
        while (true) {
            // Block until có channel ready
            selector.select();
            
            // Process all ready channels
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                
                if (key.isAcceptable()) {
                    handleAccept(key, selector);
                } else if (key.isReadable()) {
                    handleRead(key);
                }
                
                iter.remove();
            }
        }
    }
    
    private static void handleAccept(
        SelectionKey key, 
        Selector selector
    ) throws IOException {
        ServerSocketChannel serverChannel = 
            (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(selector, SelectionKey.OP_READ);
        
        System.out.println("New client: " + 
            clientChannel.getRemoteAddress());
    }
    
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        int bytesRead = channel.read(buffer);
        
        if (bytesRead == -1) {
            // Client disconnected
            channel.close();
            return;
        }
        
        buffer.flip();
        String message = new String(buffer.array(), 0, bytesRead);
        System.out.println("Received: " + message);
        
        // Echo back
        buffer.rewind();
        channel.write(buffer);
    }
}

Ưu điểm NIO:

  • ✅ 1 thread xử lý hàng nghìn connections
  • ✅ Scalable cực tốt
  • ✅ Giống Node.js event loop

Nhược điểm:

  • ❌ Code phức tạp hơn
  • ❌ Khó debug
  • ❌ Learning curve cao

🛡️ Error Handling & Best Practices

1. Graceful Shutdown

public class GracefulChatServer {
    private static volatile boolean running = true;
    private static ServerSocket serverSocket;
    
    public static void main(String[] args) {
        // Shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutting down server...");
            running = false;
            
            try {
                if (serverSocket != null) {
                    serverSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }));
        
        try {
            serverSocket = new ServerSocket(5000);
            
            while (running) {
                Socket client = serverSocket.accept();
                // Handle client...
            }
        } catch (IOException e) {
            if (running) {
                e.printStackTrace();
            }
        }
    }
}

2. Timeout Configuration

// Socket timeout - tránh hang forever
socket.setSoTimeout(30000); // 30 seconds

// Connection timeout
Socket socket = new Socket();
socket.connect(
    new InetSocketAddress(host, port), 
    5000  // 5 seconds timeout
);

3. Resource Management

// Try-with-resources - auto close
try (
    ServerSocket server = new ServerSocket(PORT);
    Socket client = server.accept();
    BufferedReader in = new BufferedReader(
        new InputStreamReader(client.getInputStream())
    );
    PrintWriter out = new PrintWriter(
        client.getOutputStream(), true
    )
) {
    // Use resources...
} // Auto close in reverse order

4. Exception Handling

try {
    // Network code
} catch (BindException e) {
    System.err.println("Port already in use");
} catch (ConnectException e) {
    System.err.println("Cannot connect to server");
} catch (SocketTimeoutException e) {
    System.err.println("Connection timeout");
} catch (IOException e) {
    System.err.println("I/O error: " + e.getMessage());
}

🎓 Lessons Learned

Từ kinh nghiệm thực tế

1. Always use thread pools

new Thread(runnable).start()
✅ executorService.submit(runnable)

2. Set timeouts

❌ socket.accept() // Block forever
✅ socket.setSoTimeout(30000) // 30s timeout

3. Close resources properly

❌ Manual close (dễ quên)
✅ Try-with-resources

4. Handle disconnects gracefully

try {
    message = reader.readLine();
    if (message == null) {
        // Client disconnected
        break;
    }
} catch (IOException e) {
    // Connection error
    break;
}

5. Use StringBuilder cho strings lớn

❌ String result = "";
   for (...) result += data;  // Slow!

✅ StringBuilder sb = new StringBuilder();
   for (...) sb.append(data);  // Fast!

Common Pitfalls

Quên flush output stream

out.write("Hello");  // Có thể bị buffer
out.flush();         // Force gửi ngay

Blocking trên main thread

// UI sẽ freeze
String data = socket.readLine();

// Nên dùng background thread
new Thread(() -> {
    String data = socket.readLine();
}).start();

Không validate input

String message = reader.readLine();
if (message == null || message.isEmpty()) {
    return;
}

🚀 Tools & Testing

1. Netcat - Swiss Army Knife

# TCP Client
nc localhost 5000

# UDP Client
nc -u localhost 6000

# TCP Server
nc -l 5000

# UDP Server
nc -u -l 6000

2. Telnet

telnet localhost 5000

3. Wireshark

Capture packets để debug:

Filter: tcp.port == 5000
Filter: udp.port == 6000

4. JUnit Testing

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class ChatServerTest {
    private static ChatServer server;
    
    @BeforeAll
    static void startServer() {
        server = new ChatServer();
        new Thread(() -> server.start()).start();
        Thread.sleep(1000); // Wait for startup
    }
    
    @Test
    void testClientConnection() throws IOException {
        Socket socket = new Socket("localhost", 5000);
        assertTrue(socket.isConnected());
        socket.close();
    }
    
    @Test
    void testMessageEcho() throws IOException {
        Socket socket = new Socket("localhost", 5000);
        PrintWriter out = new PrintWriter(
            socket.getOutputStream(), true);
        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        
        out.println("Test message");
        String response = in.readLine();
        
        assertTrue(response.contains("Test message"));
        socket.close();
    }
}

🎯 Kết luận

Java Networking là một kỹ năng quan trọng mà mọi Java developer nên nắm vững. Qua bài này, các bạn đã học được:

TCP Sockets - Connection-oriented, reliable
UDP Sockets - Connectionless, fast
Multi-threading - Xử lý nhiều clients
Thread Pools - Resource management
NIO - Non-blocking I/O
Best Practices - Error handling, timeouts, resource management

Key Takeaways:

  1. TCP cho reliability, UDP cho speed
  2. Thread pools tốt hơn tạo threads mới
  3. Always set timeouts
  4. Try-with-resources cho resource management
  5. NIO cho high-performance servers

Trong thực tế:

  • Chat apps → TCP (WhatsApp, Telegram)
  • Gaming → UDP (Valorant, PUBG)
  • Web servers → NIO (Tomcat, Jetty)
  • Enterprise apps → Thread pools

Next Steps:

📚 Học thêm:

  • Java NIO.2 (AsynchronousSocketChannel)
  • Netty framework (production-ready)
  • WebSockets (Java WebSocket API)

🛠️ Practice:

  • Build file transfer app
  • Build multiplayer game server
  • Build HTTP server from scratch

Cảm ơn đã đọc! Ở bài tiếp theo, mình sẽ nói về Concurrency & Multithreading trong Java. Stay tuned! 🚀

#Java #Networking #Sockets #TCP #UDP #ChatServer