首页 课程 师资 教程 报名

关于NIO的详解

  • 2022-12-12 11:00:11
  • 281次 星辉

什么是NIO

传统的BIO(阻塞式IO)是面向"流"的。流的特点:顺序的读写进行,并且是单项的(要么用于读,要么用于写)

所谓阻塞式IO指的是当我们在读写数据时,数据没有准备好之前,程序(线程)会进入阻塞状态等待数据准备完毕后再进行读写。

非阻塞IO:NIO是面向"通道"的。通道的特点:又能读又能写。基于一个缓冲区Buffer进行读写操作的。

在读写数据时,若数据还没有准备完毕,此时程序(线程)会继续向后执行其他逻辑并不会在读写方法这里阻塞。

NIO真正解决的问题是网络上IO操作的阻塞问题。

NIO中的缓存机制

如果此时希望再次从通道中读取数据,最多还可以读取 limit - position = 5 个字节。

如果此时我们希望将buffer中的数据进行写出操作channcl.write(buffer),写出操作的位置任然是:position到limit这段可用空间,此时这部分的空间中是没有任何数据的。理解为:读或写操作都是针对buffer空用空间进行的。

因此,当我们利用某个通道将数据读取到缓冲区buffer中,不能立即使用该缓冲区做出写出操作,因为此时的buffer中position - limit 之间并非表达的是之前读入缓冲区中的数据(如上图所示)。

所以,当我们需要写出缓冲区的数据时,我们需要修改position和limit的值,让包含数据的区域在他们之间。

Buffer中有一个重要的方法:flip() 查看源码如下:

当我们在写操作前若调用flip()方法后,缓冲区变化如下:

flip()方法调用完毕后,当前buffer的可操作空间(position到limit之间的内容)就变成了刚才读取进buffer中的数据了。

此时我们再调用channcl.write(buffer)方法,就可以写出缓冲中的数据。当写出操作完成后,position和limit的值会相等,如下图所示:

在这种情况下,无论写出还是读取操作都是没办法完成的。那么我们可以通过再次调用flip()方法将position的值等于0来解决这个问题吗?

答案是:如果只是为了写出同样的数据,那么再次调用flip()方法是可以的。但是如果为了读取数据(比如下次需要读取10个字节的数据),再次调用flip()方法是不可行的。因为原本我们缓冲区的大小是10个字节,可以操作的部分却只是position到limit之间的部分(0-5),也就是说原本我们可以一次将10字节全部写入缓冲,这个时候需要2次,后面5个字节的空间被浪费了。

buffer中有一个重要方法buffer.clear(),该方法的主要作用是:

调用该方法后缓冲中的状态如下图所示:

NIO核心API的使用demo

public class NIODemo {
    public static void main(String[] args) throws Exception {
        /*
            从文件的NIO的API介绍功能
            但是实际上读写文件是不会出现阻塞的,通过当前案例我们用于了解NIO的核心API
            使用。
            已文件复制来进行演示
         */
        /*
            传统的BIO(阻塞式IO)是面向"流"的。
            流的特点:顺序的读写进行,并且是单项的(要么用于读,要么用于写)
            NIO(非阻塞式IO)是面向"通道"的。
            通道的特点:又能读又能写。基于一个缓冲区Buffer进行读写操作的。
         */
        //用于读取文件数据的文件输入流
        FileInputStream fis = new FileInputStream("./image.jpg");
        //用于向文件中写出数据的文件输出流
        FileOutputStream fos = new FileOutputStream("./image_cp.jpg");
        /*
            获取用于操作两个文件数据的通道
            对于读写文件而言,获取通道的方式实际上是通过流获取的。
         */
        FileChannel srcCh = fis.getChannel();
        FileChannel descCh = fos.getChannel();
        /*
            创建一个字节缓冲区,里面存放的数据都是字节。
            可以使用ByteBuffer的静态方法allocate指定缓冲区大小。
            这个缓冲区可以理解为相当于原来我们使用流读写时自行创建的字节数组。
         */
        /*
            buffer中:
            position=0    当前缓冲区可操作字节的位置
            limit=10240   当前缓冲区最后一个可操作字节的位置
            capacity=10240 容量
            缓冲区中可操作的范围:position到limit之间的范围
         */
        ByteBuffer buffer = ByteBuffer.allocate(1024*10);
//        //读取前position和limit位置
//        System.out.println(buffer.position());
//        System.out.println(buffer.limit());
//        //从原文件对应的通道中读取数据到缓冲区中,方法返回值表示实际读取到了多少个字节
//        int d = srcCh.read(buffer);//此时最多可以读取10240(limit-position确定的)
//        System.out.println("实际读取到了"+d+"个字节");
//        //读取后position和limit位置
//        System.out.println(buffer.position());
//        System.out.println(buffer.limit());
//
//        /*
//            在写操作前,先进行一次buffer.flip()
//            目的:将buffer可操作范围变成之前读取到缓冲去中的数据范围
//            1:limit=position
//            2:position=0
//         */
//        buffer.flip();
//        System.out.println("flip操作了!");
//        System.out.println(buffer.position());//0
//        System.out.println(buffer.limit());//10240
//        descCh.write(buffer);//将当前缓冲区中可操作范围中所有数据全部写出
//        System.out.println(buffer.position());//10240
//        System.out.println(buffer.limit());//10240
        int d;
        while((d = srcCh.read(buffer))!=-1) {
            buffer.flip();//position:0 limit:10240
            descCh.write(buffer);//position:10240  limit:10240
            buffer.clear();//position:0 limit:10240
        }
        System.out.println("复制完毕!");
        fis.close();
        fos.close();
    }
}

利用NIO非阻塞方式修改聊天室服务端代码

流程框架

代码实现:

/**
 * 使用NIO方式完成聊天室服务端
 * 最终我们通过一个线程就可以实现原Server中多线程完成的工作
 */
public class NIOServer {
    //存放所有客户端的channel,便于广播消息
    private List<SocketChannel> allChannel = new ArrayList<>();
    public void start(){
        try {
            /*
                使用NIO的ServerSocket对应的类:ServerSocketChannel
                作用:
                1:打开服务端口
                2:等待接受客户端的连接(非阻塞的)
                  相较于ServerSocket.利用给方法accept时,等待客户端连接需要阻塞线程。
             */
            //打开ServerSocketChannel通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //将该通道设置为非阻塞状态,只有设置为非阻塞状态才能交给多路选择器去监控
            serverSocketChannel.configureBlocking(false);//设置为非阻塞
            //将ServerSocketChannel绑定到8088端口,以便我们通过该端口来接受客户端的连接
            serverSocketChannel.bind(new InetSocketAddress(8088));
            //以上三部对于传统阻塞式的相当于new ServerSocket(8088);
            /*
                NIO有一个核心API:多路选择器Selector   select:单词的含义是"选择"
                多路选择器的作用相当于是一个"监控设备",将多个"通道"注册到该选择器上,此时
                选择器就会自动监控每个通道中的事件,一旦出现了某个我们需要关注的事件,比如
                一个客户端连接了,或者某个客户端发送过来消息了。那么多路选择就可以通知我们
                某个通道需要处理该操作,此时我们就可以进行相关处理了。
             */
            //创建一个多路选择器
            Selector selector = Selector.open();
            //将ServerSocketChannel通道注册到多路选择器中,让其关注它的客户端连接事件
            /*
                所有的通道都支持一个方法:register(),该方法就是用于将当前通道注册到指定的
                多路选择器上,并让其关注本通道的某个事件
                对比:
                原来我们使用ServerSocket调用accept()等待客户端连接的操作会阻塞线程,直到
                一个客户端连接位置。这意味着,没有客户端连接是accept()方法会一组阻塞导致线程
                不能处理其他操作,这使得线程大部分事件都处于阻塞状态(摸鱼)白白浪费性能。
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            /*
                多路选择器的该方法是一个阻塞方法,除非注册的通道都没有事件需要处理时才会
                阻塞,否则会立即返回一个数字告知现在有多少个事件待处理。
                int num = selector.select();
                核心方法:selectKeys(),该方法可以获取所有代办事件,如果此时没有事件需要
                处理则阻塞。
             */
            //集合中每个元素就是一个通道代办事件
            while(true) {
                int num = selector.select();//通过多路选择器询问有多少代办事件,没有事件时会阻塞
                Set<SelectionKey> set = selector.selectedKeys();//将所有代办事件获取
                //遍历每一个事件分别进行处理
                for (SelectionKey key : set) {
                    //使用分支判断该事件是什么,以便进行对应的处理
                    if (key.isAcceptable()) {//该事件表示的是有"电话"可接。
                        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();//获取该事件所发生的通道
                    /*
                        //接电话,返回一个SocketChannel
                        使用该SocketChannel的read()和write()方法就可以和该客户端交互了。
                     */
                        SocketChannel socket = ssc.accept();
                    /*
                        这里判断socket是否为null是因为accept方法有可能返回null(坑爹)
                     */
                        if (socket != null) {
                            //将该SocketChannel也注册到多路选择器上并关注事件:客户端是否发过来消息让我们读取
                            socket.configureBlocking(false);//凡是要注册到多路选择器上的通道都必须是非阻塞的
                            socket.register(selector, SelectionKey.OP_READ);
                            allChannel.add(socket);//将当前客户端channel存取集合
                        }
                    } else if (key.isReadable()) {//该事件是否为某个通道有数据可以读取
                        //因为我们向多路选择器注册可读取数据事件时是注册SocketChannel通道
                        SocketChannel socket = (SocketChannel) key.channel();//获取该通道
                        try {
                            ByteBuffer buffer = ByteBuffer.allocate(1024 * 10);
                            int d = socket.read(buffer);//将该客户端发送过来的消息都读取到缓冲区中
                            if (d == -1) {//当读取数据返回值为-1则表示客户端断开了连接
                                socket.close();
                                continue;//继续处理下一个事件,本次事件后续操作不用再干了
                            }
                            //获取缓冲区中现有的数据
                            buffer.flip();
                            byte[] data = new byte[buffer.limit()];
                            buffer.get(data);//将Buffer中的数据拷贝到data上。
                            String line = new String(data, StandardCharsets.UTF_8);
                            System.out.println("客户端说:" + line);
                            //将该消息转发给所有客户端
                            for(SocketChannel channel : allChannel){
                                buffer.flip();
                                channel.write(buffer);
                            }
                        }catch(IOException e){
                            //若有异常抛出,通常说明客户端断开连接了
                            socket.close();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        NIOServer server = new NIOServer();
        server.start();
    }
}

 

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交