阅读 800

【Java】NIO和BIO有什么区别?回答:天壤之别|Java 开发实战

这是我参与更文挑战的第1天,活动详情查看:更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

一、什么是NIO

1.概念

NIO是java1.4中引入的,被称为new I/O,也有说是non-blocking I/O,NIO被成为同步非阻塞的IO。

思考失败.jpg

2.跟BIO流的区别

  1. BIO是面向流的,NIO是面向块(缓冲区)的
  2. BIO的流都是同步阻塞的,而NIO同步非阻塞的
  3. NIO会等待数据全部传输过来再让线程处理,BIO是直接让线程等待。
  4. NIO有选择器,而BIO没有。
  5. NIO是采用管道和缓存区的形式来处理数据的,而BIO是采用输入输出流来处理的。
  6. NIO是可以双向的,BIO只能够单向。

就这.jpg

二、NIO常用组件Channel和Buffer的使用

1.代码

这里以文件复制为例

public class test {
    public static void main(String[] args){
        try{
            //存在的照片
            File inFile=new File("C:\\Users\\Administrator\\Desktop\\study.PNG");
            //复制后要存放照片的地址
            File outFile=new File("C:\\Users\\Administrator\\Desktop\\study1.PNG");
            //打开流
            FileInputStream fileInputStream=new FileInputStream(inFile);
            FileOutputStream fileOutputStream=new FileOutputStream(outFile);
            /**
             * RandomAccessFile accessFile=new RandomAccessFile(inFile,"wr");
             *  FileChannel inFileChannel=accessFile.getChannel();
             *  和下面两行代码是一样的,都是可以拿到FileChannel
             */
            //获取Channel
            FileChannel inFileChannel=fileInputStream.getChannel();
            FileChannel outFileChannel=fileOutputStream.getChannel();
		   //创建buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024*1024);
			//读取到buffer中
            while (inFileChannel.read(buffer)!=-1){
                //翻转一下,就可以读取到全部数据了
                buffer.flip();
                outFileChannel.write(buffer);
                //读取完后要clear
                buffer.clear();
            }
            //关闭
            inFileChannel.close();
            outFileChannel.close();
            fileInputStream.close();
            fileOutputStream.close();
        }catch (Exception e){}

    }
}
复制代码

我的桌面上的确多了一张一模一样的图片

2.解释

使用NIO的话,需要注意几个步骤:

  1. 打开流
  2. 获取通道
  3. 创建Buffer
  4. 切换到读模式 buffer.flip()
  5. 切换到写模式 buffer.clear(); 其实这里也看不出来它是怎么使用缓冲区的,上面这段代码中的while循环的作用和下面的代码是一样的
 while ((i=fileInputStream.read())!=-1){
                fileOutputStream.write(i);
          }
复制代码

好了好了.jpg

让我们赶紧开始NIO的编程

三、BIO和NIO的区别

学习了Channel和Buffer的使用,我们就可以正式进入NIO的开发了

代码

NIO

NIO服务端:只是接受客户端发送过来的数据,然后打印在控制台

/**
 * NIO
 * @author xuxiaobai
 */
public class NIOTest {
    private final static int port = 8080;

    public static void main(String[] args) throws IOException {
        //启动服务端
        TCPServer();
    } 
	/**
     * TCP服务端
     * 接受TCP
     *
     * @throws IOException
     */
    public static void TCPServer() throws IOException {
        //创建服务端多路复用选择器
        Selector selector = Selector.open();
        //创建服务端SocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //定义地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //绑定地址
        serverSocketChannel.bind(inetSocketAddress);
        System.out.println("绑定成功:" + inetSocketAddress);
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //注册服务端选择端,只接受accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            //加上延时,什么原理我忘记了,只知道是为了防止死锁
            selector.select(500);
            //遍历服务端选择器的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                if (!next.isValid()) {
                    //该key无效直接跳过
                    continue;
                }
                //注意
                if (next.isAcceptable()) {
                    //1. accept事件
                    //接收到accept事件,拿到channel,这个是服务端SocketChannel
                    ServerSocketChannel channel = (ServerSocketChannel) next.channel();
                    //accept得到连接客户端的channel
                    SocketChannel accept = channel.accept();
                    accept.configureBlocking(false);
                    //注册write事件
                    accept.register(selector, SelectionKey.OP_READ);
                    iterator.remove();
                } else if (next.isReadable()) {
                    //2. read事件
                    //开启一个新的线程
                    Thread thread = new Thread(() -> {
                        SocketChannel channel = (SocketChannel) next.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        byteBuffer.clear();
                        try {
                            channel.read(byteBuffer);
                            //开始处理数据
                            byteBuffer.flip();
                            byte[] bytes = new byte[byteBuffer.remaining()];
                            byteBuffer.get(bytes);
                            String x = new String(bytes);
                            if(x.equals("")){
                                //老是会莫名其妙地打印一些空行,打个补丁
                                return;
                            }
                            System.out.println(x);
                            if ("exit".equals(x)) {
                                //关闭通道
                                try {
                                    channel.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                                next.cancel();
                            }
                        } catch (IOException e) {
                            //出现异常的处理
                            e.printStackTrace();
                            try {
                                channel.close();
                            } catch (IOException ioe) {
                                ioe.printStackTrace();
                            }
                            next.cancel();
                        }

                    });
                    iterator.remove();
                    thread.start();
                }
            }
        }
    }
}
复制代码

BIO

BIO服务端:接受客户端的数据,然后打印在控制台

BIO客户端:向服务端发送数据。NIO的测试中也使用这个客户端进行测试

/**
 * BIO
 * @author xuxiaobai
 */
public class BIOTest {
    private final static int port = 8080;

    public static void main(String[] args) throws IOException {
        TCPClient();
//        TCPServer();
    }

    /**
     * TCP客户端
     * 发送TCP
     * @throws IOException
     */
    private static void TCPClient() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        //定义地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //连接
        socketChannel.connect(inetSocketAddress);
        System.out.println("连接成功:"+inetSocketAddress);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String next = scanner.next();
            //直接包装一个buffer
            ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
            //写入
            socketChannel.write(wrap);
            if ("exit".equals(next)) {
                //等于exit时关闭channel
                socketChannel.close();
                break;
            }
        }
    }

    /**
     * TCP服务端
     * 接受TCP
     * @throws IOException
     */
    private static void TCPServer() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //定义地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //绑定
        serverSocketChannel.bind(inetSocketAddress);
        System.out.println("绑定成功:"+inetSocketAddress);
        while (true) {
            //接受连接
            SocketChannel accept = serverSocketChannel.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //定义一个缓冲区,读出来的数据超出缓冲区的大小时会被丢弃
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (true) {
                        try {
                            //每次使用前都要清空,但这里没有真的区clear数据,只是移动了buffer里面的下标
                            byteBuffer.clear();
                            //读取数据到缓冲区
                            accept.read(byteBuffer);
                            //每次读取数据前都要flip一下,这里都移动下标
                            byteBuffer.flip();
                            byte[] bytes = new byte[byteBuffer.remaining()];
                            //获取数据
                            byteBuffer.get(bytes);
                            String x = new String(bytes);
                            System.out.println(x);
                            if (x.equals("exit")) {
                                //当读出来的数据等于exit时退出
                                accept.close();
                                break;
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            //启动该线程
        }
    }
}
复制代码

流汗.jpg

搞完了代码,让我们来看看代码的演示效果————从客户端发送数据到服务端,下面展示一下效果:

先后启动BIO的TCPServer和TCPClient方法;

TCPClient:image-20210602112041917.png TCPServer: image-20210602112058012.png

步骤

在这里插入图片描述 画了个图来表示,这是关于selector的配置流程,在循环中根据不同key值所进行的操作,跟上面文件复制的例子差不多了,只不过这里的Channel是通过 key.channel()获得的。

做程序员很轻松的.jpg

差别

我们来看看一下BIO和NIO的差别。

BIO

我们用IDEA的debug启动BIO的服务端,然后在启动多个客户端。

image-20210602000034663.png

我这里启动了三个客户端,可以看到有三个线程已经创建好了,然而我这时还没有发送数据到服务端。

NIO

我们用IDEA的debug启动NIO的服务端,然后在启动多个BIO客户端。

image-20210602000510748.png

这里启动了多个客户端,服务器上没有多余的几个线程。

修改BIO的TCPClient方法

    private static void TCPClient() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        //定义地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //连接
        socketChannel.connect(inetSocketAddress);
        System.out.println("连接成功:" + inetSocketAddress);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String next = scanner.next();
            //直接包装一个buffer
//            ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
            //写入
            while (true) {
                try {
                    //休眠
                  	//注意,休眠时间建议调高一点
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                socketChannel.write(ByteBuffer.wrap(next.getBytes()));
            }
//            if ("exit".equals(next)) {
//                //等于exit时关闭channel
//                socketChannel.close();
//                break;
//            }
        }
    }
复制代码

休眠时间记得调高点!!!宕机警告!

认真警告.gif

这样客户端就会在读取到第一次时,一直发送这个数据,可以看到一些线程,也是只有在收到数据之后才会创建这个线程去打印这个数据。如果休眠时间调高一点的话,就会看到有时候这里会一闪一闪的,调低后就会出现一闪而过的很多线程,如下图。

image-20210602003626857.png

四、总结

BIO的话,每次网络请求过来之后,服务器都是会为这个请求创建一个线程,这个线程会一直等待这个请求后续的数据,等处理完成后才会销毁这个线程;而NIO,当每次网络请求过来时,服务器不会马上创建一个线程去处理这个请求,而是会交给一个Selector线程,只有这个请求后续的数据全部传输过来后,Selector才会去通知其他其他线程或者创建一个线程来处理这个请求。

这就是NIO和BIO最大的差别,只有数据传输到服务器时才会让线程去处理,减少了线程的空等待,大部分情况下可以采用线程池的方式来处理数据,可以提高线程的利用率。

——————————————————————————————

非常抱歉,由于我的疏忽,之前对知识点没有学习到位,导致我以为用到了Channel和Buffer就是用到了多路复用,后来在多个小伙伴的疑问下,我复查了文章,也发现了这个错误,在这里非常感谢这几位小伙伴。现在已经更新好了,还有问题的话,可以直接评论或者私信我,我也会在最快的时间把这个错误修正的。

如果觉得我写得还不错的话,点个赞也是对我的支持哦

未经允许,不得转载!

微信搜【程序员徐小白】,关注即可第一时间阅读最新文章。回复【面试题】有我准备的50道高频校招面试题,以及各种学习资料。

文章分类
后端
文章标签