Java NIO(New IO)是Java平台自1.4版本以来提供的一种用于处理IO操作的新API。相比旧的传统IO(即java.io包中的API),它能够更好地处理大量的并发IO操作。NIO最常用的用例之一就是创建高效的异步IO程序。
在使用Java NIO进行异步IO编程时,与传统IO模型不同的是,应用程序需要运行一个Reactor线程和多个Worker线程。Reactor线程是主要的调度员,负责注册所有I/O事件,并将这些事件分派给正确的Wroker线程。Worker线程,顾名思义,是执行实际I/O 和数据处理工作的线程。下面我们来详细介绍如何使用Java NIO进行异步IO编程的过程。
1、创建一个Selector
当一个SocketChannel或者ServerSocketChannel被创建时,它们都会分别注册到一个Selector中去。Selector则是Java NIO异步IO中最重要的概念之一。Selector对象允许线程等待一组通道中其中之一的事件发生。因此,Selector可以允许单个线程同时处理多个网络连接。
下面是一个简单的创建Selector实例的示例:
代码语言:javascript复制Selector selector = Selector.open(); // 创建一个Selector
channel.configureBlocking(false); // 将其设置为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT); // 注册 Selector,关心 Accept 事件
当你收到一个连接时,你需要做的就是注册该连接的读写事件:
代码语言:javascript复制SelectionKey key = channel.register(selector, Selectionkey.OP_READ); // 注册 Selector,关心 Read 事件
通过上述示例,我们创建了一个Selector对象并用它来注册SocketChannel。在这个过程中,使用代码把通道设置成非阻塞模式(即使信道不一定立即就准备好),并且我们将仅对Accept事件感兴趣。
2、接受新的连接
接下来,我们需要使用Java NIO处理新连接。当一个ServerSocketChannel准备好接受新的套接字连接时,就会向Selector发送一个信号,让其处理此类请求。与此相同,只要有数据可供读取或写入,则会自动向选择器发送信号。为了在我们正在等待中进行有效的工作, 我们需要调用select() 方法来确定发生了什么。
以下是如何接受新连接并为每个新客户端创建新通道的示例代码:
代码语言:javascript复制while (true) {
int i = selector.select();
if (i == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, Selectionkey.OP_READ);
}
}
}
3、从连接中读数据
当你已经成功地通过Java NIO建立了一个连接后,接下来的步骤就是开始读取数据。我们需要将客户端的请求消息(例如http请求或者其他一些协议)的内容存储在ByteBuffer对象中,并从通道上读取它。读取请求时同样需要考虑非阻塞I/O。
代码语言:javascript复制while (true) {
int i = selector.select();
if (i == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, Selectionkey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(buffer);
if (readBytes > 0) {
buffer.flip(); // 将其设为等待模式
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
}
}
}
}
上面的代码中,我们判断是否可以开始读(此事件是否准备好了),如果可以读,我们会新建一个Bytebuffer来保存读取到的数据。
4、将数据写入连接
最后一步是将响应消息写回客户端。Java NIO中也提供了同样简单易用的实现方法。正好与读取大致相同,对于写操作的ByteBuffer对象,我们需要logger它并发送到通道上:
代码语言:javascript复制while (true) {
int i = selector.select();
if (i == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, Selectionkey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(buffer);
if (readBytes > 0) {
buffer.flip(); // 将其设为等待模式
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
ByteBuffer responseBuffer = ByteBuffer.wrap("response message".getBytes());
channel.write(responseBuffer);
}
}
}
}
在编写具有可伸缩性的高性能异步服务器时,使用Java NIO异步I/O是非常必要的。不仅如此,Java NIO还提供了大量的特性,可轻松处理文件IO、内存映射以及基于信道的安全威胁等。摆脱阻塞式I/O模型,掌握Java NIO异步I/O编程能力,可以使你在高性能方面取得重大提升。