Các thành phần cốt lõi trong lập trình Java NIO

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:

  1. Viết dữ liệu vào Buffer
  2. Gọi phương thức flip() chuyển sang chế độ đọc
  3. Đọc dữ liệu từ Buffer
  4. Gọi clear() hoặc compact() 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);

Thẻ: java-nio Channel buffer selector filechannel

Đăng vào ngày 12 tháng 7 lúc 00:37