Java NIO理解
理解好Java NIO前提要熟悉IO模型,IO复用(以epoll为例)、非阻塞IO的基础知识。java.nio抽象出来一些类或接口,底层实际是对epoll等系统调用以及socket对象进行的封装。这里先做个nio和epoll在概念和API上的对比,再做个阻塞IO模式下起服务和nio模式下起服务的对比。
nio与epoll对比
IO复用以epoll举例
概念对比
java.nio | epoll | 备注 |
---|---|---|
Channel | fd | channel可以理解是对socket、fd的封装 |
Buffers | - | nio对承接读写数据的buffer的封装 |
Selector | epoll实例 | 对epollfd的封装,wait后知道哪些fd产生IO事件了 |
SelectionKey | - | 封装了fd(channel) 、IO事件和自定义attach等 |
API对比
java.nio | epoll | 备注 |
---|---|---|
Selector.open() | epoll_create() | 创建管理fd的epoll实例 |
channel.register(sel, ops, null) | epoll_ctl(epfd, EPOLL_CTL_ADD/EPOLL_CTL_MOD, fd, event) | 注册fd和感兴趣的事件 |
selector.select() | epoll_wait() | 阻塞等待IO事件发生 |
selectionKey.cancel() | epoll_ctl(epfd, EPOLL_CTL_DEL, fd, event) | 从epollfd中删除关注事件 |
阻塞IO 起一个服务
作为对比,我们以阻塞IO为服务器切入:
server
public class Server extends Thread {
private ServerSocket serverSocket;
@Override
public void run() {
try {
final int port = 8345;
serverSocket = new ServerSocket(port);
while (true) {
Socket socket = serverSocket.accept();
// 每次起一个线程,或者线程池处理。这里简单演示
new RequestHandler(socket).run();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException {
new Server().start();
}
}
/**
* 请求响应handler
* 服务端只发送一个文本给客户端
*/
class RequestHandler implements Runnable {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
out.println("nice2 meet u 来自阻塞IO响应~");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
client
server启动后,会阻塞在 accept()
上。
client 这里为了演示,不必写代码。利用nc命令
$ nc localhost 8345
nice2 meet u 来自阻塞IO响应~
显然阻塞IO在高并发服务中性能表现会很差,当fd尚无可读可写事件时,线程还是阻塞在那里浪费资源。下面我们看nio中利用IO复用和非阻塞IO的方案。
nio 起一个服务
nio 使用IO多路复用和非阻塞IO模型。demo里尽可能把注释写的详细,并且有和epoll那三个api的对比。
server
public class NIOServer {
/**
* epoll实例
*/
private static Selector selector;
public static void main(String[] args) throws Exception {
// 创建server端socket,listenfd
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8346));
// 使之非阻塞IO,搭配IO多路复用
serverSocketChannel.configureBlocking(false);
// 创建epoll实例 epollfd
selector = Selector.open();
// 把listenfd注册到epoll,并告诉关注IO事件 OP_ACCEPT(可读的再封装,区分连接事件)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server start...");
while (true) {
// epoll_wait()阻塞等待IO事件
while (selector.select() > 0) {
// selector.keys(),返回SelectionKey集合,即epollfd中关注所有哪些fd和对应关注事件等
System.out.println("epoll当下fd数量" + selector.keys().size());
// selector.selectedKeys() 不同于keys(),这个返回产生IO事件的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 循环处理IO事件
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
// 连接事件 新客户端
if (selectionKey.isAcceptable()) {
acceptHandler(selectionKey);
} else if (selectionKey.isReadable()) { // 其他可读事件(已建立连接的客户端)
readHandler(selectionKey);
}
}
}
}
}
private static void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
// 注册到epoll时,用户指定的附件信息取出来
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read;
try {
// 非阻塞IO 死循环一直读直到EAGAIN退出
while (true) {
// non-block
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
// 为了演示 读到啥响应啥
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void acceptHandler(SelectionKey selectionKey) throws IOException {
// SelectionKey封装了channel和IO事件,因此直接能拿到这个channel(fd)
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
// accept
SocketChannel accept = serverSocketChannel.accept();
SocketAddress remoteAddress = accept.getRemoteAddress();
System.out.println("连接客户端成功 address: " + remoteAddress);
// 把IO置为非阻塞IO 搭配IO复用
accept.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
// 连接事件完成后,关注客户端可读事件
// 把buffer当做自定义附件信息放到这个连接描述符对应的selectionKey上,为后续处理时使用
accept.register(selector, SelectionKey.OP_READ, byteBuffer);
}
}
client
server启动后,会阻塞在 selector.select()
上。
client 这里为了演示,不必写代码。可以起多个客户端,利用nc命令。同时注意观察服务端日志。
$ nc localhost 8346
xxx
xxx
总结
java.nio 封装了操作系统底层IO的系统调用,并提供出了灵活易用的API。需要理解NIO中一些概念和类的含义和用法,使用中对比epoll就很好理解了。