Java NIO

目录

一、基本概述

二、Buffer

写数据到Buffer

Buffer读取数据

flip方法

rewind方法

clear()、compact()方法

测试实例:

三、channel

channel将buffer中的数据写出来

channel将数据读进buffer

四、Selector

服务端实例

客户端实例

[五、Java NIO Files](#五、Java NIO Files)

Files.createDirectory()

Files.copy()

Files.walkFileTree()


一、基本概述

标准的IO编程接口是面向字节流和字符流的。而NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。

NIO包含下面几个核心的组件:

  • Channels
  • Buffers
  • Selectors

核心Channels:

  • FileChannel (用于文件的数据读写)
  • DatagramChannel(用于UDP的数据读写)
  • SocketChannel (用于TCP的数据读写)
  • ServerSocketChannel (允许我们监听TCP链接请求,每个请求会创建会一个 SocketChannel)

通常来说NIO中的所有IO都是从Channel开始的。Channel和流有点类似。通过Channel,我们即可以从Channel把数据写到Buffer中,也可以把数据冲Buffer写入到Channel

核心Buffer:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer 包裹起来,对外提供一系列的读写方便开发的接口。

选择器(Selectors)

选择器允许单线程操作多个通道。如果你的程序中有大量的链接,同时每个链接的IO带宽不高的话,这个特性将会非常有帮助。比如聊天服务器。

要使用Selector的话,我们必须把Channel注册到Selector上,然后就可以调用Selector的select()方法。这个方法会进入阻塞,直到有一个channel的状态符合条件。当方法返回后,线程可以处理这些事件。

二、Buffer

一个Buffer有三个属性是必须掌握的,分别是:

  • capacity容量
  • position位置
  • limit限制

容量(Capacity)

作为一块内存,buffer有一个固定的大小,叫做capacity容量。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。

位置(Position)

当写入数据到Buffer的时候需要中一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1.当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。

上限(Limit)

在写模式,limit的含义是我们所能写入的最大数据量。它等同于buffer的容量。一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。数据读取的上限时buffer中已有的数据,也就是limit的位置(原position所指的位置)。

写数据到Buffer

有两种方法:

  • 从Channel中写数据到Buffer
  • 手动写数据到Buffer,调用put方法

下面是一个实例,演示从Channel写数据到Buffer:

int bytesRead = inChannel.read(buf);

PUT实例:

为了获取一个Buffer对象,你必须先分配。每个Buffer实现类都有一个allocate()方法用于分配内存。下面开辟一个5字节大小的buffer,当超出分配的内存时,就会抛出Exception in thread "main" java.nio.BufferOverflowException异常

Buffer读取数据

Buffer读数据也有两种方式。

  • 从buffer读数据到channel
  • 从buffer直接读取数据,调用get方法

读取数据到channel的例子:

int bytesWritten = inChannel.write(buf);

调用get读取数据的例子:

byte aByte = buf.get();

flip方法

flip()方法可以吧Buffer从写模式切换到读模式。调用flip方法会把position归零,并设置limit为之前的position的值。

rewind方法

Buffer.rewind()方法将position置为0,这样我们可以重复读取buffer中的数据。limit保持不变。

clear()、compact()方法

一旦我们从buffer中读取完数据,需要复用buffer为下次写数据做准备。只需要调用clear或compact方法。clear方法会重置position为0,limit为capacity,也就是整个Buffer清空,如果需要保留未读数据,那么可以使用compact。compact和clear的区别就在于对未读数据的处理,是保留这部分数据还是一起清空。

测试实例:
        IntBuffer buf = IntBuffer.allocate(5);
        buf.put(1);
        buf.put(2);
        buf.put(3);
        System.out.println("写入结束,buf值:"+buf);
        buf.flip();
        System.out.println("切换到读模式,buf值:"+buf);
        while (buf.position() < buf.limit()){
            System.out.println("遍历buf所有值:"+buf.get());
        }
        System.out.println("遍历结束,buf值:"+buf);
        while (buf.position() < buf.limit()){
            // 此时读写指针都指向了limit位置,读不到数据了
            System.out.println(buf.get());
        }
        buf.rewind();
        System.out.println("rewind()方法将position置为0,此时buf值: "+buf);
        while (buf.position() < 2){
            System.out.println("我又可以读取了,遍历2条数据:"+buf.get());
        }
        System.out.println("这次buf没读完,buf值:"+buf);
        buf.compact();
        System.out.println("compact保留未读数据,buf值:"+buf);
        buf.put(4);
        buf.put(5);
        System.out.println("buf添加结束,buf值:"+buf);
        buf.flip();
        while (buf.position() < buf.limit()){
            System.out.println("遍历buf值:"+buf.get());
        }
        System.out.println("最终buf情况,buf值:"+buf);

运行结果:

三、channel

Java NIO Channel通道和流非常相似,主要有以下几点区别:

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
  • 通道可以异步读写。
  • 通道总是基于缓冲区Buffer来读写。

正如上面提到的,我们可以从通道中读取数据,写入到buffer;也可以中buffer内读数据,写入到通道中。

channel将buffer中的数据写出来
        // 1.获取文件通道,通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式
        String fileName = "/Users/wwq/Desktop/test/nio/readme3.text";
        FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.WRITE);
        ByteBuffer buf = ByteBuffer.allocate(1024);
        String text = "hello world!";
        for (int i = 0; i < text.length(); i++) {
            buf.put((byte)text.charAt(i));
            // 缓存区已满或者已经遍历到最后一个字符
            if (buf.position() == buf.limit() || i == text.length() - 1) {
                // 将缓冲区由写模式置为读模式
                buf.flip();
                // 将缓冲区的数据写到通道
                channel.write(buf);
                // 清空已经读取完成的 buffer,以便后续使用
                buf.clear();
            }
        }
channel将数据读进buffer
        String fileName = "/Users/wwq/Desktop/test/nio/readme3.text";
        FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
        ByteBuffer buf = ByteBuffer.allocate(1024);
        StringBuilder text = new StringBuilder();
        // 循环读取通道中的数据,并写入到 buf 中
        while (channel.read(buf) != -1) {
            // 缓存区切换到读模式
            buf.flip();
            // 读取 buf 中的数据
            while (buf.position() < buf.limit()) {
                // 将buf中的数据追加到文件中
                text.append((char) buf.get());
            }
            // 清空已经读取完成的 buffer,以便后续使用
            buf.clear();
        }
        System.out.println(text);
四、Selector

每个 Channel向Selector 注册时,都会创建一个 SelectionKey 对象,通过 SelectionKey 对象向Selector 注册,且 SelectionKey 中维护了 Channel 的事件。常见的四种事件如下:

  1. OP_READ:当操作系统读缓冲区有数据可读时就绪。

  2. OP_WRITE:当操作系统写缓冲区有空闲空间时就绪。

  3. OP_CONNECT:当 SocketChannel.connect()请求连接成功后就绪,该操作只给客户端使用。

  4. OP_ACCEPT:当接收到一个客户端连接请求时就绪,该操作只给服务器使用。

服务端实例
  private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);//调整缓冲区大小为1024字节
    private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
    String str;

    public ServerSocket(int port) throws IOException {
        // 打开服务器套接字通道
        this.serverSocketChannel = ServerSocketChannel.open();
        // 服务器配置为非阻塞 即异步IO
        this.serverSocketChannel.configureBlocking(false);
        // 绑定本地端口
        this.serverSocketChannel.bind(new InetSocketAddress(port));
        // 创建选择器
        this.selector = Selector.open();
        // 注册接收连接事件
        //OP_ACCEPT:当接收到一个客户端连接请求时就绪,该操作只给服务器使用
        this.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void handle() throws IOException {
        // 无限判断当前线程状态,如果没有中断,就一直执行while内容。
        while(!Thread.currentThread().isInterrupted()){
            // 获取准备就绪的channel
            if (selector.select() == 0) {
                continue;
            }

            // 获取到对应的 SelectionKey 对象
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();
            // 遍历所有的 SelectionKey 对象
            while (keyIterator.hasNext()){
                // 根据不同的SelectionKey事件类型进行相应的处理
                SelectionKey key = keyIterator.next();
                if (!key.isValid()){
                    continue;
                }
                if (key.isAcceptable()){
                    accept(key);
                }
                if(key.isReadable()){
                    read(key);
                }
                // 移除当前的key
                keyIterator.remove();
            }
        }
    }

    /**
     * 客服端连接事件处理
     *
     * @param key
     * @throws IOException
     */
    private void accept(SelectionKey key) throws IOException {
        SocketChannel socketChannel = this.serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        // 注册客户端读取事件到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("客户端服务连过来了, " + socketChannel.getRemoteAddress());
    }

    /**
     * 读取事件处理
     *
     * @param key
     * @throws IOException
     */
    private void read(SelectionKey key) throws IOException{
        SocketChannel socketChannel = (SocketChannel) key.channel();
        //清除缓冲区,准备接受新数据
        this.readBuffer.clear();
        int numRead;
        try{
            // 从 channel 中读取数据
            numRead = socketChannel.read(this.readBuffer);
        }catch (IOException e){
            System.out.println("接收数据失败");
            key.cancel();
            socketChannel.close();
            return;
        }
        str = new String(readBuffer.array(),0,numRead);
        System.out.println("接收到的数据是: " + str);
    }

    public static void main(String[] args) throws Exception {
        System.out.println("启动服务...");
        new ServerSocket(8000).handle();
    }
客户端实例
 ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
    private SocketChannel channel;
    private Selector selector;

    public SocketClient(String hostname, int port) throws Exception {
        // 打开socket通道
        channel = SocketChannel.open();
        // 配置为非阻塞 即异步IO
        channel.configureBlocking(false);
        // 连接服务器端
        channel.connect(new InetSocketAddress(hostname, port));
        // 创建选择器
        selector = Selector.open();
        // 注册请求连接事件
        //当 SocketChannel.connect()请求连接成功后就绪,该操作只给客户端使用。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    public void send() throws Exception {
        Scanner scanner = new Scanner(System.in);
        // 无限判断当前线程状态,如果没有中断,就一直执行while内容。
        while (!Thread.currentThread().isInterrupted()) {
            // 获取可操作的 Channel 中的就绪事件集合
            if (selector.select() == 0) {
                continue;
            }

            // 获取到对应的 SelectionKey 对象
            Set<SelectionKey> keys = selector.selectedKeys();
            System.out.println("selectionKey keys is:" + keys.size());
            Iterator<SelectionKey> iterator = keys.iterator();
            // 遍历所有的 SelectionKey 对象
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //判断此通道上是否在进行连接操作
                if (key.isConnectable()) {
                    channel.finishConnect();
                    //注册写操作
                    channel.register(selector, SelectionKey.OP_WRITE);
                    System.out.println("连上服务端了...");
                    break;
                } else if (key.isWritable()) {
                    System.out.println("请输入内容,并按回车键:");
                    String message = scanner.nextLine();
                    writeBuffer.clear();
                    writeBuffer.put(message.getBytes());
                    //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    writeBuffer.flip();
                    channel.write(writeBuffer);
                }
                // 移除当前的key
                iterator.remove();
            }
        }
    }
    public static void main(String[] args) throws Exception {
        new SocketClient("localhost", 8000).send();
    }
五、Java NIO Files
Files.createDirectory()

创建了一个Path实例,表示需要创建的目录。接着用try-catch把Files.createDirectory()的调用捕获住。如果创建成功,那么返回值就是新创建的路径。

如果目录已经存在了,那么会抛出java.nio.file.FileAlreadyExistException异常。如果出现其他问题,会抛出一个IOException。比如说,要创建的目录的父目录不存在,那么就会抛出IOException。父目录指的是你要创建的目录所在的位置。也就是新创建的目录的上一级父目录。

代码实例:

Path path = Paths.get("/Users/wwq/Desktop/test/nio");
try {
    Files.createDirectory(path);
} catch (FileAlreadyExistsException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
Files.copy()

copy操作可以强制覆盖已经存在的目标文件

代码实例:

        Path sourcePath = Paths.get("/Users/wwq/Desktop/test/nio/readme.text");
        Path destinationPath = Paths.get("/Users/wwq/Desktop/test/nio/readme2.text");
        try {
            Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (FileAlreadyExistsException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

Files.move()

Java NIO的Files类也包含了移动的文件的接口。移动文件和重命名是一样的,但是还会改变文件的目录位置。java.io.File类中的renameTo()方法与之功能是一样的。

复制代码
Path moveSourcePath = Paths.get("/Users/wwq/Desktop/test/nio/readme2.text");
Path moveDestinationPath = Paths.get("/Users/wwq/Desktop/test/nio/readme3.text");
try {
    Files.move(moveSourcePath, moveDestinationPath,
            StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    //moving file failed.
    e.printStackTrace();
}
Files.walkFileTree()

Files.walkFileTree()方法具有递归遍历目录的功能。walkFileTree接受一个Path和FileVisitor作为参数。Path对象是需要遍历的目录,FileVistor则会在每次遍历中被调用。

FileVisitor这个接口的定义:

public interface FileVisitor {
    public FileVisitResult preVisitDirectory(
        Path dir, BasicFileAttributes attrs) throws IOException;
    public FileVisitResult visitFile(
        Path file, BasicFileAttributes attrs) throws IOException;
    public FileVisitResult visitFileFailed(
        Path file, IOException exc) throws IOException;
    public FileVisitResult postVisitDirectory(
        Path dir, IOException exc) throws IOException {
}

FileVisitor需要调用方自行实现,然后作为参数传入walkFileTree().FileVisitor的每个方法会在遍历过程中被调用多次。如果不需要处理每个方法,那么可以继承他的默认实现类SimpleFileVisitor,它将所有的接口做了空实现。

下面看一个walkFileTree()的示例:

Files.walkFileTree(path, new FileVisitor<Path>() {
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    System.out.println("pre visit dir:" + dir);
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    System.out.println("visit file: " + file);
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    System.out.println("visit file failed: " + file);
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    System.out.println("post visit directory: " + dir);
    return FileVisitResult.CONTINUE;
  }
});

FileVisitor的方法会在不同时机被调用:

preVisitDirectory()在访问目录前被调用。postVisitDirectory()在访问后调用。

visitFile()会在整个遍历过程中的每次访问文件都被调用。他不是针对目录的,而是针对文件的。visitFileFailed()调用则是在文件访问失败的时候。例如,当缺少合适的权限或者其他错误。

上述四个方法都返回一个FileVisitResult枚举对象。具体的可选枚举项包括:

  • CONTINUE
  • TERMINATE
  • SKIP_SIBLINGS
  • SKIP_SUBTREE

返回这个枚举值可以让调用方决定文件遍历是否需要继续。

CONTINE表示文件遍历和正常情况下一样继续。

TERMINATE表示文件访问需要终止。

SKIP_SIBLINGS表示文件访问继续,但是不需要访问其他同级文件或目录。

SKIP_SUBTREE表示继续访问,但是不需要访问该目录下的子目录。这个枚举值仅在preVisitDirectory()中返回才有效。如果在另外几个方法中返回,那么会被理解为CONTINE。

相关推荐
AI向前看10 分钟前
F#语言的字符串处理
开发语言·后端·golang
m0_7482513512 分钟前
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
java·前端·spring
工业互联网专业19 分钟前
基于springboot+vue的餐饮连锁店管理系统的设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计
初学者丶一起加油19 分钟前
C语言基础:指针(常量指针和指针常量)
java·linux·c语言·开发语言·算法·ubuntu·visualstudio
NullPointerExpection28 分钟前
java 使用 poi 对指定 excel 的指定多列进行从左到右树形行合并
java·开发语言·excel·poi
ccmjga32 分钟前
升级 Spring Boot 3 配置讲解 —— 如何在 Spring Boot 3 中接入生成式 AI?
java·人工智能·spring boot·后端·docker·面试·单元测试
老大白菜34 分钟前
第2章:Go语言基础语法
开发语言·后端·golang
zhulangfly35 分钟前
【Java设计模式-1】单例模式,Java世界的“独苗”
java·单例模式·设计模式
Leaf吧38 分钟前
java设计模式 单例模式
java·单例模式·设计模式
程序员_三木40 分钟前
使用 Three.js 创建动态粒子效果
开发语言·前端·javascript·数码相机·webgl·three.js