返回顶部
首页 > 资讯 > 后端开发 > JAVA >全网最清晰JAVA NIO,看一遍就会
  • 750
分享到

全网最清晰JAVA NIO,看一遍就会

网络java 2023-09-01 14:09:59 750人浏览 八月长安
摘要

目录 目录 1.概述 1.1.计算机的IO模型 1.2.NIO概述 2.buffer 2.1.概述 2.2.API 2.3.代码示例 3.channel 3.1.概述 3.2.API 3.2.1.读写 3.2.2.文件复制 3.2.3.阻塞

目录

目录

1.概述

1.1.计算机的IO模型

1.2.NIO概述

2.buffer

2.1.概述

2.2.API

2.3.代码示例

3.channel

3.1.概述

3.2.API

3.2.1.读写

3.2.2.文件复制

3.2.3.阻塞与非阻塞

4.selector

4.1.概述

4.2.代码示例


1.概述

1.1.计算机的io模型

在聊IO之前一定要了解计算机的IO模型,因为编程语言的所有和IO有关的api,本质上一定是对于计算机IO模型的抽象。

计算机的IO其实就是在内存中为各个IO设备分配了属于它的一块内存,向这块内存中进行读写即可完成IO。这块内存是位于计算机内存中的内核段中的。

一次完整的IO过程是:

CPU执行IO指令

将内核段中数据读到用户段

用户段中数据交给CPU

CPU的执行结果写回用户段

用户段中存的结果写回内核段

 如果对计算机的内存分段、IO等相关概念不熟悉的同学,可以移步博主的另一篇文章,里面详细介绍了相关内容:

详解零拷贝__BugMan的博客-CSDN博客

1.2.NIO概述

NIO,non-blocking IO,从jdk1.4版本开始引入,其直观的特点就是非阻塞,深入一点来看的话,NIO推出之前的JAVA BIO只是对tcp进行了简单的封装,用户只能对进行简单的IO,而整个计算机底层,在内存中的IO过程是被封装成了黑盒的。NIO对计算机底层的IO过程做了抽象,开放出来了内存粒度的API,让使用者可以更加细粒度的从计算机内存的角度来控制IO。

如果不是很了解BIO的同学可以移步作者的另一篇文章,其中详细讲解了BIO:

JAVA BIO__BugMan的博客-CSDN博客

NIO有三大核心:

  1. channel
  2. buffer
  3. selector

1.buffer:

可以理解为用户段的内存的抽象。

2.channel:

可以理解为用户段和内核段IO区的连接的抽象,当然直接理解为内核段的IO区的抽象其实也可以。

3.selector:

NIO实现非阻塞式IO的核心,其可以基于事件监听的方式,选择准备好的channel,再去其中将数据读到buffer中,然后去操作buffer中的数据。这里要是有点晕,不要紧,后文在selector章节,会详细介绍。

2.buffer

2.1.概述

buffer,理解为用户段中一块内存的抽象即可。

既然是一块内存,那么其本质上就是用来进行数据读写的一个容器,由java.nio包定义,顶级接口为Buffer,定义了一套API用来管理缓 冲区中的数据针对存储不同的数据类型,有不同的buffer:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

buffer具有以下几个基本属性:

  • 容量(capacity),buffer的大小,buffer创建后,容量不能更改。
  • 限制(limit) ,buffer可用的大小,limit之后的区域无法进行读写。
  • 位置(position) 表示接下来要读写的数据的所在位置。
  • 标记(mark) 标记一个位置的索引,调用reset()方法可以回到该位置上

图示:

2.2.API

  • Buffer clear() 清空缓冲区并返回对缓冲区的引用
  • Buffer flip()为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0 0
  • int capacity() 返回 Buffer 的 capacity 大小
  • boolean hasRemaininGo) 判断缓冲区中是否还有元素
  • int Timit0 返回 Buffer 的界限(Timit) 的位置
  • Buffer limit(int n) 将设置缓冲区界限为 n,并返回一个具有新  imit 的缓冲区对象
  • Buffer mark() 对缓冲区设置标记
  • int position() 返回缓冲区的当前位置 position
  • Buffer position(int n) 将设置缓冲区的当前位置为 n ,并返回修改后的 Buffer 对象
  • int remaining() 返回 position 和  imit 之间的元素个数
  • Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
  • Buffer rewind() 将位置设为为 0, 取消设置的 mark
  • get() : 读取单个字节get(byte[] dst): 批量读取多个字节到 dst 中
  • get(int index): 读取指定索引位置的字节(不会移动 position)
  • put(byte b): 将给定单个字节写入缓冲区的当前位置
  • put(byte[] src): 将 src 中的字节写入缓冲区的当前位置
  • put(int index,byte b): 将指定字节写入缓冲区的索引位置(不会移动 position)

2.3.代码示例

由于buffer其实就是一块内存的抽象,是一个数据容器,所以核心其实就是put、get。

此处以byteBuffer为例,其它相同。

byte[] resources = "hello".getBytes();//初始化ByteBuffer byteBuffer = ByteBuffer.allocate(1024);System.out.println(byteBuffer.capacity());System.out.println(byteBuffer.position());System.out.println(byteBuffer.limit());//写byteBuffer.put(resources);System.out.println(byteBuffer.capacity());System.out.println(byteBuffer.position());System.out.println(byteBuffer.limit());//读//不开读模式,读不到任何数据System.out.println(byteBuffer.get());System.out.println(byteBuffer.get());//开起读模式才能读到数据byteBuffer.flip();System.out.println(byteBuffer.get());System.out.println(byteBuffer.get());//读取全部System.out.println(new String(byteBuffer.array()));

3.channel

3.1.概述

Channel,通道,由java.nio.channel包下定义,用来向buffer中读写数据,可以理解为内核段和用户段之间进行数据传输的一条逻辑通道,甚至可以直接理解为内核段中内存的一个抽象。

通道具有以下特性:

  • 全双工,读写可以同时进行,即可以向缓冲区中写,又可以向缓冲区中读
  • 支持异步

Channel是一个顶级父接口,针对需要传输的数据格式的不同分为:

  • FileChannel 用于读取、写入、映射操作文件
  • DatagramChannel 用于通过UDP读写网络中的数据
  • SocketChannel 通过TCP读写网络中的数据,底层封装的Socket
  • ServerSocketChannel SocketChannel的升级版,可以自动监听新的TCP连接,每一条新连接创建一个SocketChannel。

可以从以下地方获取不同的通道:

  • FileInputStream
  • FileOutStream
  • RandoMaccessFile
  • Socket ServerSocket

3.2.API

3.2.1.读写

用channel将buffer中的数据写出来:

       try {            FileOutputStream fos = new FileOutputStream( "nio_channel/data01.txt");            //获取file类型的channel            FileChannel channel=fos.getChannel();            //准备好要写出的内容            ByteBuffer buffer=ByteBuffer.allocate(1024);            buffer.put("helloWorld!".getBytes());            //将buffer切换成读模式            buffer.flip();            //写出            channel.write(buffer);            channel.close();        } catch (Exception e) {            e.printStackTrace();        }

用channel将数据读进buffer:

        try {            FileInputStream is=new FileInputStream("data01.txt");            FileChannel channel = is.getChannel();            ByteBuffer buffer=ByteBuffer.allocate(1024);            //用channel将数据读到buffer中            channel.read(buffer);            System.out.println(new String(buffer.array()));        } catch (IOException e) {            e.printStackTrace();        }

3.2.2.文件复制

在JAVA NIO中有两种方式可以实现文件的复制:

  1. 非零拷贝,即一个通道向buffer中写,另一个通道去buffer中读,数据要走用户段。
  2. 零拷贝,直接从磁盘的一个地方拷贝到磁盘的另一个地方,数据不用走用户段。

这里要是对零拷贝不了解的同学,可以移步博主的另一篇文章,对零拷贝进行了详细讲解:

详解零拷贝__BugMan的博客-CSDN博客

1.非零拷贝:

一个channel向buffer中写,另一个channel去buffer中读。

        try {            File srcFile=new File("data01.txt");            File targetFile=new File("data02.txt");            FileInputStream fis=new FileInputStream(srcFile);            FileOutputStream fos=new FileOutputStream(targetFile);            FileChannel isChannel=fis.getChannel();            FileChannel osChannel=fos.getChannel();            ByteBuffer buffer=ByteBuffer.allocate(1024);            while (true){                //读数据                int flag=isChannel.read(buffer);                if(flag==-1){                    break;                }                //读模式                buffer.flip();                //写数据                osChannel.write(buffer);                //清空buffer                buffer.clear();            }        } catch (IOException e) {            e.printStackTrace();        }

2.零拷贝:

当使用Java NIO进行文件传输时,提供了两个底层使用零拷贝的API,一个是transferTo、一个是transferFrom。可以通过transferTo方法将数据从一个Channel传输到另一个Channel,也可以使用transferFrom方法将数据从一个Channel传输到另一个Channel。

// 定义源文件和目标文件路径String sourceFilePath = "path/to/source/file.txt";String targetFilePath = "path/to/target/file.txt";// 创建源文件和目标文件的RandomAccessFile对象try (RandomAccessFile sourceFile = new RandomAccessFile(sourceFilePath, "r");RandomAccessFile targetFile = new RandomAccessFile(targetFilePath, "rw")) {// 获取源文件和目标文件的FileChannelFileChannel sourceChannel = sourceFile.getChannel();FileChannel targetChannel = targetFile.getChannel();// 使用transferTo()方法将数据从源文件传输到目标文件// 从源文件的位置0开始,传输全部文件内容到目标文件long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);System.out.println("文件传输成功,传输了 " + transferredBytes + " 字节数据。");// 使用transferFrom()方法将数据从目标文件传输回源文件// 从目标文件的位置0开始,传输全部文件内容回源文件transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());System.out.println("数据回传成功,传输了 " + transferredBytes + " 字节数据。");} catch (IOException e) {  e.printStackTrace();}

3.2.3.阻塞与非阻塞

ServerSocketChannel、SocketChannel支持两种阻塞模式:

  • 阻塞模式,遇到阻塞操作产生阻塞的时候会直接阻塞。
  • 非阻塞模式,遇到阻塞操作产生阻塞的时候会直接返回null。

默认都是阻塞模式,可以手动设置为非阻塞模式。

代码示例:

//准备bufferByteBuffer buffer = ByteBuffer.allocate(1024);//创建服务器ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();//设置为非阻塞模式//serverSocketChannel.configureBlocking(false);//绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));//获取连接,这是一步阻塞操作,阻塞模式下,没读到连接会在这一步阻塞;非阻塞模式下不会阻塞,会直接返回一个nullSocketChannel socketChannel = serverSocketChannel.accept();//设置为非阻塞模式//socketChannel.configureBlocking(false);//读数据,这是一步阻塞操作if(socketChannel!=null) {   //阻塞模式下,没读到连接会在这一步阻塞;非阻塞模式下不会阻塞,会直接返回一个null   socketChannel.read(buffer);}

4.selector

4.1.概述

selector,NIO实现非阻塞式IO的核心,它的功能很简单,就是用事件机制来监听channel,挑选出触发事件的channel。

我们知道,如果线程中有IO操作,IO没有完成,资源没有准备好之前,线程是会进入阻塞状态的。我们可以用单线程起一个selector去监听channel是否准备好数据,将准备好数据的channel挑选出来交给其它线程去处理,这样就不会因为IO资源没准备好导致线程阻塞。

我猜大家看到这里会有一个疑惑,什么时候会有上面描述的这种用线程去并发的处理IO?

网络通信的时候

如果用BIO的方式通信一进来就给一个线程去处理,那么就会有可能因为数据包还没收完,IO等待、阻塞,造成线程阻塞。而用NIO的话就可以用selector挑选出数据包收完的IO出来处理,不会有线程阻塞:

selector的事件监听:

通道的监听事件一共有如下类型:

既可以用常量表示,也可以用数字表示。

  • 读 SelectionKey.OP_READ 1
  • 写 SelectionKey.OP_WRITE 4
  • 连接 SelectionKey.OP_CONNECT 8
  • 接收 SelectionKey.OP_ACCEPT 16
  • 复合 若不止一个事件时,使用或操作符连接

4.2.代码示例

以下是用NIO进行非阻塞式网络通信的代码示例。

服务端:

//获取通道ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();//切换为非阻塞模式serverSocketChannel.configureBlocking(false);//绑定连接的端口serverSocketChannel.bind(new InetSocketAddress(9999));//获取选择器Selector selector=Selector.open();//将通道注册到选择器上,并开始指定监听接收事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//轮询监听while(selector.select()>0){    Iterator iterator=selector.selectedKeys().iterator();    while (iterator.hasNext()){        SelectionKey selectionKey=iterator.next();        //判当前socket的事件        //1.接收事件(表示socket接收到了数据)        if(selectionKey.isAcceptable()){            SocketChannel socketChannel=serverSocketChannel.accept();            //切换为非阻塞模式            socketChannel.configureBlocking(false);            //将通道以读就绪的事件重新注册到选择器            socketChannel.register(selector,SelectionKey.OP_READ);        }        //2.读就绪事件        if(selectionKey.isReadable()){            SocketChannel socketChannel=(SocketChannel)selectionKey.channel();            //读取数据            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);            int length=0;            while((length=socketChannel.read(byteBuffer))>0){                byteBuffer.flip();                System.out.println(new String(byteBuffer.array(),0,length));                byteBuffer.clear();            }        }        //事件处理完成,移除事件        iterator.remove();    }}

客户端:

//获取通道SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));//切换成非阻塞模式socketChannel.configureBlocking(false);//分配缓冲区ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//发送数据Scanner scanner=new Scanner(System.in);while(true){    String msg=scanner.nextLine();    byteBuffer.put(msg.getBytes());    byteBuffer.flip();    socketChannel.write(byteBuffer);    byteBuffer.clear();}

来源地址:https://blog.csdn.net/Joker_ZJN/article/details/131852342

--结束END--

本文标题: 全网最清晰JAVA NIO,看一遍就会

本文链接: https://lsjlt.com/news/388624.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作