NIO个人学习笔记

目录

一、初入NIO

1)是什么?

2)什么不同?

a)Channels 和 Buffers(通道和缓存)

b)Non-blocking IO(非阻塞)

c)Selectors(选择器)

二、概述

1)Channel和Buffer

a)Channel

b)Buffer

2)Selector

三、Channel(通道)

1)Channel的实现

2)示例

四、Buffer(缓存)

1)用法

2)capacity、limit和position

a)capacity

b)position

c)limit

3)Buffer类型

4)示例

五、Scatter/Gather(分散和聚集)

1)Scatter

2)Gather

六、Channel之间的数据传输

1)transferFrom

2)transferTo

3)示例

七、Selector

1)为什么需要selector

2)如何创建Selector

3)如何向Selector中注册Channel

4)示例


一、初入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做基础

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值