一丶什么是 NIO
NIO 是非阻塞 IO, 也可以说是新 IO. 在读写时并不会阻塞.
二丶同步与异步, 阻塞与非阻塞
同步, 一个任务的完成依赖于另一个任务的完成, 需要等待另一个任务的完成, 才能执行本任务.
异步, 一个任务的完成不依赖另一个任务的完成, 无须等待.
阻塞, 是相对于 CPU 来说的, 挂起当前线程.
非阻塞, 无须挂起当前线程, 可以去执行其他程序.
三丶与 IO 的对比
a. 旧 IO 是阻塞的, 新 IO 是非阻塞的.
b. 旧 IO 是面向流的, 只能单向读写, 新 IO 是面向缓冲的, 可以双向读写
在使用旧 IO 做 Socket 连接时, 由于单向读写, 当没有数据时, 会挂起当前线程, 阻塞等待, 为防止影响其它连接, 需要为每个连接新建线程处理. 然而系统资源是有限的, 不能过多的新建线程. 线程过多带来线程上下文的切换, 从来带来更大的性能损耗. 因此需要使用新 IO 进行 IO 多路复用, 使用一个线程来监听所有 Socket 连接, 使用本线程或者其他线程处理连接
四丶 UNIX 5 种 IO 模型
5 种 IO 模型均可分为两个阶段, 一个是等待数据到达内核缓冲区, 一个是将数据从内核空间拷贝到用户空间, 其中前 4 种为同步 IO, 最后 1 种为异步 IO, 前 4 种的第二阶段完全相同
a. I/O 阻塞
阻塞等待数据到达内核空间, 最后将数据拷贝到用户空间
b. I/O 非阻塞
轮训检查数据是否到达内核空间, 数据就绪后将数据拷贝到用户空间
c. I/O 多路复用
IO 多路复用, 主要思想是一个线程阻塞监听所有 IO 事件, 当事件到达时, 分发给对应处理线程, 将数据拷贝到用户空间进行处理
IO 多路复用又分为 3 种实现方式 -- select/poll/epoll,
select 方式会将需要监听的文件描述符从用户空间拷贝到内核空间, 内核会通过轮询该文件描述符数组, 来判断对应事件是否就绪, 当事件就绪时, 就会将会整个文件描述符数组拷贝到用户空间. 所以 select 存在几个缺点: 1) 由于采用了数组保存文件描述符, 所以一般最大限制数量为 1024 2)两次文件描述符的拷贝 3) 用户空间和内核空间上下文切换
poll 方式与 select 方式类似, 只不过没有了最大文件描述符数量的限制, 因为采用了链表保存.
epoll 方式则不同, 采用了红黑树保存需要监听的文件描述符, 并为该文件描述符注册回调函数, 当事件到达时, 会调用回调函数, 将红黑树中对应的文件描述符, 保存到一个双向链表中. 最后通过判断该双向链表是否为空即可.
三者的更详细区别可以查看此博文
d. 信号驱动 I/O
在文件描述符就绪时发送 SIGIO 信号通知
e. 异步 I/O
两个阶段都不用等待, 由内核完成, 数据到达内核缓冲区之后, 内核将数据拷贝到用户空间, 完成之后再通知用户程序.
五丶 Buffer 的介绍
buffer 是缓冲区, 本质与其他缓冲 (如 BufferInputStream) 一样, 里面都是字节数组 (byte[]), 它在于控制一次性从数据源(磁盘等) 读取多个字节, 减少与数据源的交互次数, 从而提高性能.
a. buffer 有几个重要属性:
capacity: 缓冲区数组的长度
position: 下一个要操作元素的位置
limit: 下一个不可操作元素的位置
mark: 用于记录当前 position 的前一个位置或者默认是 0
b. ByteBuffer 几个重要方法
- // 创建缓冲区
- ByteBuffer allocate(int capacity); // 在堆中创建缓冲区
- ByteBuffer allocateDirect(int capacity); // 在堆外内存中创建缓冲区
- // 存数据进 buffer, position 位置会随着数据的增加而变化
- ByteBuffer put(byte b);
- ByteBuffer put(byte[] src);
- // 读取 buffer 中的缓冲数据
- byte get(); // 读取 position 对应的位置的数据, 并 "移动"position (+1)
- // 将 position 设置为 0, 一般用于读操作之后的写操作
- Buffer flip();
- // 判断是否还有元素可操作,(position<limit)
- boolean hasRemaining();
- // 清空数据
- Buffer clear();
- // 使用 mark 标记 position 的位置
- Buffer mark();
- // 重置 position, 将 position 变为之前 mark 的操作
- Buffer reset();
c. 注意事项
在调用 #put(byte[])方法将数据存进 buffer 中后, 需要调用 #flip()方法, 将 position 指针移到开头, 然后才能将数据写进其他地方, 因为 position 表示下一个要操作的元素的位置, 操作示意图可以查看此博文
六丶 Channel 的介绍
channel 是一个通道, 与 io 中的 stream 类似, 都是用于读取输出数据, 不同之处在于, channel 是双向的, stream 是单向的, channel 是不能直接操作数据, 需要将数据读取到缓冲区 buffer 之中, 然后缓冲区中读取数据, 或者将数据写入缓冲区 buffer 中, 然后在将 buffer 中的数据写入通道中
七丶 Selector 的介绍
选择器, 用于注册各种 channel 感兴趣的事件, 共有 4 种感兴趣的事件: OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT
- ServerSocketChannel ssc=ServerSocketChannel.open();
- ssc.configureBlocking(false); // 设置非阻塞 io
- // 绑定端口 backlog 设为 1024
- ssc.socket().bind(new InetSocketAddress(8040),1024000000);
- ssc.register(selector, SelectionKey.OP_ACCEPT);
- SocketChannel sc=ssc.accept();
- sc.configureBlocking(false);
- sc.register(selector, SelectionKey.OP_READ);
selector.select(); 是一个阻塞方法, 当没有感兴趣的事件时, 会阻塞等待.
selector.selectedKeys(); 返回感兴趣的所有事
SelectionKey#isAcceptable(), #isReadable(), #isWritable(), #isConnectable() 判断发生的是哪种事件.
八丶完整示例代码
- public class NioServerTests {
- public static void main(String[] args) throws IOException {
- // channel , selector
- ByteBuffer buffer=ByteBuffer.allocate(5);
- Selector selector=Selector.open();
- // Path path=FileSystems.getDefault().getPath("test.txt");
- // Channel channel=FileChannel.open(path, StandardOpenOption.WRITE);
- ServerSocketChannel ssc=ServerSocketChannel.open();
- ssc.configureBlocking(false); // 设置非阻塞 io
- // 绑定端口 backlog 设为 1024
- ssc.socket().bind(new InetSocketAddress(8040),1024000000);
- ssc.register(selector, SelectionKey.OP_ACCEPT);
- while(true){
- System.out.println("阻塞等待...");
- selector.select(); // 阻塞等待
- Set<SelectionKey> selectedKeys=selector.selectedKeys();
- Iterator<SelectionKey> iter=selectedKeys.iterator();
- StringBuilder sb=null;
- while (iter.hasNext()){
- SelectionKey selectionKey=iter.next();
- iter.remove(); // 移除被选择的通道, 防止重复处理
- if(selectionKey.isAcceptable()){
- ssc=(ServerSocketChannel) selectionKey.channel();
- // 需要注册 channel
- SocketChannel sc=ssc.accept();
- sc.configureBlocking(false);
- sc.register(selector, SelectionKey.OP_READ);
- System.out.println("服务端接受连接...");
- }else if(selectionKey.isReadable()){
- SocketChannel sc=(SocketChannel) selectionKey.channel();
- int len=-1;
- ByteArrayOutputStream baos=new ByteArrayOutputStream();
- while((len=sc.read(buffer))>0){
- buffer.flip(); // 反转 postion, position 表示操作的下一个位置
- byte b=-1;
- while(buffer.hasRemaining()){
- b=buffer.get();
- baos.write(b);
- }
- buffer.clear();
- }
- if(baos.size()> 0){
- System.out.println("服务端接收消息为:"+ baos.toString());
- }
- if(len==-1){ // -1, 表示客户端主动关闭连接关闭,
- System.out.println("关闭连接...");
- sc.close(); // 服务端也需要关闭连接, 否则该链接仍然会可读
- }
- }
- }
- }
- }
- }
- public class NioClientTests {
- public static void main(String[] args) throws IOException, InterruptedException {
- SocketChannel sc=SocketChannel.open(new InetSocketAddress(8040));
- sc.configureBlocking(false); // 设置成非阻塞 io
- ByteBuffer buffer=ByteBuffer.allocate(102400000);
- int i=0;
- while(true){
- System.out.println("发送消息...");
- buffer.clear(); // 清空消息
- buffer.put("timfruit, 您好...".getBytes());
- buffer.flip(); // 将 position 反转至开始的位置, 用于写
- sc.write(buffer);
- i++;
- if(i>1){
- break;
- }
- Thread.sleep(1000);
- }
- sc.socket().close();
- // sc.close();
- }
- }
完整源码: 点此查看
学习资料:
<深入分析 java web 技术内幕>
UNIX 的 5 种 IO 模型介绍 https://www.jianshu.com/p/71804d2511bc
select,poll,epoll 之间的区别(搜狗面试)
来源: http://www.bubuko.com/infodetail-3115805.html