NIO个人学习笔记
目录
一、初入NIO
1)是什么?
Java NIO是一个可以替代传统IO的一个IO API,它是从jdk1.4开始引入的,NIO提供了与传统IO不同的工作方式
2)什么不同?
传统的IO是通过读取到内存,然后在jvm中开辟一个堆内存空间,再通过复制/映射,读的时候从内存映射到jvm中,写的时候jvm映射到内存中,并且这个过程是独占锁去操作,是一个完全阻塞对的过程。
NIO不同在于引入了以下三个概念:
a)Channels 和 Buffers(通道和缓存)
标准IO是基于字节流和字符流进行操作的,而NIO是基于通道和缓存进行操作的,数据是从通道读到缓冲区,缓冲区写到通道。
b)Non-blocking IO(非阻塞)
NIO可以让你非阻塞式的使用IO;其实可以理解为让线程从通道读到缓冲区的时候,线程还可以做其他事情,线程从缓冲区写到通道的时候,线程也可以做其他事情。
c)Selectors(选择器)
NIO引入选择器这个概念,是为了监听多个通道的事件,比如:数据到达、连接打开等等;所以单个线程是可以监听多个通道的数据。
二、概述
NIO虽然有很多类和组件,但是核心却是由Channel、Buffer和Selectors组成。
1)Channel和Buffer
为什么要把这两个放在一起呢?首先看下下面这个图你也许就能够明白了
Channel和Buffer在NIO中有很多种类型,下面说一些主要的实现
a)Channel
-
FileChannel
-
SocketChannel
-
ServerSocketChannel
-
DatagramChannel
正如我们常用的,这些通道基本上覆含了文件、网络这两大类的IO
b)Buffer
-
ByteBuffer
-
CharBuffer
-
DoubleBuffer
-
FloatBuffer
-
IntBuffer
-
LongBuffer
-
ShortBuffer
2)Selector
Selector的存在就允许单个线程去处理出多Channel,为什么呢?当我们看到下面这个图解就能明白了
正如我们所看到的,要使用selector去管理channel就必须先把channel注册到selector中去,然后在去调用它的select()方法。select()这个方法一直会阻塞到某个channel有事件就绪,一旦这个方法返回了,那么线程就能去处理这个事件了
三、Channel(通道)
为什么不先说Buffer呢?因为我们的Buffer区都是从Channel中读过来的,Channel才是真正的“外交官”。它和传统的流有点一样但是又不太一样,我们可以这样理解,自来水和水管的区别,流只能单向,而Channel是可以双向的。不像流那样直接读写,而是先读到缓存中,再有缓存中写入。
1)Channel的实现
-
FileChannel:从文件中读写数据
-
SocketChannel:通过TCP读写网络中的数据
-
ServerSocketChannel:可以监听网络中新额TCP连接,它会对每个新进来的连接建立一个SocketChannel
-
DatagramChannel:通过UDP读写网络中的数据(不要问为啥UDP没有监听,因为它是一个无连接的网络通信协议)
2)示例
因为Channel和Buffer是要一起使用的,所以实例中可能会有Buffer
@Test public void Channel() throws Exception { File file = new File(""); FileInputStream inputStream = new FileInputStream("123.png"); FileChannel channel = inputStream.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024);//jvm中创建缓冲区 // ByteBuffer.allocateDirect(1024); 从内存中创建 int read = channel.read(buffer); while (read != -1){ System.out.println("读到了:" + read + "长度"); /** * 源码: * limit = position; * position = 0; * mark = -1; */ buffer.flip();//翻转模式,之前buffer是读模式,现在要切换到写模式 while (buffer.hasRemaining()){ System.out.println(buffer.get()); } /** * 源码: * position = 0; * limit = capacity; * mark = -1; */ buffer.clear(); read = channel.read(buffer); } }
注意:这里调用get()方法之前调用了flip()方法,这个是必须的。具体说明请看buffer章节
四、Buffer(缓存)
当文件被读到Channel后,想要写入到另外一个地方,但是这不能直接写入的,这里就需要Buffer。缓冲器本质上是一块可以被写入数据,也可以被读取数据的内存;这块内存被NIO包装后就成为了Buffer对象。
1)用法
正如我们的上一个实例,
1.通过Channel的read()方法把数据写到Buffer中(对channel来说是读,对buffer来说是写)。
2.调用Buffer的flip()方法
3.从Buffer中获取数据
4.清空缓冲区
当我们把数据写到buffer中去,buffer会记录下我们写了多少数据进去,这时候buffer是读模式,我们要获取buffer中的数据就需要切换的读模式,这里就需要调动flip()方法进行模式的切换。
一旦读完所有的数据,我们就需要清空缓存中的数据,这样是为了让它可以再次被写入;这里有两个方法:
1.clear(),清除所有的数据(limit = capacity, position = 0)
2.compact(), 清除已经读的数据(这里会计算capacity和limit的差 然后赋值给position,建议去看下源码好理解一点)
2)capacity、limit和position
在用法上面说到了几个方法,他们都去改变了limit、position的值。这里就拿这几个属性来说下Buffer的原理。
其实buffer的底层就是数组去实现的,它额外映入了mark,position、limit、capacity概念。
a)capacity
它所记录的就是我们一开始开辟内存的大小,这个是固定的,一旦被写满后就需要清除,如果没有清除的话再写入数据就睡报错
b)position
position的初始值是0,当我们写入数据的时候position会记录我们把数据写到哪个位置了,当buffer从写模式切换到读模式后flip()方法,position值会立马被设置为0。
c)limit
在写模式下,limit就等于capacity的值,这表示我们能写多少个数据到buffer中去;当切换到读模式下,limit就是写模式下的position的值,这表示我们最多能读出来多少数据
3)Buffer类型
-
ByteBuffer
-
MappedByteBuffer
-
CharBuffer
-
DoubleBuffer
-
FloatBuffer
-
IntBuffer
-
LongBuffer
-
ShortBuffer
4)示例
Buffer的方法有很多,我们通过代码来看一下
@Test public void testBuffer() throws Exception{ FileChannel inChannel = FileChannel.open(Paths.get("123.png"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("124.png"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); //分配缓冲区大小 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //向buffer中写数据 int read = inChannel.read(byteBuffer); while (read != -1){ //反转模式 写模式到读模式 byteBuffer.flip(); //向通道里面写数据 outChannel.write(byteBuffer); //清除缓冲区 byteBuffer.clear(); read = inChannel.read(byteBuffer); } inChannel.close(); outChannel.close(); }
五、Scatter/Gather(分散和聚集)
Scatter和Gather分别用于描述充Channel中读取和写入。
1)Scatter
Scatter是分散,是从Channel中读取写入到不同的Buffer中。
代码实例
FileChannel inChannel = FileChannel.open(Paths.get("123.png"), StandardOpenOption.READ); ByteBuffer allocate1 = ByteBuffer.allocate((int) inChannel.size()); ByteBuffer allocate2 = ByteBuffer.allocate((int) inChannel.size()); ByteBuffer[] arr = {allocate1, allocate2}; inChannel.read(arr);
注:首先将所有的Buffer放到一个数组中,然后将数组做为参数传入到read方法中,read方法会按数组的顺序一个一个的将数据写入到Buffer中
2)Gather
Gather是将多个个Buffer中的数据写入到一个Channel中
代码实例
FileChannel outChannel = FileChannel.open(Paths.get("124.png"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); ByteBuffer allocate1 = ByteBuffer.allocate((int) inChannel.size()); ByteBuffer allocate2 = ByteBuffer.allocate((int) inChannel.size()); outChannel.write(arr);
arr作为参数传入到write方法中,这里写入也会按照数组的顺序依次写入,但是这里写入只会读取Buffer position到limit中的数据
六、Channel之间的数据传输
当我们创建好Channel之后,我们就可以在两个Channel之间进行数据的传输,但是必须又有个Channel是FileChannel
它主要通过两个方法去实现
1)transferFrom
/** *src 元数据来自哪里 *position 从哪个位置开始写入 *count 写入多少 */ public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
2)transferTo
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
3)示例
FileChannel inChannel = FileChannel.open(Paths.get("123.png"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("124.png"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); inChannel.transferTo(0, inChannel.size(), outChannel); outChannel.transferFrom(inChannel, 0, inChannel.size());
七、Selector
selector为我们提供了一个线程监控多个Channel,并能够知道某个通道具体干嘛,这样就让我们能够监听多个网络连接
1)为什么需要selector
首先我们的内存是有限的,多一条线程可能没什么太大的影响,但是线程越来越多的话,那么内存势必是不够用的。当用一个线程就可以管理所有Channel的时,那内存开销是不是就减少了很多呢?而且还省去了多个线程在CPU中的切换。
2)如何创建Selector
Selector open = Selector.open();
3)如何向Selector中注册Channel
之前说过,要像selector管理Channel就必须让Channel注册到selector中去。
Selector selector = Selector.open(); SocketChannel socketChannel = SocketChannel.open(); //设置为非阻塞模式 socketChannel.configureBlocking(false); //完成注册 //这里注册需要设置好监听的对象 //OP_CONNECT 连接 //OP_ACCEPT 同意 //OP_READ 读 //OP_WRITE 写 socketChannel.register(selector, SelectionKey.OP_READ, SelectionKey.OP_ACCEPT);
注:这里不能使用FileChannel, 应为FileChannel不能被设置为非阻塞模式
4)示例
我们之前写的都是基于阻塞模式下的NIO,那么NIO是非阻塞的一个IO,那我们具体需要怎么做?
首先我们不能使用FileChannel,因为上面也说了FileChannel是没有方法把它切换到非阻塞模式下的,那么我们以SocketChannel为例说明
先写一个阻塞模式下的完全示例
@Test public void client() throws IOException { SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888)); //分配内存空间 ByteBuffer buffer = ByteBuffer.allocate(1024); System.out.println(LocalDateTime.now().getDayOfWeek().toString()); //读取数据 buffer.put("发送数据".getBytes()); //一定要切换模式,不然在write的时候是没有数据内容的 buffer.flip(); //发送到服务器 int write = socketChannel.write(buffer); System.out.println(new String(buffer.array(), 0, write)); //关闭通道 socketChannel.close(); } @Test public void server() throws IOException{ ServerSocketChannel channel = ServerSocketChannel.open(); //监听端口号 channel.bind(new InetSocketAddress(8888)); //获取channel SocketChannel socketChannel = channel.accept(); //分配缓冲区大小 ByteBuffer buffer = ByteBuffer.allocate(1024); //读到buffer中 int length = socketChannel.read(buffer); //切换模式 buffer.flip(); //写出buffer中数据 System.out.println("22" + new String(buffer.array(), 0, length)); //关闭通道 channel.close(); socketChannel.close(); }
为什么说这个是阻塞的呢,当我们服务端没有收到的时候,线程就一直在等待,那么就不能去干别的事情,客户端也是一样的
加入selector后的非阻塞式
@Test public void send() throws IOException { SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888)); socketChannel.configureBlocking(false); //分配内存空间 ByteBuffer buffer = ByteBuffer.allocate(1024); System.out.println(LocalDateTime.now().getDayOfWeek().toString()); //读取数据 buffer.put(LocalDateTime.now().getDayOfWeek().toString().getBytes()); //一定要切换模式,不然在write的时候是没有数据内容的 buffer.flip(); //发送到服务器 int write = socketChannel.write(buffer); System.out.println(new String(buffer.array(), 0, write)); //关闭通道 socketChannel.close(); } @Test public void recver() throws IOException { ServerSocketChannel channel = ServerSocketChannel.open(); //监听端口号 channel.bind(new InetSocketAddress(8888)); //获取channel channel.configureBlocking(false); Selector selector = Selector.open(); channel.register(selector, SelectionKey.OP_ACCEPT); //用while循环是因为while循环可以改变长度 for循环做不到 while (selector.select() > 0){ Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey selectionKey = iterator.next(); if (selectionKey.isAcceptable()){ SocketChannel socketChannel = channel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()){ SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len = 0; if (( len = socketChannel.read(byteBuffer)) > 0){ byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0, len)); byteBuffer.clear(); } } } //一定要romove 不然会一直有 iterator.remove(); } selector.close(); }
注意,这里的selector是阻塞的
public void SelectorTest() throws Exception{ Selector selector = Selector.open(); SocketChannel socketChannel = SocketChannel.open(); //设置为非阻塞模式 socketChannel.configureBlocking(false); //完成注册 //这里注册需要设置好监听的对象 //OP_CONNECT 连接 //OP_ACCEPT 同意 //OP_READ 读 //OP_WRITE 写 socketChannel.register(selector, SelectionKey.OP_READ, SelectionKey.OP_ACCEPT); while(true) { int readyChannels = selector.selectNow(); if (readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } } }
这只是一个学习笔记而已。为学习netty做基础