【网络IO系列 四】Java中NIO BIO AIO的实际应用和比较
以下的示例代码均来自彤哥的netty专栏
NIO BIO AIO的代码大多都是大同小异,所以在看的时候无需计较代码某一行是怎么写的,最重要的是通过代码去理解各种IO模型的思想和优劣点,
BIO
在前面的文章中我们讲了几种IO的模型,这篇文章主要讲讲BIO在的一些实际应用,同时再来分析他的一些缺点(好像相比NIO来说没啥优点QAQ)
先来复习一下BIO模型的一些概念
BIO在数据准备阶段和数据拷贝的时候线程都会被阻塞住,直到完成,就像是你没带手机去排队吃饭,在老板把饭准备好和端到你面前之前,你什么都干不了。
话不多说,直接上代码
public class BIODemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = null;
while (reader.readLine() != null) {
System.out.println("接收到新消息 msg =" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
以上就是一个BIO的服务端的程序,我们这里启动了一个BIO的Server来监听客户端的连接,每监听到一个连接,就启动一个线程去处理这个连接,然后从网络连接中读取到消息,再打印出来。
但是我们其实通过上面的代码,其实也能看出来一个问题,那就是每过来一个连接,服务端就要启动一个线程去处理,如果连接特别多呢,假设来了几万个连接,是不是就要启动几万个线程,并且我们还要记得,BIO是两个阶段都会阻塞的,每过来一条消息,BIO都需要阻塞到内核数据处理完毕和拷贝完毕,这样的话服务器的资源很容易就被耗光了。这就是BIO最典型的一个缺陷。
NIO
在讲NIO的语言层面的应用前我们需要区分一下语言层面的NIO和模型NIO之间的关系和区别,一般语言层面的NIO和我们之前说到的IO五种模型中的NIO,略有不同,现在在Java语言层面的NIO,不再是由用户线程去反复的轮询数据,也就是说语言层面的NIO,现在也是采用IO多路复用的思路来实现的。IO模型是思想,语言层面的NIO则是具体的实现,他可以采用不同的模型。
那么我们来复习一下IO多路复用的机制,IO多路复用,实际上,是通过IO请求都通过一个selector来管理,用户进程的IO请求就不直接发给内核处理程序了,而是注册到这个selector上面,由selector来告诉内核需要哪些数据,然后定时的去查询内核程序,我这个selector上需要的数据,有哪些准备好了,然后再由selector告诉那些准备好了的用户线程,让该用户线程去拷贝数据。
先上代码
public class ChatServer {
public static void main(String[] args) throws IOException {
//创建一个selector
Selector selector = Selector.open();
//创建channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口以及设定为非阻塞模式
serverSocketChannel.bind(new InetSocketAddress(13111));
serverSocketChannel.configureBlocking(false);
//将selector注册到serverSocketChannel上 注册为accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//selector需要不断的去查询数据准备好了没 也可以看作是一种阻塞
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//如果是连接事件 即有新的客户端连接过来
if (selectionKey.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn:"+socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
//selector注册到channel上 注册为读事件
socketChannel.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
SocketChannel ssc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据阶段的阻塞
int length = ssc.read(buffer);
if (length >0 ){
//切换到读模式
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
if (content.equalsIgnoreCase("quit")){
selectionKey.cancel();
ssc.close();
}
}
}
iterator.remove();
}
}
}
}
我们首先创建了一个selector,并且注册为了accept事件,当有一个客户端连接过来的时候,会分配一个selectionKeys,这个selectionKeys中会绑定连接过来的客户端对应的那个物理连接channel,然后将连接注册到这个selector上,委托selector去进行轮询,我们可以通过 while (iterator.hasNext())这一行知道,进行轮询的实际上是selector中的iterator来做的,它会不断的看是不是有数据已经被准备好了或者是不是有新的连接过来了或者其他的事件,然后从这个key中,取出对应的那个客户端连接(channel),然后再通过这个key的类型去做对应的操作。
这个过程就相当于前面说到的,客户进店点菜,然后服务员把xx桌客户需要的菜记录好,然后服务员一次又一次的进厨房看看有哪些人的菜已经好了,这些个key就相当于是客人的座位号,如果菜好了就通知客人端菜吃饭。当然,这里只是一种比喻,实际上selectionKey的事件肯定不止这一个,但是大家理解其工作的流程就行了。
从上面的例子然后我们再来比较一下之前的BIO的例子我们可以发现,相比较BIO每来一个客户端就要创建一个新线程来说,NIO的这个程序,自始自终都只有selector这一个线程来轮询和处理,这样的话BIO这种无限增加线程的问题就得到了解决,服务器资源也能得到非常大的节约。
AIO
全异步IO是最理想的一种IO模型,所谓全异步IO就是,用户进程发起了一个IO请求,接下来可以干其他的事了,不需要等内核准备好,也不需要执行数据拷贝,这两个阶段都是由内核自动完成,数据异步拷贝到用户空间之后,会通知用户线程,用户进程直接拿来用就行了。完全不用用户线程操心这些事。
直接上代码
public class AIOEchoServer {
public static void main(String[] args) throws IOException {
// 启动服务端
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8003));
// 监听accept事件,完全异步,不会阻塞
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
//再次监听accept事件
serverSocketChannel.accept(null, this);
// 消息的处理
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 是数据准备好的回调 读取数据 这里说明数据已经准备好了
//说明内核通知用户线程 数据已经好了 可以直接用了
Future<Integer> future = socketChannel.read(buffer);
if (future.get() > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 读取数据
buffer.get(bytes);
System.out.println("receive msg: " + content);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed");
}
});
// 阻塞住主线程 取消注释的话 程序会直接结束 因为AIO就是全异步的 直接一路往下执行
//System.in.read();
}
}
从代码可以看出,代码层面的AIO主要是通过一种事件回调机制来实现的。当发生了某某事件,或者某某事件已经就绪的时候,会触发回调执行对应的方法,这种方式就叫做Reactor模型,事件驱动。只不过对于AIO来说,是内核通知用户进程,然后再执行相应的事件处理。当方法内核数据准备完成的时候,会触发complete 方法回调,然后数据拷贝好了的时候,Future将会有一个标记,此时意味着用户空间数据已经可以直接用了,去做对数据的的处理。
就像是你去吃饭,你只需要点菜,等厨房把菜做好了(内核数据准备完成,触发completed),会通知服务员然后服务员会把菜端给你(数据拷贝完成,future.get() > 0),这个时候你直接吃就行了,整个过程你只需要做两件事,点菜和吃饭,至于菜怎么做的,啥时候好的,怎么端过来的,完全不用你担心,等待期间你可以玩手机或者去干其他的事情