关于传统IO和NIO的概念和区别什么的就不在这里说明了,这片文章主要是关于通过Java nio来实现异步非阻塞模型,我们先来看一段代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class AsyncNonBlockingServer {
private Selector selector;
private ServerSocketChannel serverChannel;
private ByteBuffer buffer;
public AsyncNonBlockingServer(int port) throws IOException {
// 创建选择器和服务器通道
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
// 注册服务器通道到选择器,并注册接收连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
buffer = ByteBuffer.allocate(1024);
}
public void start() throws IOException {
System.out.println("Server started.");
while (true) {
// 阻塞等待事件发生
selector.select();
// 处理事件
Iterator keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isAcceptable()) {
// 接收连接事件
handleAccept(key);
} else if (key.isReadable()) {
// 可读事件
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("New client connected: " + clientChannel.getRemoteAddress());
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
key.cancel();
clientChannel.close();
System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());
return;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("Received message from client: " + new String(data));
}
public static void main(String[] args) {
try {
AsyncNonBlockingServer server = new AsyncNonBlockingServer(8080);
server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个代码相信大家都很熟悉,通过java nio实现的一个简单的异步非阻塞io模型,但是这里有一个问题,就是如果连接数过多的话,循环遍历一次所有的连接可能会很耗时,主要是对数据的处理可能会很耗时,那么我们可以优化一下,把对数据的处理做成异步处理,丢给子线程去处理,此处我们采用线程池的方式,改版如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncNonBlockingServerWithThreadPool {
private Selector selector;
private ServerSocketChannel serverChannel;
private ByteBuffer buffer;
private ExecutorService executorService;
public AsyncNonBlockingServerWithThreadPool(int port) throws IOException {
// 创建选择器和服务器通道
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
// 注册服务器通道到选择器,并注册接收连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
buffer = ByteBuffer.allocate(1024);
// 创建线程池,用于处理事件
executorService = Executors.newFixedThreadPool(10);
}
public void start() throws IOException {
System.out.println("Server started.");
while (true) {
// 阻塞等待事件发生
selector.select();
// 处理事件
Iterator keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isAcceptable()) {
// 接收连接事件
handleAccept(key);
} else if (key.isReadable()) {
// 可读事件
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("New client connected: " + clientChannel.getRemoteAddress());
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
key.cancel();
clientChannel.close();
System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());
return;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// 将事件处理的代码提交到线程池中
executorService.submit(() -> {
System.out.println("Received message from client: " + new String(data));
});
}
public static void main(String[] args) {
try {
AsyncNonBlockingServerWithThreadPool server = new AsyncNonBlockingServerWithThreadPool(8080);
server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
相比与前面的写法,这种就要好的多,这也是netty框架的基本思想精华所在,在handleRead中读事件数据就绪时,将读取到的数据交给线程池去处理业务,那么可能会有人说这样遍历一次不还是很耗时吗,这个当然,但是相对来说,
key.isAcceptable()
方法本身的执行时间很短,因为它只是检查一个标志位是否被设置。所以它的执行时间非常快,几乎可以忽略不计。
在Selector.select()
方法返回时,会返回一组已就绪的事件,这些事件会被放入一个集合中。在处理这些事件时,我们需要遍历集合并处理每个事件。这个遍历过程可能会耗时,特别是当连接数非常多时。但是,key.isAcceptable()
方法本身的执行时间是非常短的,不会对整体性能产生明显的影响。
Selector.select()对于不同的操作系统底层的实现方式也不一样,windows是poll,linux一般都是epoll,关于poll和epoll的区别如下:
poll
是一种阻塞式的系统调用,它会遍历所有的文件描述符,并向操作系统询问每个文件描述符是否有数据准备好。
poll
的工作方式与select
类似,但相比于select
,poll
有一些优势。首先,poll
没有最大文件描述符数量的限制,因为它使用动态分配的数据结构来存储文件描述符集合。其次,poll
的代码更加简洁,不需要像select
一样使用位图来表示文件描述符集合。
然而,与epoll
相比,poll
的性能较差。因为poll
需要遍历所有文件描述符,并向操作系统发起询问,这会带来一定的开销。而epoll
使用事件驱动的方式,只通知就绪的文件描述符,避免了遍历所有文件描述符的开销,因此具有更高的效率和更好的扩展性
在使用epoll时,应用程序将所有需要监视的文件描述符一次性交给操作系统内核,然后内核会对这些文件描述符进行监视。当其中某个文件描述符上发生事件时(例如有数据准备好了),内核会将该文件描述符标记为就绪,并通知应用程序。
这样,应用程序就不需要自己循环遍历所有文件描述符,向内核询问每个文件描述符是否有数据准备好了。相反,应用程序只需要等待内核通知就绪的文件描述符,然后处理这些就绪的文件描述符即可。
这种方式可以大大提高程序的效率,特别是在连接数较多的情况下。因为应用程序不需要遍历所有文件描述符,而是由内核来处理这个任务,从而避免了遍历的开销。这也是epoll相比于传统的select和poll机制具有更高效率和更好扩展性的原因之一
总的来说就是一句话:poll是应用程序自己遍历所有的文件描述符(也就是每一个连接),挨个询问操作系统数据准备好了没有,而epoll则是应用程序把所有的文件描述符一次性给操作系统内核,由内核遍历文件描述符,然后告诉应用程序哪些文件描述符数据准备就绪了,重点就在于对文件描述符的循环遍历,poll是应用程序层面遍历,每遍历一个文件描述符询问操作系统内核都会涉及到用户态和内核太的切换,而epoll则是把循环遍历的动作交给了操作系统内核,避免了每次都需要用户态和内核态的切换,所以epoll效率要比poll更高