关于BIO和NIO的概念和区别什么的就不在这里说明了,这片文章主要是关于通过Java nio来实现同步非阻塞模型,我们首先要搞明白此处的同步非阻塞只是针对IO,而不是读取数据之后的处理操作,然后就是有几个前置的知识要明确:
-
同步强调的是应用程序需要自己主动去询问操作系统内核,对数据进行读写,目前linux只支持同步,windows支持异步,异步则指的是应用程序本身不需要主动的去询问操作系统数据读写情况,数据准备好之后操作系统会通知应用程序来处理数据
-
阻塞指的是应用程序在等待客户端连接函数读取数据的时候表现出来的,当没有客户端连接和数据过来的时候,应用程序代码会阻塞住,无法往下执行,而异步则是应用程序在调用系统函数,比如accept和read的时候,一定会有一个返回值,应用程序会根据这个返回值来决定如何处理,而阻塞的时候,调用系统函数,比如accept和read的时候,如果没有数据,就不会有返回值
我们先来看一段代码:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class NioServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//服务端非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8888));
List<SocketChannel> clients = new LinkedList<>();
while (true) {
TimeUnit.SECONDS.sleep(1);
SocketChannel client = serverSocketChannel.accept();
if (null == client) {
System.err.println("没有新的客户端连接.....");
} else {
//客户端非阻塞
client.configureBlocking(false);
System.err.println(client.socket());
clients.add(client);
}
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
for (SocketChannel socketChannel : clients) {
int num = socketChannel.read(byteBuffer);
if (num > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
String b = new String(bytes);
System.err.println(b);
byteBuffer.clear();
}
}
}
}
}
这是一个基本的NIO同步非阻塞模型,它的问题就是对于连接和读取数据都在一个线程里面去处理的,当连接数量多了之后,性能会几句下降,可能大家会想到把数据处理丢到一个线程池里面去处理,也就是for循环的那一段,确实这样可以提高效率,但是这样指标不治本,这个代码最根本的问题就在于,这个for循环里面的int num = socketChannel.read(byteBuffer);这一句,这一句代码每次都会涉及到一次系统调用,也就是会有一次用户态和内核态的切换过程,那么为了解决这个问题,就演化出了多路复用器模型,也就是下面的代码
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<SelectionKey> 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层面最核心的就是这一行了:selector.select();
它在操作系统层面的实现,早期都是select,但是select支持的文件描述符(每一个连接都可以看做是一个文件描述)数量有限制,于是后来又出现了poll,相比于select,没有文件描述符的限制
总结一:相比于传统的NIO模型,多路复用模型在询问操作系统数据准备好没有的时候,是一次性把所有的文件描述符传递给内核,也就是每一轮循环,只涉及到一次用户态和内核态的切换,而传统的NIO,因为遍历连接是在应用程序层面进行的,在询问每一个连接数据准备好与否的时候都会涉及到一次用户态和内核态的切换,所以多路复用模型要比传统的NIO模型性能更高
ps:后续还会更新进一步的演化epoll