- IO Multiplexing
Trong BIO, mỗi yêu cầu từ客户端 đều được xử lý bởi một thread riêng, dẫn đến việc tăng số lượng thread và gây ra các vấn đề về hiệu suất. Vì vậy,(IO multiplexing) đã ra đời để cho phép một thread xử lý nhiều kết nối同時に. Cơ chế này trong Java NIO được thực hiện thông qua các API multiplexing của hệ điều hành như select, poll, và epoll.
1.1 Cơ chế select
Select sử dụng fd_set để giám sát các file descriptor. Mỗi lần gọi select, hệ điều hành sẽ kiểm tra các descriptor trong fd_set và trả về các descriptor đã sẵn sàng. Tuy nhiên, select có một số nhược điểm:
- Phải sao chép fd_set từ không gian người dùng sang không gian nhân mỗi lần gọi
- Hiệu suất kém khi số descriptor lớn
- Giới hạn số descriptor (1024 hoặc 2048)
| Cơ chế | select | poll | epoll |
|---|---|---|---|
| Cách giám sát | Linear scan | Linear scan | Event-driven |
| Số descriptor tối đa | Giới hạn | Không giới hạn | Không giới hạn |
1.2 Cơ chế poll
Poll cải thiện hơn select bằng cách không giới hạn số descriptor và sử dụng cấu trúc链表. Tuy nhiên, nó vẫn gặp các vấn đề tương tự như select khi các descriptor không hoạt động.
1.3 Cơ chế epoll
Epoll là một cải tiến lớn hơn. Nó sử dụng các callback để thông báo khi một descriptor sẵn sàng, giảm đáng kể số lần kiểm tra. Epoll cũng hỗ trợ cả level-triggered và edge-triggered modes.
- Lý thuyết NIO
BIO sử dụng thread同步 và bị tắc khi chờ IO. NIO, ngược lại, cho phép một thread giám sát nhiều kết nối và chỉ xử lý khi có sự kiện xảy ra.
2.1 Three Core Modules
- Channel: Quản lý giao tiếp giữa客户端 và server
- Buffer: Lưu trữ dữ liệu trung gian
- Selector: Giám sát các sự kiện trên các channel
2.2 Ví dụ ứng dụng NIO
Server NIO:
public class NioServer {
private static ServerSocketChannel serverSocketChannel;
private static Selector selector;
public static void main(String[] args) {
try {
init();
start();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void init() throws IOException {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
private static void start() throws IOException {
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
selector.selectedKeys().remove(key);
}
}
}
}
Client NIO:
public class NioClient {
private SocketChannel channel;
public static void main(String[] args) throws Exception {
NioClient client = new NioClient();
client.connect();
client.send("Hello Server");
client.receive();
}
private void connect() throws Exception {
channel = SocketChannel.open(new InetSocketAddress("localhost", theo dõi các sự kiện của các channel và chỉ xử lý khi có sự kiện xảy ra. Điều này giúp giảm thiểu việc tạo và hủy các thread, tăng hiệu suất xử lý.</p>
- Phân tích mã nguồn NIO
3.1 Khởi tạo server
Server được khởi tạo bằng cách tạo ServerSocketChannel và Selector. Mỗi channel được đăng ký với Selector để监听 các sự kiện.
3.2 selector.select() và các sự kiện
Phương thức select() gây tắc thread cho đến khi có sự kiện. Các SelectionKey được trả về và xử lý từng phần tử.
3.3 buffer và các phương thức
Buffer có các thuộc tính chính:
- capacity: Kích thước tối đa
- position: Vị trí hiện tại
- limit: Giới hạn đọc hoặc viết
- mark: Vị trí được đánh dấu
Các phương thức quan trọng:
- put(): Thêm dữ liệu vào buffer
- flip(): Chuyển sang chế độ đọc
- compact(): Compress dữ liệu chưa đọc
- Buffer và Channel
Channel là các đối tượng thực hiện giao tiếp IO. Buffer đóng vai trò trung gian lưu trữ dữ liệu. Cả hai đều là thành phần cốt lõi của NIO.
Lưu ý: Phải xóa các SelectionKey đã xử lý để tránh việc xử lý lại trong lần select tiếp theo.