在研究Netty源码的过程中,可能是由于对java的nio编程方式不够熟习,关于Selector以及线程那一块,看起来总是觉得差一点。于是,抽空研究了下jdk 的 nio与bio。不得不说,我对于nio与bio的区别了解的不够透彻,而这非常影响对于Netty的学习。这一篇博客会总结下我对nio与bio的学习与了解。(一部分 未完)
为了能更深入的说明两者之间的区别,会从 socket编程 -> nio编程 开始一步步说明。
文章比较长,假如时间紧可以看下阻塞IO的局限性这一段
socket编程
用户端bio编程
这里举的例子非常简单了,忽略了异常、超时等等各种情况。实际使用也不大可能仅仅只是读几条数据,就直接关流。这样写主要是为了简单,便于说明API。
简单来说,可以总结这样几点:
1)创立socket(指定要连接的ip及端口)
2)获取并解决输入流
3)关闭socket(真实场景这一步可能是在退出APP后)
服务端bio编程
服务端也非常简单,大致就是:
1)启动一个服务端,监听某一个端口
2)接收用户端连接
3)根据用户端的请求,写入响应(这里用户端请求没有数据)
4)关闭服务器(真实场景可能在服务器中止时触发)
阻塞IO解决多个用户端请求
上述示例只是为了说明服务端API的使用。真实场景当然不可能只有一个用户端连接为了支持多个用户端,我们简单的加个循环。
1)通过for循环,服务端就变的能解决多个用户端连接
2)注释掉serverSocket.close();是由于上面while(true)最后一条语句怎样样都执行不到了。真实场景一定会有个触发close的地方。
某个连接解决导致服务端无法响应
上述写法,从逻辑上来看改成伪代码如下:
这种写法有个非常严重的问题:
因为整个接收请求和解决请求都是在同一个线程里(本示例是主线程)当解决用户端请求这一步发生了阻塞,或者者说慢了,后来的所有连接请求都会被阻塞住。
处理方法也很简单,启动一个线程去专门解决每一个请求
这样解决流程就变成这样了:
上述方式尽管处理了,某个用户端请求阻塞导致的服务端无法解决连接的问题。但是每次一个新的连接,都会启动一个线程。其余不说,假设有1百万个连接,按照一个连接最少64k来算,64k*1000000 约 61G
(关于一个线程需要多少内存,可以看这个启动一个线程所需内存)按这么算,当连接足够多时,服务端啥都不用干,内存就会被撑爆。
使用线程池解决超多连接
处理方式也很简单,不再每次连接进来都去启动一个线程,而是改成使用线程池
整个流程大致如下:
单个线程解决阻塞导致的其余连接无法响应问题
使用线程池意味着,一个线程可能会解决来自多个用户端连接的请求,比方A用户端和B用户端恰好请求都被提交给 线程C,那么结果就是,A用户端的解决慢了,B用户端会连带着响应的特别慢。或者者A用户端的请求阻塞了,B用户端的请求也会连带着阻塞了。
阻塞IO的局限性
那么如何处理这个问题呢?在深入研究NIO和BIO的区别时,我第一反应就是使用非阻塞IO呀。但是,其实我没有弄清楚究竟非阻塞IO和阻塞IO的核心区别是啥。
首先非阻塞IO和阻塞IO最重要的一点区别,我认为是,非阻塞IO的读、写、接收连接是不会产生阻塞的
啥意思呢,首先回到之前写的服务端的示例:
当时我没有说明一个非常重要的情况,假设一直没有用户端的连接进来,这一步就会阻塞住。而这完全是没有必要的,由于可能在一段时间,根本不会有用户端去连接服务端。我们希望的情况是,用户端有连接了,我们再去accept,打个比如,我再卖菜,我当然希望有人来买菜了,我才去收银。而不是,就在收银台那边干等着,白费时间。
再来看看上述问题——单个线程解决阻塞导致的其余连接无法响应,我们首先要问,为啥会产生阻塞?
第一个起因,由于业务解决很慢。比方读写DB,可能业务就是要读取、写入很多数据,这种慢是没有办法的,无论怎样样,就是需要这么多时间。
第二个起因,socket的读写慢了。由于阻塞IO的读与写都是阻塞的。也就是说,假设服务端开始读了,服务端在用户端发送数据之前会一直阻塞住 啥问题呢,如下图:
假设用户端A和用户端B都是在一个线程中解决,用户端A已经开始读了(调用了 InputStream.read方法),但是因为没数据,服务端只能阻塞住。用户端B呢,尽管它有数据准备发给服务端,但是由于该线程已经被阻塞住了,所以用户端B的连接也只能等着。
写的场景也一样,假设服务端准备往A写数据,但是呢数据还没准备好,导致用户端B也只能在那等着。(真实场景,这种情况可能比较少。比方收到一个查询db的请求,我们都是从db里读取了数据之后,才会调用write方法写数据的。很少会出现没有数据的问题)
那么比较理想的情况是啥呢?只有用户端有数据发过来了,服务端才去读,才去解决这也就是非阻塞IO。
到了这里,阻塞IO与非阻塞IO一个非常重要的区别应该就清楚了,阻塞IO的读、写、连接都会阻塞整个线程
非阻塞IO的写法public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8888)); serverSocketChannel.configureBlocking(false); //设置服务端操作都是非阻塞的 Selector selector = Selector.open(); //选择器 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //对用户端的accept事件关心 while (true) { selector.select(); //会阻塞住,直到有事件触发 Set selectionKeys = selector.selectedKeys(); //看下有哪些事件被触发了 System.out.println("selectionKeys:" + selectionKeys); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { //用户端 accept被触发了 ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel(); SocketChannel clientChannel = serverChannel.accept(); System.out.println("channel is acceptable"); clientChannel.configureBlocking(false); //用户端channel注册OP_WRITE事件 clientChannel.register(selector, SelectionKey.OP_WRITE); } else if (key.isWritable()) { //用户端可以往里写数据了 System.out.println("channel is writeable"); String data = "hello world\n"; //注意这里的是用户端的channel,由于是使用用户端channel注册OP_WRITE事件 SocketChannel clientChannel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(data.length()); buffer.put(data.getBytes()); buffer.flip(); clientChannel.write(buffer); } key.cancel(); //取消事件 iterator.remove(); } }}
由于这里只是为了说明java nio的写法,所以写的不是很严谨。仅供参考。。实际使用别这么写。
第一次看nio写法时候,很乱,不能了解为啥阻塞IO写起来那么清楚,到了非阻塞IO就变得这么复杂了呢?这里的Selector究竟是啥?这里的SelectionKey又是啥?为啥要判断 acceptable为啥?为啥又要判断writable?
首先牢记一点非阻塞IO的所有操作都是异步的,这意味着什么?首先当我们直接调用 serverSocketChannel.accept(); 很可能直接返回一个null,由于用户端没有连接进来。而阻塞IO会一直等到用户端有连接
在拿服务端读作为例子,当我们直接调用SocketChannel.read()时,可能获取到的就直接是null,而阻塞IO会一直阻塞,直到用户端向服务端发送了数据
这样来看,由于所有请求都是异步,服务端必需要有某种机制,能知道:
1)用户端的连接过来了
2)用户端发送数据过来了
3)用户端可写了
....
而得知这些的方式,就是Selector。通过Selector的select操作,我们能遍历出当前有哪些事件准备好了,比方用户端连接过来了、用户端有数据过来了、可以往用户端发送数据了。
到目前为止,应该能说明为啥非阻塞IO的API设计是这样。更详细的java nio的使用,会在后面的博客里说明。
一个小问题:非阻塞IO是不是肯定比阻塞IO性能要好?
分析到这里,可能会有种感觉,非阻塞IO性能肯定比阻塞IO性能要好。但是其实这样说,并不精确。非阻塞IO处理了服务端有很多不活跃连接 的问题,比方说,用户端连接后,很长一段时间不发送任何请求,这样服务端解决该连接的线程就会一直卡在那里。
但是当连接不多时,并且每个连接都很活跃时,阻塞IO性能可能比非阻塞要好。
总结
这篇博客说明了java 阻塞IO与非阻塞IO的一个非常重要的区别——IO操作能否阻塞。非阻塞IO处理了大量不活跃连接的问题。
问题
非阻塞IO与阻塞IO的区别当然不止这些,对于非阻塞IO的API说明也没有非常详尽。比方非阻塞IO里的buffer是啥?为啥非阻塞IO使用起来,要比阻塞IO复杂的多?后面博客会详细说明这几点。