Giới thiệu tổng quan
Java NIO (New Input/Output) là một tập hợp các API được giới thiệu từ phiên bản JDK 1.4 trở lên, cung cấp cơ chế xử lý I/O không đồng bộ và hiệu quả hơn so với phương pháp truyền thống. NIO hoạt động dựa trên mô hình đồng bộ không chặn (synchronous non-blocking), trong đó luồng liên tục kiểm tra trạng thái sự kiện I/O, đồng thời có thể thực hiện các tác vụ khác khi chờ I/O hoàn tất.
Các thành phần chính
Bộ ba thành phần cốt lõi của NIO bao gồm:
- Channel: kênh truyền dữ liệu hai chiều
- Buffer: vùng đệm dữ liệu
- Selector: công cụ chọn lựa sự kiện I/O
Một số loại Channel phổ biến:
- FileChannel: đọc/ghi dữ liệu từ/tới tệp tin
- DatagramChannel: hỗ trợ giao tiếp UDP
- SocketChannel: hỗ trợ giao tiếp TCP qua mạng
- ServerSocketChannel: lắng nghe kết nối TCP mới
Nguyên lý hoạt động
Mô hình Reactor sử dụng một luồng đơn để mô phỏng đa luồng, cải thiện hiệu suất sử dụng tài nguyên và tăng khả năng xử lý đồng thời của hệ thống.
Trong Java NIO:
- Dữ liệu luôn di chuyển giữa Channel và Buffer
- Channel hỗ trợ cả đọc và ghi, khác với Stream truyền thống chỉ một chiều
- Selector cho phép một luồng duy nhất giám sát nhiều Channel
Chi tiết về Channel
Channel tương tự như Stream nhưng có những điểm khác biệt:
- Hỗ trợ đọc và ghi hai chiều
- Có thể thực hiện thao tác bất đồng bộ
- Dữ liệu luôn đi qua Buffer khi truyền nhận
Ví dụ minh họa sử dụng FileChannel:
RandomAccessFile inputFile = new RandomAccessFile("data/input.txt", "rw");
FileChannel dataChannel = inputFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = dataChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while(buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = dataChannel.read(buffer);
}
inputFile.close();
Chi tiết về Buffer
Các bước cơ bản khi sử dụng Buffer:
- Viết dữ liệu vào Buffer
- Gọi phương thức
flip()chuyển sang chế độ đọc - Đọc dữ liệu từ Buffer
- Gọi
clear()hoặccompact()xóa dữ liệu
Ví dụ sử dụng Buffer:
RandomAccessFile dataFile = new RandomAccessFile("data/source.txt", "rw");
FileChannel sourceChannel = dataFile.getChannel();
ByteBuffer workBuffer = ByteBuffer.allocate(2048);
int dataRead = sourceChannel.read(workBuffer);
while (dataRead != -1) {
workBuffer.flip();
while(workBuffer.hasRemaining()) {
System.out.print((char) workBuffer.get());
}
workBuffer.clear();
dataRead = sourceChannel.read(workBuffer);
}
dataFile.close();
Thông số Buffer
- Capacity: dung lượng tối đa của Buffer
- Position: vị trí hiện tại khi đọc/ghi
- Limit: giới hạn cuối cùng cho thao tác đọc/ghi
Các loại Buffer
- ByteBuffer, MappedByteBuffer
- CharBuffer, ShortBuffer, IntBuffer
- LongBuffer, FloatBuffer, DoubleBuffer
Scatter/Gather
Scatter (phân tán) và Gather (tập trung) là tính năng cho phép đọc dữ liệu vào nhiều Buffer hoặc ghi dữ liệu từ nhiều Buffer vào một Channel.
Scatter Read:
ByteBuffer headerBuffer = ByteBuffer.allocate(512);
ByteBuffer contentBuffer = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {headerBuffer, contentBuffer};
channel.read(bufferArray);
Gather Write:
ByteBuffer headerBuffer = ByteBuffer.allocate(512);
ByteBuffer contentBuffer = ByteBuffer.allocate(1024);
// Ghi dữ liệu vào các buffer
ByteBuffer[] bufferArray = {headerBuffer, contentBuffer};
channel.write(bufferArray);
Chuyển đổi giữa Channel
FileChannel hỗ trợ chuyển dữ liệu trực tiếp giữa các Channel:
Sử dụng transferFrom():
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel targetChannel = targetFile.getChannel();
long startPosition = 0;
long dataSize = sourceChannel.size();
targetChannel.transferFrom(sourceChannel, startPosition, dataSize);
Sử dụng transferTo():
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel targetChannel = targetFile.getChannel();
long startPosition = 0;
long dataSize = sourceChannel.size();
sourceChannel.transferTo(startPosition, dataSize, targetChannel);
Selector
Selector là thành phần cho phép một luồng duy nhất quản lý nhiều Channel, rất hữu ích trong việc xây dựng server có khả năng xử lý hàng ngàn kết nối đồng thời.
Tạo Selector
Selector eventSelector = Selector.open();
Đăng ký Channel với Selector
channel.configureBlocking(false);
SelectionKey registrationKey = channel.register(eventSelector, SelectionKey.OP_READ);
Các loại sự kiện:
- OP_CONNECT: kết nối đã sẵn sàng
- OP_ACCEPT: chấp nhận kết nối mới
- OP_READ: dữ liệu sẵn sàng để đọc
- OP_WRITE: kênh sẵn sàng để ghi
Xử lý sự kiện
while(true) {
int readyChannels = eventSelector.select();
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = eventSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// Xử lý kết nối mới
} else if(key.isConnectable()) {
// Xử lý kết nối hoàn tất
} else if(key.isReadable()) {
// Xử lý dữ liệu đọc
} else if(key.isWritable()) {
// Xử lý dữ liệu ghi
}
keyIterator.remove();
}
}
FileChannel
FileChannel luôn hoạt động ở chế độ chặn, không hỗ trợ chế độ không chặn.
Đọc dữ liệu
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(readBuffer);
Ghi dữ liệu
String content = "Dữ liệu cần ghi";
ByteBuffer writeBuffer = ByteBuffer.allocate(content.length());
writeBuffer.put(content.getBytes());
writeBuffer.flip();
fileChannel.write(writeBuffer);
Quản lý vị trí
long currentPosition = fileChannel.position();
fileChannel.position(currentPosition + 100);
SocketChannel
SocketChannel dùng để kết nối mạng TCP.
Kết nối tới máy chủ
SocketChannel networkChannel = SocketChannel.open();
networkChannel.connect(new InetSocketAddress("example.com", 80));
Chế độ không chặn
networkChannel.configureBlocking(false);
while(!networkChannel.finishConnect()) {
// Chờ kết nối hoàn tất
}
ServerSocketChannel
ServerSocketChannel dùng để lắng nghe kết nối đến.
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
while(true) {
SocketChannel clientChannel = serverChannel.accept();
if(clientChannel != null) {
// Xử lý kết nối client
}
}
DatagramChannel
DatagramChannel hỗ trợ giao tiếp UDP.
DatagramChannel udpChannel = DatagramChannel.open();
udpChannel.socket().bind(new InetSocketAddress(9999));
ByteBuffer receiveBuffer = ByteBuffer.allocate(48);
udpChannel.receive(receiveBuffer);
String message = "Tin nhắn UDP";
ByteBuffer sendBuffer = ByteBuffer.allocate(message.length());
sendBuffer.put(message.getBytes());
sendBuffer.flip();
udpChannel.send(sendBuffer, new InetSocketAddress("recipient", 80));
Pipe
Pipe tạo kết nối một chiều giữa hai luồng.
Pipe dataPipe = Pipe.open();
Pipe.SinkChannel sink = dataPipe.sink();
Pipe.SourceChannel source = dataPipe.source();
// Ghi dữ liệu vào pipe
String data = "Dữ liệu gửi qua pipe";
ByteBuffer writeBuffer = ByteBuffer.allocate(data.length());
writeBuffer.put(data.getBytes());
writeBuffer.flip();
sink.write(writeBuffer);
// Đọc dữ liệu từ pipe
ByteBuffer readBuffer = ByteBuffer.allocate(48);
source.read(readBuffer);