+1

[JavaNet p3] Blocking Server

Bài viết được lấy từ https://truongphuoc.wordpress.com/2024/07/18/javanet-p3-blocking-server/

Bài này gồm hơn 2000 chữ và rất nhiều code, tin mình đi bạn không muốn đọc trên điện thoại đâu. Vì thế nếu đang đọc trên điện thoại thì bạn nên note lại, sau đó mở laptop hay ipad lên đọc sau nha.

Mở bài

Trong bài Java Networking, ta đã tìm hiểu về cách hai máy tính tạo kết nối TCP với nhau sử dụng Socket và ServerSocket.

Trong bài Java IO, ta đã tìm hiểu về Stream, Buffer, Reader và Writer. Ta cũng đã biết Reader, Writer có thể kết hợp được với Stream.

Note: Nếu chưa đọc hai bài trước thì các bạn đọc lại rồi hãy quay lại đây, việc bỏ qua có thể khiến việc đọc bài này trở thành ác mộng đó 😇

Trong bài viết này, ta sẽ sử dụng Java IO để đọc, ghi dữ liệu thông qua Socket.

Socket InputStream & Socket OutputStream

Socket cung cấp cho chúng ta InputStream và OutputStream để có thể thao tác với dữ liệu. Chúng ta có thể sử dụng mọi thứ trong Java IO để đọc ghi dữ liệu dựa trên InputStream và OuptutStream này

Socket socket;
// socket = new Socket();
// socket = serverSocket.accpet();

InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();

BufferedReader reader = new BufferedReader(new InputStreamReader(in));
PrintWriter writer = new PrintWriter(out);

writer.println("Hello");
writer.flush();

Đấy, chỉ thế thôi. Nếu bạn đã hiểu về Java Networking và Java IO thì tới đây mọi thứ đều quen thuộc rồi. Nếu bạn chưa thấy quen thuộc thì đọc và làm bài tập hai bài trước nha, có ích lắm đó.

Ứng dụng tính bình phương

Học phải đi đôi với hành phải không nào, giờ chúng ta hãy cùng làm một chương trình Console đơn giản nhé. Chương trình như sau:

Client sẽ cho người dùng nhập một số, Server sẽ trả về bình phương của số đó.

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

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        try {
            socket = new Socket();
            InetAddress localhost = InetAddress.getByName("localhost");
            InetSocketAddress inetSocketAddress = new InetSocketAddress(localhost, 8080);

            socket.connect(inetSocketAddress);
            System.out.println("Client info: " + socket);
            // Client info: Socket[addr=localhost/127.0.0.1,port=8080,localport=57884]

            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();

            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            PrintWriter writer = new PrintWriter(out);

            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));

            System.out.print("Enter a number (enter 0 to exit): ");
            String userInput = consoleReader.readLine();
            writer.println(userInput);
            writer.flush();

            String response = reader.readLine();
            System.out.println("Server response: " + response);

        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
}
import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket();
            InetAddress inetAddress = InetAddress.getByName("0.0.0.0");
            SocketAddress socketAddress = new InetSocketAddress(inetAddress, 8080);
            serverSocket.bind(socketAddress);

            Socket clientSocket = serverSocket.accept();
            System.out.println("Connected to client: " + clientSocket);
            // Connected to client: Socket[addr=/127.0.0.1,port=57884,localport=8080]

            InputStream in = clientSocket.getInputStream();
            OutputStream out = clientSocket.getOutputStream();

            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            PrintWriter writer = new PrintWriter(out);

            String clientInput = reader.readLine();
            int number = Integer.parseInt(clientInput);
            System.out.println("client input: " + clientInput);

            int square = number * number;
            writer.printf("%d square is %d", number, square);
            writer.println();
            writer.flush();

        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

Trong ví dụ trên, Client và Server mở một kết nối TCP với nhau, sau đó trao đổi thông tin. Mô hình đại loại thế này:

Code trên khá dài do mình paste cả vào trong này để các bạn dễ dàng copy về máy và chạy thử nha, các bạn nên đọc code chứ đừng lướt qua, làm mọi thứ chậm lại thì chúng ta mới hiểu kỹ được.

Bài tập về nhà: Tự viết lại đoạn code trên, có thể biến tấu theo ý hiểu của mình. Nếu bạn đã đọc và làm bài tập trong hai bài trước thì sẽ thấy được tác dụng của việc làm bài tập nha. Nếu bạn chưa đọc hay làm bài tập của hai bài trước thì các bạn nên dừng tại đây, quay lại và đi theo lộ trình nhé.

Nào cùng cải tiến

Server và Client ở trên là khá là đơn giản, thô sơ. Chúng kết nối với nhau, trao đổi 1 thông tin và đóng kết nối.

Để dễ dàng hình dung thì chúng ta sẽ coi Server là một nhà hàng, nhà hàng này chỉ có một anh nhân viên phục vụ (một thread đang chạy), còn Client là Khách hàng, cô này tới nhà hàng để ăn tối.

Với cách xử lý trong ví dụ ở trên thì anh nhân viên của nhà hàng chỉ có thể phục vụ cho một cô khách hàng 🤫, mà cô này chỉ gọi được 1 món, sau khi bán hàng cho 1 người thì nhà hàng đóng cửa (Bạn chạy Server, bạn chạy Client, Client gọi Server và gửi 1 (và chỉ 1) phép tính, Server phục vụ cho Client đúng chỉ 1 phép tính đó (và chỉ 1 client đó), rồi hai bên đóng kết nối và tắt ứng dụng).

Chúng ta không muốn như thế chút nào phải không ạ, chúng ta - những chủ nhà hàng, chúng ta là tư bản mà, muốn nhân viên phải phục vụ được càng nhiều người càng tốt, mà một người phải gọi được nhiều món lại càng tốt. Thế nên ta cần phải cải tiến ba việc:

  • Thứ nhất là nhà hàng phải mở cửa liên tục
  • Thứ hai là một người có thể gọi được nhiều món
  • Thứ ba là một nhân viên phục vụ được nhiều người.

Let's do it.

Nhà hàng mở cửa liên tục

Để giải quyết vấn đề nhà hàng mở cửa liên tục, chúng ta phải chắc rằng sau khi phục vụ xong 1 người, nhà hàng sẽ không đóng cửa ngay (Server tắt ngay). Cách giải quyết đơn giản là chúng ta thêm vòng lặp, để khi nhà hàng phục vụ xong một người, nó sẽ chuyển sang phục vụ người khác.

public class Server {
    public static void main(String[] args) throws IOException {
		...
		while (true) { // thêm while vào đây nè
		    Socket clientSocket = serverSocket.accept();
		    ...
		    writer.println();
		    writer.flush();
		}
		...
    }
}

Đến đây lại nảy sinh một chuyện, rõ là anh chàng phục vụ nhà hàng này được đào tạo từ lớp Java IO. Lớp này có một tật là khi đã phục vụ một người, anh ta phải phục vụ xong người đó thì mới chuyển sang phục vụ người khác. Điều này là do cơ chế Blocking của Java IO, khi đọc/ghi dữ liệu, Java IO sẽ chờ cho tới khi có dữ liệu để đọc hoặc ghi thì mới đi tiếp (Việc này quan trọng lắm nha, các bạn nhớ kỹ giúp mình nhá)

Điều này tạo ra một cơ chế hàng đợi cho cho connection, vấn đề càng lớn khi có một vị khách order đồ lâu, dẫn tới những người ở sau phải đợi rất lâu.

Dưới đây là ví dụ một ông khách mất 10 giây để order

public class Client {
    public static void main(String[] args) throws Exception {
				...
        String userInput = consoleReader.readLine();
        writer.println(userInput);
        Thread.sleep(10_000); // ngủ 10 giây nhé
        writer.flush();
        ...
    }
}

Để giải quyết vấn đề này chúng ta có thể thêm người phục vụ. Lý tưởng nhất là cứ mỗi khách vào nhà hàng, sẽ có một người phục vụ khách đó, như thế sẽ không ai phải đợi ai nữa.

public class Server {
    public static void main(String[] args) throws IOException {
		...
		while (true) {
		      Socket clientSocket = serverSocket.accept();
		      new Thread(() -> { // mỗi ông đến là có một người phục vụ riêng :)))
				...
			      writer.println();
			      writer.flush();
		      }.start();
		}
		...
    }
}

Cứ mỗi connection tới, chúng ta sẽ tạo ra một Thread mới để xử lý. Việc này sẽ giải quyết được vấn đề block của thread khiến các client khác không kết nối được.

Khách hàng gọi được nhiều món

Vấn đề này thì dễ dàng giải quyết hơn, chúng ta có thể sử dụng vòng lặp. Nhưng nếu sử dụng vòng lặp thì "lại" nảy sinh một vấn đề khác, lúc nào thì khách hàng đã gọi hết món mong muốn? (Hay nói cách khác lúc nào thì đóng kết nối?) Với bài toán bình phương trên chúng ta có thể quy ước là nếu User mà nhập số 0 thì là gọi hết món.

Tổng kết lại chúng ta có code Client và Server như sau, code hơi dài, mình cho vào đây luôn để các bạn tiện copy vào máy chạy để hiểu thêm nha.

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

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        try {
            socket = new Socket();
            InetAddress localhost = InetAddress.getByName("localhost");
            InetSocketAddress inetSocketAddress = new InetSocketAddress(localhost, 8080);

            socket.connect(inetSocketAddress);
            System.out.println("Client info: " + socket);
            // Client info: Socket[addr=localhost/127.0.0.1,port=8080,localport=57884]

            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();

            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            PrintWriter writer = new PrintWriter(out);

            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));

            while (true) { // cho phép gọi nhiều món
                System.out.print("Enter a number (0 to exit): ");
                String userInputStr = consoleReader.readLine();
                if (userInputStr.equals("0")) {
                    System.out.println("Close connection");
                    break;
                }
                writer.println(userInputStr);
                writer.flush();

                String response = reader.readLine();
                System.out.println("Server response: " + response);
            }

        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
}
import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) throws IOException {
		    System.out.println("Thread: " + Thread.currentThread().getName());
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket();
            InetAddress inetAddress = InetAddress.getByName("0.0.0.0");
            SocketAddress socketAddress = new InetSocketAddress(inetAddress, 8080);
            serverSocket.bind(socketAddress);
            while (true) { // phục vụ được nhiều khách
                Socket clientSocket = serverSocket.accept();

                new Thread(() -> { // mỗi khách một người phục vụ riêng
                    try {
		                System.out.println("Thread: " + Thread.currentThread().getName());
                        System.out.println("Connected to client: " + clientSocket);
                        // Connected to client: Socket[addr=/127.0.0.1,port=57884,localport=8080]

                        InputStream in = clientSocket.getInputStream();
                        OutputStream out = clientSocket.getOutputStream();

                        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        PrintWriter writer = new PrintWriter(out);

                        String clientInput;
                        while ((clientInput = reader.readLine()) != null) { // Cho phép gọi nhiều món
                            int number = Integer.parseInt(clientInput);
                            System.out.println("client input: " + clientInput);
                            if (number == 0) {
                                System.out.println("Client requested to close connection.");
                                break;
                            }

                            int square = number * number;
                            writer.printf("%d square is %d", number, square);
                            writer.println();
                            writer.flush();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

Nhân viên phục vụ được nhiều người

Để làm việc này thì chúng ta có thể sử dụng Pool thay vì cứ mỗi connection tới ta tạo ra một thread mới. Còn một cách nữa, cách này sẽ nói ở bài tiếp theo, bí mật bí mật.

Code đoạn pool này sẽ là bài tập về nhà của các bạn nha.

Bài tập về nhà:

  • Bài 1: Các bạn hãy copy thành nhiều Client khác nhau, chạy thử với Thread.sleep() để thấy sự khác biệt nhé, chú ý các thông tin Thread được tạo ra mỗi connection. Các bạn nên làm bài tập về nhà, các bạn sẽ hiểu thêm rất nhiều thay vì chỉ đọc thôi, học phải đi đôi với hành nè.
  • Bài 2: Hãy tự viết Server và Client cho riêng mình nhé. Trong đó Server thay vì luôn tạo Thread mới sẽ sử dụng pool thay thế.

Sự ưu - nhược điểm của cơ chế chờ - blocking

Đến đây là bạn đã hiểu về Blocking Server rồi phải không nào? Tiếp theo chúng ta hãy cùng tìm hiểu về sự ưu - nhược của blocking nhé

Ưu

  • Dễ hiểu: cơ chế này khá dễ hiểu, một anh bồi bàn phục vụ hết một người, sau đó chuyển sang người khác, cứ thế mà làm.
  • Dễ bảo trì, dễ code: Cơ chế của Java IO khá là đơn giản, straightforward nên dễ dàng bảo trì và code cũng dễ nữa.

Nhược

  • Tốn tài nguyên: Dễ thấy cứ mỗi một khách hàng tới thì lại cần một phục vụ mới ra đón khách. Quả là tốn tiền thuê nhân viên.
  • Khó mở rộng: Tại vì sử dụng mỗi thread một kết nối tới cho nên khó lòng mở rộng nếu có nhiều connection. Ví dụ nếu lượng connection tăng là 1 triệu thì phải làm thế nào?

Kết luận

Blocking Server khá là dễ hiểu, dễ code và bảo trì. Nó là sự kết hợp của Java Networking và Java IO để có thể đọc ghi dữ liệu trong Socket. Chúng ta cũng đã đi qua về việc cải tiến Server để có thể có một Server tốt hơn, phục vụ được nhiều người cùng một lúc và có thể duy trì kết nối. Hi vọng sau bài này các bạn có một góc nhìn mới về Server.

P/S: Tại sao mình viết những bài thế này? Tại vì khi tìm hiểu trên mạng về Server, WebServer,... thì ra những bài viết rất chung chung, nó không làm thỏa mãn được mình. Nên mình tìm hiểu và viết lại trong bài này hi vọng các bạn hiểu hơn về Server, chúng ta có thể code được một Server đơn giản, hiểu được cơ chế của nó, nó không có gì quá cao siêu, ngoài tầm với và quá khó hiểu đúng không.

Link bài viết gốc https://truongphuoc.wordpress.com/2024/07/18/javanet-p3-blocking-server/


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.