[JavaNet p4] Java NIO & Non-Blocking Server
Bài viết được lấy từ https://truongphuoc.wordpress.com/2024/07/18/javanet-p4-java-nio-non-blocking-server/
Bài viết này gồm hơn 4000 chữ và rất rất nhiều code, bạn sẽ không muốn đọc đống chữ này trên điện thoại đâu, nếu bạn đang đọc trên điện thoại thì hãy ghim lại, sau đó mở máy tính lên và đọc nha. Hãy bảo vệ đôi mắt của bạn.
Mở bài
Sử dụng Java Networking và Java IO để tạo một Server có nhiều bất cập, một trong số những bất cập lớn là vì cơ chế blocking của Java IO. Trong bài này chúng ta cùng tìm hiểu Java NIO và cách nó giải quyết vấn đề đó nhé.
Java New Input/Output (Java NIO)
Java NIO là viết tắt của Java New Input/Output hay Java New IO (Chứ không phải là Java Non-blocking IO nha). Nó được sinh ra như một thay thế của Java IO và Java Networking. Thằng Java NIO này có một vài khái niệm mới (và cả cũ) là Channel, Buffer, Selector. Ta vọc từng thằng một để xem nó khác biệt với Java IO thế nào nha.
Channel & Buffer
Nếu trong Java IO, mọi thứ được bắt đầu từ Stream và kết thúc tại Stream thì trong Java NIO, mọi thứ được bắt đầu từ Channel và kết thúc tại Channel 😂 (Như phim). Giờ hãy cùng quay lại truyền thống về việc lấy ví dụ trong thực tế cho những thứ trừu tượng nào. Lần này chúng ta sẽ có cách tiếp cận khác so với dòng suối - stream trong Java IO. Hãy tưởng tượng chương trình của chúng ta là một nhà máy chế biến gỗ. Nhà máy này được đặt ở đồng bằng, nơi không có những con suối. Để tiện cho việc chế biến và sản xuất gỗ, tất cả các cơ sở gỗ đều được đặt cạnh một nguồn nước, có thể là ao, hồ, sông,...
Để có gỗ chế biến nhà máy phải lấy từ nhiều nguồn khác nhau, với mỗi nguồn gỗ nhà máy phải đào một con kênh từ nhà máy tới nguồn để có đường lấy gỗ mà chế biến. Tất cả các nguồn, kho dự trữ gỗ đều đặt cạnh nguồn nước, nên rất thuận tiện. Tại vì ở đồng bằng, không thể có dòng chảy từ trên xuống như suối nên để vận chuyển gỗ, chúng ta phải sử dụng thuyền. Mỗi chiếc thuyền có thể chở được 1,2,..10... cây gỗ tùy chúng ta cài đặt thôi.
Các bạn hiểu được bối cảnh rồi phải không nào, giờ hãy xem nhà máy gỗ là ứng dụng của ta, gỗ thô chính là dữ liệu. Nguồn của chúng ta có thể là File, Socket, ServerSocket,,... Khi ta muốn lấy dữ liệu từ một file, ta mở một con kênh tới file = FileChannel, khi ta muốn lấy dữ liệu từ một Socket, ta mở một con kênh tới Socket = SocketChannel. Cũng dễ hiểu phải không ta.
Còn thiếu một thứ nữa: con thuyền. Khi ta đã mở một con kênh tới file, ta sẽ sử dụng một con thuyền là Buffer để vận chuyển dữ liệu.
Thế là chúng ta đã có khái niệm về Channel và Buffer trong Java NIO rồi. Lý thuyết tới đây là đủ rồi, hãy cùng thực hành với ví dụ đọc/ghi file quen thuộc nào
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class CopyBytes {
public static void main(String[] args) throws IOException {
Path source1 = Paths.get("source1.txt"); // vị trí nguồn 1
Path source2 = Paths.get("source2.txt"); // vị trí nguồn 2
FileChannel channel1 = null;
FileChannel channel2 = null;
try {
channel1 = FileChannel.open(source1, StandardOpenOption.READ); // mở một kênh tới nguồn 1 để đọc
channel2 = FileChannel.open(source2, StandardOpenOption.WRITE); // mở một kênh tới nguồn hai để ghi
ByteBuffer buffer = ByteBuffer.allocate(3); // con thuyền của chúng ta chứa được 3 cây gỗ
while (channel1.read(buffer) > 0) { // đọc dữ liệu tại source1
buffer.flip(); // chuyển từ chế độ đọc sang ghi
channel2.write(buffer); // ghi dữ liệu vào source2
buffer.clear(); // chuyển chế độ ghi sang đọc, xóa hết dữ liệu thừa đi
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (channel1 != null) {
channel1.close();
}
if (channel2 != null) {
channel2.close();
}
}
}
}
Các bạn đọc code thấy vài chỗ khó hiểu như flip, clear ư? Hãy đọc phần dưới rồi quay lại đọc để hiểu nhé.
Buffer
Chúng ta cùng tìm hiểu sâu thêm một chút về buffer nha. Buffer chính là một mảng byte giống như Buffered trong Java IO. Trong buffer có ba thông số quan trọng cần quan tâm: position, limit và capacity. Buffer có hai trạng thái là đọc và ghi, ứng với mỗi trạng thái thì các thông số lại khác nhau.
Capacity
Capacity là sức chứa của một buffer, nó luôn cố định. Khi chúng ta khởi tạo buffer ta phải thông báo sức chứa của nó là bao nhiêu.
ByteBuffer byteBuffer = ByteBuffer.allocate(8); // khởi tạo một byte buffer với sức chứa là 8 byte
Limit
Limit là giới hạn để đọc, ghi.
Khi chúng ta ở chế độ ghi, thì Limit = Capacity, chúng ta có thể ghi hết mảng phải không nào.
Khi chúng ta ở chế độ đọc, thì Limit = số lượng phần tử được ghi, chúng ta ghi vào buffer 3 phần từ thì chúng ta có thể đọc tối đa được 3 phần tử đúng không.
Position
Đây là vị trí đọc/ghi tiếp theo.
Thế là đã hết phần thông số, tiếp theo là các method quan trọng trong Buffer nhé.
remaining()
remaining = limit - position
hasRemaining()
hasRemaining = position < limit
flip()
Để chuyển từ chế độ ghi sang chế độ đọc
rewind()
Rewind sẽ set position về vị trí đầu tiên, nếu bạn muốn đọc lại buffer từ đầu, bạn có thể sử dụng nó.
clear() và compact()
Khi bạn đã đọc xong dữ liệu từ buffer và bạn muốn xóa hết dữ liệu đi và ghi lại, bạn có thể sử dụng clear(). Bản thân clear() không xóa dữ liệu trong buffer đi, nó sẽ set lại position = 0 và limit = capacity, nên về cơ bản thì bạn sẽ ghi dữ liệu từ đầu.
Khi bạn đang đọc dở dữ liệu và muốn ghi dữ liệu mới, nhưng bạn lại không muốn xóa dữ liệu chưa đọc đi, bạn có thể sử dụng compact(). Method compact() sẽ copy dữ liệu chưa đọc lên đầu buffer, sau đó set position ngay sau nó.
mark() và reset()
Bạn có thể đánh dấu vị trí hiện tại và sau đó trở về vị trí đó bằng hai method mark() và reset()
buffer.mark();
buffer.get();
buffer.get();
buffer.reset(); //set position back to mark.
Bài tập về nhà:
- Bài 1: Hãy vẽ tay buffer ra giấy và bắt đầu thực hiện đọc, ghi, flip, clear, compact,... thủ công, để xem cách buffer hoạt động thế nào. Việc làm bài tập này là cần thiết để bạn hiểu về buffer nha.
- Bài 2: Hãy viết lại đoạn code copy bytes ở trên. Việc bạn tự viết lại làm bạn phải chú ý tới từng dòng, từng câu lệnh, các bạn sẽ hiểu rõ hơn là cứ ậm ừ kêu đã hiểu nhá.
Xử lý văn bản với buffer
Có nhiều cách để xử lý văn bản với buffer, mình thích cách sử dụng String hơn, nó đơn giản và cũng rất quen thuộc. Để ghi/ghi văn bản với ByteBuffer như sau:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// ghi
String writeData = new String("This is input data".getBytes(), StandardCharsets.UTF_8);
byteBuffer.put(writeData .getBytes());
byteBuffer.flip();
// đọc
String readData = new String(byteBuffer.array(), StandardCharsets.UTF_8).trim();
System.out.println(readData );
NIO Server
Chúng ta vừa tìm hiểu về nio rồi, giờ hãy áp dụng để làm Server nào. Trong Java Networking, chúng ta có Socket và ServerSocket là Endpoint. Trong Java NIO, ta coi Socket và SocketServer là nguồn dữ liệu, tương ứng nguồn dữ liệu này ta có SocketChannel và ServerSocketChannel. Chúng ta làm lại ứng dụng tính bình phương của một số sử dụng Java NIO nha.
import java.io.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
InetAddress localhost = InetAddress.getByName("localhost");
InetSocketAddress inetSocketAddress = new InetSocketAddress(localhost, 8080);
socketChannel.connect(inetSocketAddress);
System.out.println("Client info: " + socketChannel);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("Enter a number (0 to exit): ");
String userInputStr = consoleReader.readLine();
if (userInputStr.equals("0")) {
System.out.println("Close connection");
break;
}
byteBuffer.put(userInputStr.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear(); // xóa hết dữ liệu còn lại đi, chuyển sang chế độ đọc
socketChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println("Server response: " + new String(byteBuffer.array(), 0, byteBuffer.limit()).trim());
byteBuffer.clear();
}
} finally {
if (socketChannel != null) {
socketChannel.close();
}
}
}
}
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Server {
public static void main(String[] args) throws IOException {
System.out.println("Thread: " + Thread.currentThread().getName());
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = ServerSocketChannel.open();
InetAddress inetAddress = InetAddress.getByName("0.0.0.0");
SocketAddress socketAddress = new InetSocketAddress(inetAddress, 8080);
serverSocketChannel.bind(socketAddress);
while (true) {
SocketChannel clientSocketChannel = serverSocketChannel.accept();
new Thread(() -> {
try {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Connected to client: " + clientSocketChannel);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (clientSocketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
String clientInput = new String(byteBuffer.array(), 0, byteBuffer.limit()).trim();
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;
String response = String.format("%d square is %d\\r\\n", number, square);
byteBuffer.clear();
byteBuffer.put(response.getBytes());
byteBuffer.flip();
clientSocketChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
} finally {
if (serverSocketChannel != null) {
serverSocketChannel.close();
}
}
}
}
Code hơi dài để các bạn có thể copy code và chạy nha.
Nếu các bạn đã làm bài tập về nhà trong bài Blocking Server rồi thì sẽ thấy là cấu trúc code cũng chẳng thay đổi gì nhiều, vẫn phục vụ được nhiều client, vẫn phải chờ, vẫn phải tạo mới thread khi có connection mới tới. Phải chăng có sự thay đổi là đổi từ Java IO sang Java NIO mà thôi. (Việc làm bài tập về nhà rất quan trọng nha).
Tối ưu Server
Các bạn còn nhớ ví dụ về nhà hàng và người phục vụ chứ. Tại vì nhân viên phục vụ của nhà hàng phải phục vụ xong một khác hàng thì mới chuyển sang phục vụ khách khác, dẫn tới chúng ta phải thuê rất nhiều người phục vụ (Tạo một thread mới mỗi khi có connection).
Cách làm trên khiến rất tiêu tốn tài nguyên. Chúng ta cùng tính thử chi phí nhé: Theo cài đặt mặc định thì để tạo một thread với 32bit JVM thì sẽ mất 320KB bộ nhớ RAM, còn nếu bạn sử dụng 64bit JVM thì sẽ mất 1024KB=1MB bộ nhớ RAM. Nếu ứng dụng của chúng ta ngon nghẻ, có nhiều khách ghé thăm thì sẽ trở thành một bài toán khó. Ví dụ ứng dụng của chúng ta có 1_000_000 người ghé thăm trong một lúc thì chúng ta sẽ phải có RAM khoảng 1TB thì mới xử lý được.
Có một cách để giải quyết vấn đề này là sử dụng Pool để quản lý Thread. Việc này để tránh tràn bộ nhớ vì tạo quá nhiều Thread. Nhưng nó cũng có vấn đề là nếu số lượng pending connection quá nhiều thì sẽ hết số lượng Thread để phục vụ khách mới (Nghĩ tới việc Pool size = 100 và có 100 ông khác gọi món rất lâu).
Để giải quyết triệt để vấn đề trên, chúng ta phải tạo ra một cơ chế không chờ đợi, nơi mà một nhân viên có thể phục vụ được nhiều khách mà không chờ đợi khách nào cả. Trong thực tế chúng ta làm thế nào? Thực tế khi đến nhà hàng, chúng ta sẽ ngồi vào bàn ăn, xem menu. Nếu chúng ta có nhu cầu gọi món thì chúng ta sẽ gọi nhân viên. Còn trên góc nhìn của nhân viên, họ sẽ chờ ở một nơi nào đó, khi có khách hàng gọi họ thì họ mới tới. Và tất nhiên nhân viên sẽ không phục vụ xong một khách mới phục vụ khách khác, họ phục vụ tất cả, hay nói cách khách nhân viên không chờ đợi (Nghĩ tới việc nhân viên kêu bạn đến khi nào sẵn sàng gọi món thì gọi lại họ, chứ họ không cứ đứng chờ mãi).
Để so sánh thì bạn hãy nhớ rằng, nhân viên trong Java IO, bạn đến nhà hàng và nhân viên phục vụ bàn của bạn, nếu bạn chưa gọi món thì nhân viên sẽ cứ đứng đó, chờ bạn gọi. Họ sẽ chờ bạn ăn xong và ra về thì mới phục vụ khách hàng khác. Cho dù có một khách mới vào nhà hàng thì nhân viên không quan tâm, thế nên khách mới phải chờ.
Java NIO cung cấp cho chúng ta một cơ chế không chờ đợi gọi là non-blocking.
Non-blocking IO
Trong Java NIO, không phải tất cả các implementation của Channel đều hỗ trợ non-blocking IO. FileChannel không hỗ trợ non-blocking IO, nhưng có hai implementation rất quan trọng là ServerSocketChannel và SocketChannel có hỗ trợ non-blocking IO.
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configBlocking(false); // setup non-blocking for Socket channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configBlocking(false); // setup non-blocking for Server socket channel
Đọc/ghi dữ liệu trong non-blocking
Trong non-blocking, khi chúng ta đọc hoặc ghi dữ liệu, method read(), write() sẽ trả về ngay, có nghĩa là nó không bị block lại, nếu không có dữ liệu gì để đọc/ghi, method sẽ trả về giá trị 0.
// blocking
Socket socket = new Socket();
InputStream input = socket.getInputStream();
int inputData = input.read(); // Thread sẽ chờ tại đây cho tới khi có dữ liệu để đọc
System.out.println(inputData);
// non-blocking
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configBlocking(false); // mặc định thì socketChannel là blocking nên ta phải set lại
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffere); // Thread sẽ không chờ, method trả về 0 nếu không có dữ liệu để đọc
System.out.println(read);
Bài tập về nhà: thử chạy code để thấy sự khác biệt giữa blocking và non-blocking. Việc làm bài tập này giúp bạn thấy được sự khác biệt giữa blocking và non-blocking.
Selector
Tiếp tục quay trở lại câu chuyện nhà hàng. Trong môi trường blocking - Java IO thì khách hàng không cần gọi nhân viên, tại nhân viên ở ngay đó luôn luôn chờ khách hàng rồi. Còn trong môi trường non-blocking thì không như thế, nhân viên có lẽ đang đứng đâu đó, hoặc nhân viên đang bận phục vụ khách hàng khác. Để thuận tiện thì nhiều nhà hàng có đặt nút chuông, hoặc một cái ipad tại bàn ăn, khi khách hàng muốn gọi bồi bàn có thể bấm nút chuông hoặc gọi trên ipad. Nhân viên sẽ dựa trên tín hiệu và đến phục vụ.
Java NIO cung cấp cho chúng ta một Class để làm việc đó: Selector. Nó là Class dùng để thu thập những lời yêu cầu của khách hàng để nhân viên có thể phục vụ những yêu cầu đó.
Để làm rõ hơn nhiệm vụ của Selector, chúng ta hãy chở lại ví dụ nhà máy gỗ. Nhà máy gỗ của chúng ta có rất nhiều kênh để vận chuyển gỗ. Để quản lý hiệu quả các kênh này thì sẽ có một bộ phận giám sát, thu thập. Nếu một kênh muốn vận chuyển gỗ đi hoặc tới nhà máy, nó phải đăng ký với bộ phận này. Đây là bộ phận giám sát kênh, sau khi kênh đã đăng ký, cứ mỗi lần kênh vận chuyển hàng tới thì bộ giám sát sẽ biết và báo cho các bộ phận liên quan để làm việc với kênh.
SelectionKey
Kênh có thể đăng ký những loại hành động mong muốn giám sát, hay đăng ký với Selector những hoạt động của kênh mà Selector cần quan tâm như sau:
- Accept: Kênh sẵn sàng chấp nhận kết nối. (SelectionKey.OP_ACCEPT)
- Connect: Kênh sẵn sàng hoàn thành kết nối. (SelectionKey.OP_CONNECT)
- Read: Kênh sẵn sàng cho việc đọc dữ liệu. (SelectionKey.OP_READ)
- Write: Kênh sẵn sàng cho việc ghi dữ liệu. (SelectionKey.OP_WRITE)
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configBlocking(false);
// serverSocketChannel đăng ký với Selector để quản lý sự kiện chấp nhận kết nối của nó
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configBlocking(false);
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
// socketChannel nói với selector rằng cần quan tâm tới các hành động đọc và ghi của nó
socketChannel.register(selector, key);
Lưu ý quan trọng: Selector chỉ làm việc với non-blocking channel, có nghĩa là channel phải chuyển sang chế độ non-blocking trước khi đăng ký với Selector.
Thực hiện giám sát
Để theo dõi các sự kiện trong các kênh đã đăng ký, Selector cung cấp method select() . Method này là một blocking method, có nghĩa là nó sẽ chờ cho tới khi có hoạt động mà nó quan tâm đã sẵn sàng. Ngoài ra chúng ta có một non-blocking method để làm chuyện này là selectNow() . Cả hai method select() và selectNow() đều trả về giá trị int là số lượng Channel đã sẵn sàng để thực hiện hoạt động nó đã đăng ký với Selector.
int selected = selector.select(); // blocking method
int selected = selector.selectNow(); // non-blocking method
SelectionKey
Sau khi Selector phát hiện ra có Channel đã sẵn sàng hoạt động mà nó quan tâm, ta có thể lấy danh sách Channel đó ra để xử lý. Selector cung cấp cho chúng ta method selectedKeys() , nó trả về một danh sách các SelectionKey đại diện cho các Channel của chúng ta.
...
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey selectionKey = keyIterator.next();
if (selectionKey.isAcceptable()) {
// có thể thực hiện hành động accept tại đây
}
if (selectionKey.isConnectable()) {
// có thể thực hiện hành động trong quá trình kết nối tại đây
}
if (selectionKey.isReadable()) {
// có thể thực hiện hành động đọc tại đây
}
if (selectionKey.isWritable()) {
// có thể thực hiện hành động ghi tại đây
}
keyIterator.remove(); // xóa selectionKey sau khi đã xử lý
}
Non-blocking Server
Cuối cùng hãy ghép tất cả mảnh ghép trên lại, để tạo một non-blocking server nào
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Server {
public static void main(String[] args) throws IOException {
System.out.println("Thread: " + Thread.currentThread().getName());
InetAddress inetAddress = InetAddress.getByName("0.0.0.0");
SocketAddress socketAddress = new InetSocketAddress(inetAddress, 8080);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(socketAddress);
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server is running on port 8080");
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
}
keyIterator.remove(); // xóa key sau khi đã xử lý
}
}
}
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
System.out.println("Thread: " + Thread.currentThread().getName());
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// đăng ký client socket channel với selector để giám sát hành động đọc của nó
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("accepted " + serverSocketChannel);
}
private static void handleRead(SelectionKey key) throws IOException {
System.out.println("Thread: " + Thread.currentThread().getName());
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read;
while ((read = clientSocketChannel.read(byteBuffer)) != -1) {
if (read == 0) {
break;
}
byteBuffer.flip();
String clientInput = new String(byteBuffer.array(), 0, byteBuffer.limit()).trim();
int number = Integer.parseInt(clientInput);
System.out.println("client input: " + clientInput);
if (number == 0) {
System.out.println("Client requested to close connection.");
clientSocketChannel.close();
break;
}
int square = number * number;
String response = String.format("%d square is %d\\r\\n", number, square);
byteBuffer.clear();
byteBuffer.put(response.getBytes());
byteBuffer.flip();
clientSocketChannel.write(byteBuffer);
byteBuffer.clear();
}
}
}
Code hơi dài, mình cho vào đây để các bạn có thể dễ dàng copy code và chay nha.
Bài tập về nhà: Các bạn hãy chạy Server này với 2 client, quan sát số thông tin thread và rút ra kết luận nha. Lý tưởng là các bạn sửa lại đoạn code ở trên đầu thành đoạn code hỗ trợ non-blocking. Tin mình đi, nếu bạn sửa được thì bạn cũng sẽ hiểu rất rõ về non-blocking server rồi.
Ưu - nhược của non-blocking Server
Ưu
- Tiết kiệm tài nguyên: Việc ta chỉ sử dụng một Thread để xử lý được tất cả request dẫn tới ta không cần phải tốn tài nguyên để tạo các thread mới. Nghĩ tới việc nhà hàng chỉ cần một người phục vụ là phục vụ được hết rồi thì còn gì bằng =)))
- Có khả năng mở rộng: Nếu chúng ta thấy anh phục vụ của mình quá tải, ta sẽ thuê thêm một anh nữa bằng việc tạo Thread mới, rồi hai anh này chia sẻ việc với nhau. Điều này giúp nhà hàng chúng ta có khả năng mở rộng.
Nhược
- Khó triển khai code: bạn thấy đấy, một ông phục vụ cho nhiều người thì sẽ khó khăn để triển khai hơn là mỗi ông phục vụ cho một người phải không nào.
Kết luận
Thế là chúng ta đã xong về Java NIO và Non-blocking IO. Điểu quan trọng cần lưu ý là với blocking IO ta phải tạo nhiều Thread để xử lý kết nối, còn với non-blocking IO, ta chỉ cần 1 Thread là đã xử lý được rồi. Việc chỉ sử dụng một Thread giúp chúng ta rất nhiều trong việc mở rộng hệ thống (nhớ tới 1TB RAM chứ).
Link bài viết gốc https://truongphuoc.wordpress.com/2024/07/18/javanet-p4-java-nio-non-blocking-server/
All Rights Reserved