• 30分钟掌握NIO


    NIO对文件的操作 

    先来看一段BIO也就是传统IO复制文档的代码片段。

       public static void bioCopy() throws Exception {
            //读取这个文件
            FileInputStream in = new FileInputStream("C:\Users\szwb295\Desktop\1.txt");
            //往这个文件写入内容
            FileOutputStream out = new FileOutputStream("C:\Users\szwb295\Desktop\2.txt");
    
    
            //输入流缓冲区
            BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
            //输出流缓冲区
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out);
    
            //临时保存字节,字节数组的 Capacity(容量) 为1024 最多一次放1024个字节
            byte[] b = new byte[3];
    
            //一次读取到的字节数量
            int len;
            /*
             * 例如 b的Capacity为3,总共需要读取的数据10,则read()方法会执行5次len分别为3,3,3,1,-1
             * */
            while ((len = bufferedInputStream.read(b)) != -1) {
                //将每次放入字节数组中的数据写入到流中,position(位置),0开始写,最大限度(limit)为len
                bufferedOutputStream.write(b, 0, len);
            }
    
            //关闭流
            bufferedInputStream.close();
            bufferedOutputStream.close();
    
        }
    View Code

    下面看NIO复制文件代码片段。

     public static void nioCopy() throws Exception {
    
            // 1. 输入流
            FileInputStream fin = new FileInputStream("C:\Users\szwb295\Desktop\a.txt");
    
            //输出流
            FileOutputStream fout = new FileOutputStream("C:\Users\szwb295\Desktop\b.txt");
    
            // 2. 获取数据源的输入输出通道
            FileChannel fcin = fin.getChannel();
    
    
            FileChannel fcout = fout.getChannel();
    
            // 3. 创建 缓冲区 对象 参数为Buffer的  Capacity(容量)代表这个缓冲区最大存储字节数
            ByteBuffer buff = ByteBuffer.allocate(3);
    
    
            int len;
            // 4. 从通道读取数据 & 写入到缓冲区
            // 注:若 以读取到该通道数据的末尾,则返回-1
            while ((len = fcin.read(buff)) != -1) {
                // 5. 将 limit设置为当前位置然后将 position设置为0
                buff.flip();
    
                // 6. 从 Buffer 中读取数据 & 传出数据到通道
                fcout.write(buff);
    
                //将 limit设置为当前位置然后将 position设置为0
                buff.flip();
            }
    
            //直接关闭FileInputStream会自动关闭channel
            fin.close();
            //同上
            fout.close();
        }
    View Code

    以上NIO代码片段包含两部分内容 Buffer(缓冲区)和Channel(通道)。

    我们从InputStream和OutputStream中获取两个通道,通道是连接输入/输出的一端,另一端则是Buffer。

    例如 fcin.read(buff)这一行代码是从磁盘中将文件内容读取到Buffer。fcout.write(buff);则是从缓冲区将数据写入到磁盘中。如下图

    Channel有多种类型,以上我们通过InputStream/OutputStream获取的是对文件操作的管道。在API文档中还有针对Socket的通道 ServerSocketChannel SocketChannel

    ByteBuffer buff = ByteBuffer.allocate(3);创建一个字节类型的缓冲区,其Capacity为3,也就是最大容量为3个字节

    Buffer有三个重要的属性:Capacity(容量)代表一个缓冲器最多存储的内容。Position(标记位)一个指针,指向下一个操作的位置,

    例如读一个Capacity为10的Buffer,起始位的Position是0每读一个位置增加1。若Position为10则不能继读,0-9。Limit(限制位)代表可以对当前缓冲区操作的最大位置-1

    ,例如当前缓冲区的Capacity为10,若Limit为5则表示无论是读或写只能到4的位置。

    接下来的while循环读,就和BIO的逻辑一样了,若读的返回值不是-1则表示有值,读到的数据放到Buffer中,若Buffer满了,则进入循环体。此时Position,Capacity,Limit一样。

    接下来我们要执行写操作,将Buffer中的数据写入到输出流中,我们执行了。flip()方法。该方法会将Limit更改为Position,然后将Position更改为0,表示从0开始,直到Position和Limit

    一致则表示已经把缓冲区的数据全部复制到输入流了。此时我们又要往缓冲区写数据,所以在调用flip()将Position更改为0,表示从0开始写入缓冲区,直到Position和Limit。然后诸如

    此循环。

    其实上边代码循环体最后一个flip()方法使用的不得当。思考一个问题,设缓冲区Capacity为10,若最后一次往Buffer中放数据

    的个数为5在循环体内取数据也是5.那么最后一个flip()会将Limit置为5。然后循环结束。但如果我们在循环外想在此使用该Buffer

    那无论是读或写最多只能操作5个空间的位置,因为Limit()为5。

    Buffer还有两个方法。clear()这个方法将Limit置为Capacity,将Position置为0。rewind()这个方法Limit位置不变,只将Position置为0。

    NIO网络编程

    关于BIO网络编程,也就是Socket的代码已经弊端,在我的上一篇博客中有写到,感兴趣的可以去看。这里直接贴出来NIO的网络编程。

    package com.datang.pet.control.test;
    
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    
    public class NIOClient {
        public static void main(String[] args) throws Exception {
    
            for (int i = 0; i < 20; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        send();
                    }
                }.start();
            }
        }
    
        public static void send() {
            try {
                //获取一个客户端的通道
                SocketChannel clientChannel = SocketChannel.open();
    
                //设置非阻塞客户端
                clientChannel.configureBlocking(false);
    
                //服务端的地址
                InetSocketAddress serverAddr = new InetSocketAddress("localhost", 9999);
    
                /*
                 * 当connect()正确的连接上服务端后,才会返回true
                 * 因为上边设置了非阻塞的,意味着无论连接成功或者
                 * 失败都会执行后续代码,若连接失败则后边发送数据
                 * 也会失败。
                 * finishConnect()则是刷新连接,尝试重新连接。
                 * 如果连接成功返回 true。
                 * */
                if (!clientChannel.connect(serverAddr)) {
                    while (!clientChannel.finishConnect()) {
                        System.out.println("正在尝试重新连接服务端");
                        continue;
                    }
                }
    
    
                ByteBuffer byteBuffer = ByteBuffer.wrap("我是客户端".getBytes("UTF-8"));
    
                //发送消息
                clientChannel.write(byteBuffer);
    
                System.out.println("已发送消息");
    
                //将缓存区的limit设置为capacity,将position设为0
                byteBuffer.clear();
    
                //获取服务端的响应信息
                clientChannel.read(byteBuffer);
    
                System.out.println(new String(byteBuffer.array(),"UTF-8"));
    
                System.in.read();
    
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
    View Code

    上边代码片段为客户端代码。为了模拟多客户的情况,使用了循环+线程模拟。

    clientChannel.configureBlocking(false);这行代码设置了客户端的是否是阻塞的。如果true,则表示是阻塞客户端

    意味着在调用 clientChannel.connect(serverAddr)时,如果连接不上服务端,则会一直阻塞,直到超时。我们选择

    的是非阻塞客户端。这意味着调用 clientChannel.connect(serverAddr) 无论是否连接上都会继续执行剩余代码。

    那肯定是会报错的,没有连接上服务端,发送接收消息都会失败。所以我们使用while循环,如果连接失败则尝试重新连接。

    剩下的代码逻辑上和Socket并无差别。

    package com.datang.pet.control.test;
    
    
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    import java.util.Set;
    
    public class NIOServer {
        public static void main(String[] args) throws Exception {
            //服务端channel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            //非阻塞的服务端
            serverChannel.configureBlocking(false);
            //绑定服务端端口号
            serverChannel.bind(new InetSocketAddress(9999));
    
            //多路复用器 selector
            Selector selector = Selector.open();
    
            //将服务端channel注册到多路复用器,并告诉selector,这个管道对“接受客户端请求的事件感兴趣”
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            //循环不停的执行select()
            while (true) {
                /* 轮询监听 有 Channel就绪,所谓的就绪就是 channel感兴趣的事件来了。
                 *  如果select()返回值是0,则表示没有就绪的channel
                 * */
                if (selector.select(1000) == 0) {
                    System.out.println("没有channel准备就绪");
                    continue;
                }
    
                //能走到这说明有就绪的channel,一个channel有一个唯一的SelectionKey。
                //这个集合包含所有就绪的channel。
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
    
                //注意这里一定要用迭代器循环,不要用增强for。很多博客上直接用的增强for,其实会有bug。
    
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
    
                //遍历所有就绪的key
                while (iterator.hasNext()) {
    
                    SelectionKey key = iterator.next();
    
                    //如果当期key的类型为 OP_ACCEPT,说明可以接受客户端连接了。
                    if (key.isAcceptable()) {
                        System.out.println("-------------接受到Client的连接了-----------------");
    
                        //获取一个客户端的 channel
                        SocketChannel clientChannel = serverChannel.accept();
                        //设置客户端为非阻塞
                        clientChannel.configureBlocking(false);
                        //获取到客户端channel后,需要将其注册到多路复用器,并告诉selector,这个管道对“我对读事件感兴趣”
                        clientChannel.register(selector, SelectionKey.OP_READ);
    
                    }
    
                    //如果当期key的类型为 OP_READ,说明可以读客户端的数据了。
                    if (key.isReadable()) {
                        try {
                            System.out.println("----------------channel可读了----------------");
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //注意,当前类里的操作都是异步的,以为这,当前这个key,上边的key肯定不是同一个。
                            //这个key是上一个里获取到的客户端channel。
                            SocketChannel clientChannel = (SocketChannel) key.channel();
                            //将数据读到Buffer
                            clientChannel.read(buffer);
                            //将buffer转换为字节数组
                            byte[] array = buffer.array();
    
                            System.out.println(new String(array, "UTF-8"));
    
                            //向客户端发送消息
                            clientChannel.write(ByteBuffer.wrap("我是服务端".getBytes()));
                        } catch (Exception e) {
                            //如果客户端关闭了,会走到这里,则调用cancel关闭此通道
                            key.cancel();
                        }
                    }
                    //防止重复执行 已准备好的key
                    iterator.remove();
                }
    
            }
    
        }
    
    
    }
    View Code

    上边代码片段是服务端代码。首先我们需要关注的是创建服务端,设置非阻塞服务端,绑定端口。接下来Selector多路复用器就要

    出场了。Selector.open();获取一个多路复用器。serverChannel.register(selector, SelectionKey.OP_ACCEPT);服务端管道将

    自身注册到Selector中,并且添加感兴趣的事件,事件类型为接受客户端的连接。

    接下来while(true)在循环中调用 select(1000)方法,此方法是阻塞方法,参数则为阻塞的毫秒数。select(1000)方法,轮询的去查看

    当前注册到selector中的channel有没有准备就绪的,也就是有没有触发相应的事件。如果1秒内有准备就绪的channel。则拿到准备

    就绪的集合。这里需要说明一点,所有注册到selector的channel都变成了一个SelectionKey,该对象有channel的所有特征。

    接下来拿到set的迭代器。遍历迭代器,判断SelectionKey的事件类型。对于一个客户端连接来说,最开始触发的肯定是 isAcceptable()

    也就是OP_ACCEPT,然后调用服务端channel的 accept()获取一个客户端连接channel,这里就类似bio的socket了。同样的,我们需要

    将这个客户端channel注册到selector中,此时我们关注的事件类型是OP_READ,可读。也就是说当一个客户端连接到达后,我们需要

    读取客户端发来的消息,但是这个读取本身是阻塞的,在bio中我们做个测试。所以我们将这个channel注册到selector中,并且告诉

    selector如果这个客户端channel可以读了,你告诉我。剩下的就是等待可读后,再次遍历SelectionKey。selector流程类似下图。

    最后我们要注意两点。在while迭代的最后,我们将当前的key从迭代器中移除了,为什么要移除?思考一下,如果当前的key为OP_ACCEPT,代表客户端连接了,我们获取到客户端连接后,如果不将

    此key从准备就绪的 selectedKeys中移除掉,那再次执行select()方法这个key依然是准备就绪的状态,会再次的到 if(key.isAcceptable()) 此时就出现问题了,同样的一个客户端被我们

    获取了两次channel,报错!。

    最后一点,在可读事件执行的代码块中,用try{}catch(key.cancle()),因为当客户端关闭后,同样会发送一条消息,该消息会被识别到OP_READ,但是此时客户端已经关闭,读取会报错。所以

    执行cancle()表示将该key,对应的channel移除掉,表示会话结束。

    以上是NIO的基础使用,学习NIO是为了学习NIO框架Netty。如有错误,望指正。

  • 相关阅读:
    升级:Logical Upgrade升级MySQL5.6.26
    基于GTID恢复误篡改数据
    mysql迁移:ibd表空间迁移库表
    mysql迁移:xtrabackup迁移mysql5.7.32
    Pycharm最新激活码2019
    转载:Prometheus+Grafana搭建HBase监控仪表盘
    总结《Elasticsearch源码解析和优化实战》第一讲
    yarn resourcemanager调优
    presto安装和集成kerberos的hive
    转载:shell expect实战实例
  • 原文地址:https://www.cnblogs.com/zumengjie/p/13158442.html
Copyright © 2020-2023  润新知