admin 管理员组文章数量: 887021
【NIO】
什么是NIO
IO回顾
- IO:Input OutPut(输入 输出)
- IO技术的作用:解决设备和设备之间的数据传输问题
- IO的应用场景:图片上传、下载、打印机打印信息表、解析XML…
概念
- 即
Java New IO
- 是1个全新的、
JDK 1.4
后提供的IO API
- Java API中提供了两套NIO,一套是针对
标准输入输出NIO
,另一套就是网络编程NIO
作用
NIO
和IO
有相同的作用和目的,但实现方式不同- 可替代 标准
Java IO
的IO API
- IO是以流的方式处理数据,而NIO是以块的方式处理数据。
流与块的比较
- NIO和IO最大的区别是数据打包和传输方式。
- IO是以流的方式处理数据,而NIO是以块的方式处理数据。
- 面向流的IO一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节。
- 面向块的IO系统以块的形式处理数据。每一个操作都在一步中产生或消费一个数据块。按块要比按流快的多
新特性
对比于 Java IO
,NIO
具备的新特性如下:
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞(Non Blocking IO) |
/ | 选择器(Selectors) |
- 可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
- 面向流的I/O 系统一次一个字节地处理数据
- 一个面向块(缓冲区)的I/O系统以块的形式处理数据
核心组件
Java NIO
的核心组件 包括:
- 通道(
Channel
) - 缓冲区(
Buffer
) - 选择器(
Selector
)
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据。
Selector是因为NIO可以使用异步的非阻塞模式才加入的东西
简单理解一下:
- Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理! - Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
- 相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的
Buffer缓冲区
Buffer缓冲区概述
作用:缓冲区,用来存放具体要被传输的数据,比如文件、scoket 等。这里将数据装入 Buffer 再通过通道进行传输。
Buffer 就是一个数组,用来保存不同数据类型的数据
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示
- ByteBuffer:存储字节数据到缓冲区
- ShortBuffer:存储字符串数据到缓冲区
- CharBuffer: 存储字符数据到缓冲区
- IntBuffer:存储整数数据到缓冲区
- LongBuffer:存储长整型数据到缓冲区
- DoubleBuffer:存储小数到缓冲区
- FloatBuffer:存储小数到缓冲区
对于 Java 中的基本数据类型,都有一个 Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据)
ByteBuffer的创建方式
-
在堆中创建缓冲区:allocate(int capacity) (最常用)
-
在系统内存创建缓冲区:allocateDirect(int capacity)
-
通过普通数组创建缓冲区:wrap(byte[] arr)
import java.nio.ByteBuffer;public class Demo01Buffer {public static void main(String[] args) {//在堆中创建缓冲区:allocate(int capacity) (最常用)ByteBuffer buffer1 = ByteBuffer.allocate(10);//在系统内存创建缓冲区:allocateDirect(int capacity)ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);//通过普通数组创建缓冲区:wrap(byte[] arr)byte[] arr = {97,98,99};ByteBuffer buffer3 = ByteBuffer.wrap(arr);}
常用方法
拿到一个缓冲区我们往往会做什么?很简单,就是读取缓冲区的数据/写数据到缓冲区中。
所以,缓冲区的核心方法就是:
- put(byte b) : 给数组添加元素
- get() :获取一个元素
import java.nio.ByteBuffer;
import java.util.Arrays;public class Demo02Buffer的方法 {public static void main(String[] args) {//创建对象ByteBuffer buffer = ByteBuffer.allocate(10);//put(byte b) : 给数组添加元素buffer.put((byte)10);buffer.put((byte)20);buffer.put((byte)30);//把缓冲数组变成普通数组byte[] arr = buffer.array();//打印System.out.println(Arrays.toString(arr));//get() :获取一个元素byte b = buffer.get(1);System.out.println(b); //20}
}
核心变量
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息
-
容量Capacity
缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组) -
界限Limit
缓冲区中可以操作数据的大小,代表了当前缓冲区中一共有多少数据(从limit开始后面的位置不能操作)。 -
位置Position
下一个要被读或写的元素的位置。Position会自动由相应的get( )
和put( )
函数更新。 -
标记Mark
一个备忘位置。用于记录上一次读写的位置。就是读写操作之前的position
byteBuffer.put("baidu".getBytes());
System.out.println("第一次put后---->position--->" + byteBuffer.position());// 做一个标记:记录上一次读写位置 position的值 5
byteBuffer.mark();byteBuffer.put("zimu".getBytes());
System.out.println("第二次put后---->position--->" + byteBuffer.position());// 还原到标记位置
byteBuffer.reset();
System.out.println("reset后---->position--->" + byteBuffer.position());
public static void main(String[] args) {// 创建一个缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 看一下初始时4个核心变量的值System.out.println("初始时-->limit--->"+byteBuffer.limit());System.out.println("初始时-->position--->"+byteBuffer.position());System.out.println("初始时-->capacity--->"+byteBuffer.capacity());System.out.println("初始时-->mark--->" + byteBuffer.mark());System.out.println("--------------------------------------");// 添加一些数据到缓冲区中String s = "JavaEE";byteBuffer.put(s.getBytes());// 看一下初始时4个核心变量的值System.out.println("put完之后-->limit--->"+byteBuffer.limit());System.out.println("put完之后-->position--->"+byteBuffer.position());System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());System.out.println("put完之后-->mark--->" + byteBuffer.mark());}
运行结果:
现在我想要从缓存区拿数据,怎么拿呀??NIO给了我们一个flip()
方法。这个方法可以改动position和limit的位置!
还是上面的代码,flip()
一下后,再看看4个核心属性的值会发生什么变化:
byteBuffer.flip();
System.out.println("flip完之后-->limit--->"+byteBuffer.limit());
System.out.println("flip完之后-->position--->"+byteBuffer.position());
System.out.println("flip完之后-->capacity--->"+byteBuffer.capacity());
System.out.println("flip完之后-->mark--->" + byteBuffer.mark());
运行结果:
- limit变成了position的位置了
- 而position变成了0
- 每当要从缓存区的时候读取数据时,就调用
filp()
“切换成读模式”。
读取缓冲区的数据
// 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
byte[] bytes = new byte[byteBuffer.limit()];// 将读取的数据装进我们的字节数组中
byteBuffer.get(bytes);// 输出数据
System.out.println(new String(bytes, 0, bytes.length));
运行结果:
随后输出一下核心变量的值:
读完我们还想写数据到缓冲区,那就使用clear()
函数,这个函数会“清空”缓冲区
数据没有真正被清空,只是被遗忘掉了,此时继续执行byteBuffer.get(bytes);还是可以获取到javaEE
Channel通道
Channel通道概述
通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中(白话: 就是数据传输用的通道,作用是打开到IO设备的连接,文件、套接字都行)
Channel API
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来 的连接都会创建一个 SocketChannel。
FileChannel基本使用
- 使用FileChannel完成文件的复制
public class CopyTest {public static void main(String[] args) throws Exception {// 通道依赖于IO流FileInputStream fileInputStream = new FileInputStream("D:\\初音未来.jpg");FileOutputStream fileOutputStream = new FileOutputStream("D:\\复制.jpg");// 使用流获取通道FileChannel channel = fileInputStream.getChannel();FileChannel channel1 = fileOutputStream.getChannel();// 创建缓冲数组ByteBuffer byteBuffer = ByteBuffer.allocate(1024);while (channel.read(byteBuffer) != -1) {byteBuffer.flip();channel1.write(byteBuffer);byteBuffer.clear();}fileOutputStream.close();fileInputStream.close();}
}
网络编程收发信息
客户端
public class NIOClient {public static void main(String[] args) throws IOException {// 创建客户端通道,使用SocketChannel.open()方法SocketChannel socketChannel = SocketChannel.open();// 指定要连接的服务器地址和端口,使用connect(SocketAddress remote)方法// 参数SocketAddress是抽象类,使用其实现类InetSocketAddress构造socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(), 1080));// 缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);// 编写数据buffer.put("你好,NIO!".getBytes());// 调整position和limit,否则写数据的时候有问题buffer.flip();// 输出数据socketChannel.write(buffer);// 释放资源socketChannel.close();}
}
服务器端
public class NIOServer {public static void main(String[] args) throws IOException, InterruptedException {// 创建服务器通道,使用ServerSocketChannel.open()方法ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 绑定端口,使用bind(SocketAddress local)// 参数SocketAddress是抽象类,使用其实现类InetSocketAddress构造serverSocketChannel.bind(new InetSocketAddress(1080));// 设置非阻塞,ttue为阻塞serverSocketChannel.configureBlocking(false);while (true) {// 等待客户端连接SocketChannel accept = serverSocketChannel.accept();if (accept != null) {// 缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 读取数据,读的是缓冲区中有数据的长度int len = accept.read(byteBuffer);// 打印,将缓冲区转为数组并转成字符串System.out.println(new String(byteBuffer.array(), 0, len));break;} else {System.out.println("做一些别的事");Thread.sleep(5000);}}}
}
Selector选择器
多路复用的概念
一个选择器可以同时监听多个服务器端口, 帮多个服务器端口同时等待客户端的访问
Selector的和Channel的关系
选择器(Selector) 是 Channel(通道)的多路复用器,Selector 可以同时监控多个 通道的 IO(输入输出) 状况。
选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
可选择通道(SelectableChannel)
注意:并不是所有的Channel,都是可以被Selector 复用的。比方说,FileChannel就不能被选择器复用。
判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能。
SelectableChannle 的结构如下图:
SelectableChannel类提供了实现通道的可选择性所需要的公共方法
通道和选择器注册之后,他们是绑定的关系吗?
答:不是。不是一对一的关系。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。
Channel注册到Selector
使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。
第一个参数:指定通道要注册的选择器是谁
第二个参数:指定选择器需要查询的通道操作
可以供选择器查询的通道操作,从类型来分,包括以下四种:
(1)可读 : SelectionKey.OP_READ
(2)可写 : SelectionKey.OP_WRITE
(3)连接 : SelectionKey.OP_CONNECT
(4)接收 : SelectionKey.OP_ACCEPT
如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
Selector的使用流程
// 1、获取Selector选择器
Selector selector = Selector.open();// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
上面通过调用通道的register()方法会将它注册到一个选择器上。
需要注意的是:
与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException
选择键(SelectionKey)
Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。
select() :选择器等待客户端连接的方法
阻塞问题:
1.在开始没有客户访问的时候是阻塞的
2.在有客户来访问的时候方法会变成非阻塞的
3.如果客户的访问被处理结束之后,又会恢复成阻塞的
selectedKeys() :选择器会把被连接的服务端对象放在Set集合中,这个方法就是返回一个Set集合
轮询查询就绪操作
通过Selector的 select() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。
select()方法返回的int值,表示有多少通道已经就绪
而一旦调用select()方法,并且返回值不为0时,下一步工干啥?
通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作
NIO 编程实例:
客户端
public static void main(String[] args) throws IOException {//创建客户端SocketChannel sc = SocketChannel.open();//指定要连接的服务器ip和端口sc.connect(new InetSocketAddress("127.0.0.1",9000));//创建缓冲输出ByteBuffer buffer = ByteBuffer.allocate(1024);//给数组添加数据buffer.put("百度".getBytes());//切换buffer.flip();//输出数据sc.write(buffer);//关闭资源sc.close();}
服务端
public class Selector服务端 {public static void main(String[] args) throws IOException {// 1、获取Selector选择器Selector selector = Selector.open();// 2、获取通道ServerSocketChannel ssc1 = ServerSocketChannel.open();ServerSocketChannel ssc2 = ServerSocketChannel.open();ServerSocketChannel ssc3 = ServerSocketChannel.open();// 3.设置为非阻塞ssc1.configureBlocking(false);ssc2.configureBlocking(false);ssc3.configureBlocking(false);// 4、绑定连接ssc1.bind(new InetSocketAddress(8000));ssc2.bind(new InetSocketAddress(9000));ssc3.bind(new InetSocketAddress(10000));// 5、将通道注册到选择器上,并注册的操作为:"接收"操作ssc1.register(selector, SelectionKey.OP_ACCEPT);ssc2.register(selector, SelectionKey.OP_ACCEPT);ssc3.register(selector, SelectionKey.OP_ACCEPT);// 6、采用轮询的方式,查询获取"准备就绪"的注册过的操作while (selector.select() > 0) {// 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();while (selectedKeys.hasNext()) {// 8、获取"准备就绪"的事件SelectionKey selectedKey = selectedKeys.next();// 9、获取ServerSocketChannelServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectedKey.channel();// 10、接受客户端发来的数据SocketChannel socketChannel = serverSocketChannel.accept();// 11、读取数据ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int length = 0;while ((length = socketChannel.read(byteBuffer)) != -1) {byteBuffer.flip();System.out.println(new String(byteBuffer.array(), 0, length));byteBuffer.clear();}socketChannel.close();}// 12、移除选择键selectedKeys.remove();}// 13、关闭连接ssc1.close();ssc2.close();ssc3.close();}}
本文标签: NIO
版权声明:本文标题:【NIO】 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1687836881h147425.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论