注意标题.此章只是让我们对NIO有个概念,而不是让你真正的去敲或者熟悉以下代码
传统BIO编程如图1,来一个客户端连接就需要一个新的线程.
而NIO中会把新的连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据
举个例子:
幼儿园小朋友要上厕所,但是太小不会表达.
BIO 一个小朋友(客户端连接)配一个老师(线程),同时老师还要不停的轮询(循环)小朋友是否需要上厕所.然后再一个个带小朋友去厕所
NIO 50个小朋友配一个老师,老师统一询问是否有小朋友需要上厕所(客户端是否有数据传输),如果有则批量带去上厕所(接收数据)
由于 NIO 模型中线程数量大大降低,线程切换效率因此也大幅度提高
IO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。 而 NIO 的读写是面向 Buffer 的,你可以随意读取里面任何一个字节数据,不需要你自己缓存数据,这一切只需要移动读写指针即可。
下面是JDK 原生的 NIO 来实现服务端代码(从网上copy的 来自闪电侠)
扫描二维码关注公众号,回复:
9225023 查看本文章
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
核心思路
- NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在我们这个例子中
serverSelector
负责轮询是否有新的连接,clientSelector
负责轮询连接是否有数据可读 - 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到
clientSelector
上,这样就不用 IO 模型中 1w 个 while 循环在死等,参见(1) clientSelector
被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)
方法可以轮询出来,进而批量处理,参见(2)- 数据的读写面向 Buffer,参见(3)
不用对代码的细节深究到底。总之,强烈不建议直接基于JDK原生NIO来进行网络开发
- JDK 的 NIO 编程需要了解很多的概念,编程复杂,对 NIO 入门非常不友好,编程模型不友好,ByteBuffer 的 Api 简直反人类
- 对 NIO 编程来说,一个比较合适的线程模型能充分发挥它的优势,而 JDK 没有给你实现,你需要自己实现,就连简单的自定义协议拆包都要你自己实现
- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug
所以Netty 横空出世!