标签:lin 而在 close nal 代码 buffer tags ash 非阻塞io
异步IO编程在javascript中得到了广泛的应用,之前也写过一篇博文进行梳理。
js的异步IO即是异步的,也是非阻塞的。非阻塞的IO需要底层操作系统的支持,比如在linux上的epoll系统调用。
从另外一个角度看待的话,底层操作系统对于非阻塞IO的系统调用是一种多路复用机制,js对其进行了比较厚的封装,转换成了异步IO。
但是,也可以进行一层稍微薄点的封装,保留这种多路复用的模型,比如java的NIO,是一种同步非阻塞的IO模型。
非阻塞IO的一大优势是,性能好,快啊!这在对IO性能要求高的场景得到了大量应用,比如SOA框架。
<!--more-->
传统的同步IO方式,比如网络传输,比如文件IO,在调用者调用read()时,调用会被一层一层调用下去直到OS的系统调用,调用者的线程会被阻塞。
当读取完成时,该线程又会被唤醒,read()函数返回IO操作读取的数据。
我们很容易能发现这种方式的特点及优劣:
在客户端编程时,第二点这个问题不大。客户端程序对IO的并发要求不高,反而因为同步阻塞IO的接口易于编程而能够减轻编程难度,代码更直观更可读,从而变相的提高可调试性和开发效率。
然而,在服务器端编程的时候,这个劣势就很明显了,服务器端程序可能会面临大量并发IO的考验。
传统的同步IO方式,比如说socket编程,服务器端的一个简单的处理逻辑是这样的:
在实际场景中会有很多优化技术,比如使用线程池。然而线程池仅仅是将TCP连接放入一个队列里交由线程池中空闲的线程处理。
实质上,即使使用线程池,也改变不了正在被处理的每一个请求都需要占用一个单独的线程这一事实。
这样,会造成一些问题:
java提供的NIO就是一种多路复用IO方式。
它能够将多个IO操作用一个线程去管理,一个线程即可管理多个IO操作。
NIO的操作逻辑是这样的,首先将需要监控的IO操作注册到某个地方,并由一个线程管理。
当这些IO操作完成,会以事件的形式产生。该线程能够获取到完成的事件列表,并且对其进行处理。
java的NIO中有三个重要的概念:
这里只是做个总结,看下下面的示例代码就明白了。
private void exec(int port) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int n = selector.select(); // Block
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
if (channel != null) {
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
onAccept(channel);
}
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
onRead(socketChannel);
}
it.remove();
}
}
}
来一步一步的分析这些代码。
首先,第3行到第6行是对通道ServerSocketChannel
的操作。
对于这个ServerSocketChannel,首先是设定了它的监听地址,这个与传统的阻塞IO一致,给定一些初始的数据。传统的阻塞IO之后会调用socket.accept()
来获取客户端连接的TCP连接,这是一个阻塞的方法。
但是NIO在这里把ServerSocketChannel注册到了Selector上,并且监控OP_ACCEPT事件。这个时候socket可以认为已经在监听了,但是没有阻塞线程。
之后,如果有TCP连接连接上,OP_ACCEPT事件就会产生,通过selector即可处理该事件。
因此,NIO的操作逻辑其实是事件驱动的。
后面的循环则是Selector处理的主逻辑。
第9行,这是一个阻塞的方法。它会等待被注册的这些IO操作处理完成。一旦有一部分IO操作完成,它就会返回。
通过selector.selectedKeys()
即可获得完成的IO操作的事件。后面的代码也就是在处理这些事件。
这部分完成的IO事件处理完毕后,就会循环的去处理下一批完成的IO事件,如此往复。
这里,我们可以清晰的看到,通过NIO的多路复用模型,我们通过一个线程,就能管理多个IO操作。
循环内部处理的逻辑,key.isAcceptable()
可以认为是判断该事件是否是OP_ACCEPT
事件。是的话表示已经有客户端TCP连接连接上了,第15行获取该TCP连接的socket对象。由于是NIO编程,这是获取到的是SocketChannel
对象。
之后将该对象的OP_READ
注册到Selector上,发起IO读操作,并且让Selector监听读完成的事件。
后面的key.isReadable()
也是同样的道理,这里只有上面的代码注册了OP_READ
事件,因此这里一定是上面的读操作完成了产生的事件。
上面的代码里,当有新的TCP连接连入时,调用回调函数onAccept
;当对方传输数据给自己时,数据读取完成后,调用回调函数onRead
。
下面是这两个回调函数的实现,它的功能很简单:
hello\n
给对方。private void onRead(SocketChannel socketChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int count;
while ((count = socketChannel.read(buffer)) > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
buffer.clear();
}
if (count < 0) {
socketChannel.close();
}
}
private void onAccept(SocketChannel channel) throws IOException {
System.out.println(channel.socket().getInetAddress() + "/" + channel.socket().getPort());
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.put("hello\n".getBytes());
buffer.flip();
channel.write(buffer);
}
从上面的代码可以看出:
上面通过一个小DEMO,也就是一个简单的ECHO服务器演示了NIO编程。下面来测试下结果:
frapples:~ ?> nc -nvv 127.0.0.1 4040
Connection to 127.0.0.1 4040 port [tcp/*] succeeded!
hello
jfldjfl
jfldjfl
jfldjflieu
jfldjflieu
jfldhgldjfljdl
jfldhgldjfljdl
效果不错!不过这还没完。
尝试开启多个终端,同时连接服务器,你会惊讶的发现,服务器能够完美的同时和多个客户端连接而不会出现“卡死”的情况。
回顾刚才的小DEMO我们可以发现,刚才的DEMO是 单线程 的,但是通过多路复用模型,却能同时处理多个IO操作。
之前在博文《异步IO和同步IO》中也提到了一些异步IO的操作系统机制。
非阻塞IO需要操作系统机制的支持,在linux系统上,对应的是select/poll系统调用或epoll系统调用。
操作系统的作用之一是对硬件设备的管理,我们发现,负责运算的部件CPU和负责网络传输的部件网卡,它们是互相独立的,因此,它们实际上可以同时执行任务。那么,底层硬件的支持使得完全可以做到以下步骤:
这里有个小小的问题,在读取数据的时候,上面的步骤网卡读取数据时显然是不通过CPU的。以我个人有限的硬件知识推测,非阻塞IO的机制可能需要用到DMA。
仍然是个人推测,以后有时间去查阅相关资料去解决这个疑惑。
我们可以看到,硬件的运作方式天然就是异步的,也因此,操作系统也非常容易基于此进行抽象和封装,向上提供非阻塞的IO系统调用。
linux操作系统的系统调用提供了多路复用的非阻塞IO的系统调用,这也是java NIO机制实现需要用到的。
在linux2.6之前,采用select/poll系统调用实现,而在linux2.6之后,采用epoll实现,使用红黑树优化过,也因此性能更高。
本篇博文梳理的java的NIO机制,这是一种多路复用模型,能够使用一个线程去管理多个IO操作,避免传统同步IO的线程开销,大大提升性能。
从我个人的观点,评判一种模型是否易用,一方面来看该模型是否与实际的问题特点相契合;另外一方面,看该模型需要开发者花多少成本在模型本身上而非业务逻辑上。
从这个标准出发,我们也不难发现,本身异步IO的回调方式就够让开发者头疼的了,然而和异步IO相比,NIO比异步IO还要麻烦。
你需要花大量精力去时间去处理,去理解NIO本身的逻辑。因此,NIO的缺点是较高的开发成本和较晦涩的代码,不优雅。
NIO在SOA框架,RPC框架等服务器领域有着较大的应用,除了java标准库的NIO之外,这些实际生产的框架多使用第三方的NIO框架Netty。
原因之一是,java标准库的NIO有一个bug,可能造成CPU 100%的占用。
标签:lin 而在 close nal 代码 buffer tags ash 非阻塞io
原文地址:https://www.cnblogs.com/wsxnihao/p/12989781.html