Java7技术系列:NIO.2异步IO

Java7技术系列:try-with-resource
Java7技术系列:int与二进制的转换优化
Java7技术系列:MultiCatchException
Java7技术系列:NIO.2异步IO
Java7技术系列:DI依赖注入
Java7技术系列:Queue
Java7技术系列:Java并发

1 NIO.2相关API介绍

NIO.2把位置(由Path表示)的概念和物理文件系统的处理(比如复制一个文件)分得很清楚,物理文件系统通常是由Files辅助类实现的【即Path不一定代表真实的文件或目录,你可以随心所欲地操作Path,用Files中的功能来检查文件是否存在,并对它进行处理

【个人理解:Path是虚拟的,不对实际的文件进行操作处理,只读取Path指定目录或文件的信息;
Files是会对实际的文件进行操作处理,比如复制、读写等】

1.1 NIO.2关键基础类

API描述
NIO2Path类中的方法可以用来获取路径信息,访问该路径中的各元素,将路径转换为其他形式,或提取路径中的一部分。有的方法还可以匹配路径字串以及移除路径中的冗余项
Paths工具类,提供返回一个路径的辅助方法,比如get(String first, String… more)和get(URI uri)
FileSystem与文件系统交互的类,无论是默认的文件系统,还是通过其统一资源标识(URI)获取的可选文件系统
FileSystems工具类,提供各种方法,比如其中用于返回默认文件系统的FileSystems.getDefault()

相对路径与绝对路径(以访问 /java7developer/src/main 目录为例):

比如程序在路径 /java7developer/src/test 目录下运行,代码要读取位于/java7developer/src/main目录的文件名。

为了进入 /java7developer/src/main 目录,可以用相对路径 ../main

如果程序在路径 /java7developer/src/test/java/com/java7developer 运行,用相对路径 ../main 则无法进入目标目录,而是会访问到不存在的目录 /java7developer/src/test/java/com/main,只能用绝对路径 /java7developer/src/main

【总结:使用相对路径,相当于将当前运行程序的目录回退一个目录,然后再进入对应的目标目录,也就是说,要访问的目录与程序运行目录,它们的上一级目录是相同的,此时才能使用相对路径正确访问】

1.2 文件处理的基础类

API描述
Files实现复制、移动、删除或处理文件的工具类
WatchService用来监视文件或目录的核心类,不管它们有没有变化

2 Path

2.1 获取Path

private static void operate1() {
    // NIO2.get(String first, String... more),第二个参数一般用不到,它仅仅用来把额外的字符串合并起来形成Path字符串
    // 注意:NIO.2的API中,Path或Paths中的各种方法抛出的受检异常只有IOException。这有时会存在掩藏潜在的问题,
    //       如果想处理IOException的显式子类,则需要额外编写异常处理代码。
    java.nio.file.Path path = Paths.get("/usr/bin/zip");
    java.nio.file.Path samePath = FileSystems.getDefault().getPath("/usr/bin/zip"); // 与上面代码等价
    java.nio.file.Path absolutePath = path.toAbsolutePath(); // 转换为绝对路径
}

2.2 从Path中获取信息

private static void operate2() {
    java.nio.file.Path path = Paths.get("/usr/bin/zip");
    java.nio.file.Path filename = path.getFileName(); // 获取文件名,zip
    int nameCount = path.getNameCount(); // 获取文件名字符数,3
    java.nio.file.Path parent = path.getParent(); // 获取父目录,/usr/bin
    java.nio.file.Path root = path.getRoot(); // 获取根目录,/
    java.nio.file.Path subpath = parent.subpath(0, 2); // 根据索引获取子路径,usr/bin
}

2.3 去除冗余项

private static void operate3() {
    // ①在处理Path时可能会有一个或两个点:
    // .表示当前目录 ..表示父目录
    // 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行,
    // 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录

    // normalize()去除掉冗余项./,结果为Listing_2_1.java
    java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize();

    // ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt,
    // 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置,
    // 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。

    // toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能
    try {
        java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.4 转换Path

private static void operate4() {
    // 合并Path,调用resolve()方法,合并表示/uat/conf/application.properties的完整Path
    java.nio.file.Path prefix = Paths.get("/uat/");
    java.nio.file.Path completePath = prefix.resolve("conf/application.properties");

    // 计算两个目录之间的路径,通过relativize()方法
    String java7developerPath = "/java7developer/src/test/java/com/java7developer";
    String mainPath = "/java7developer/src/main";
    java.nio.file.Path java7developerDir = Paths.get(java7developerPath);
    java.nio.file.Path mainDir = Paths.get(mainPath);
    java.nio.file.Path java7developerRelativizePath = java7developerDir.relativize(mainDir);
    java.nio.file.Path mainRelativizePath = mainDir.relativize(java7developerDir);
    System.out.println(java7developerRelativizePath); // 结果:..\..\..\..\main
    System.out.println(mainRelativizePath); // 结果:..\test\java\com\java7developer
}

2.5 java7的Path与java7之前的File之间转换

private static void operate5() {
    File file = new File("../Listing_2_1.java");
    java.nio.file.Path path = file.toPath(); // 通过toPath()转换为Path
    path.toAbsolutePath();
    file = path.toFile(); // 通过toFile()转换为File
}

2.6 文件属性

private static void operate3() {
    // ①在处理Path时可能会有一个或两个点:
    // .表示当前目录 ..表示父目录
    // 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行,
    // 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录

    // normalize()去除掉冗余项./,结果为Listing_2_1.java
    java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize();

    // ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt,
    // 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置,
    // 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。

    // toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能
    try {
        java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3 文件操作

3.1 创建文件

// 创建文件,通常处于安全考虑,要定义所创建的文件是用于读、写、执行,或者三者权限的某种组合时,要指明该文件的某些FileAttributes,这取决于文件系统,所以需要使用与文件系统相关的文件权限类

// 创建一个在POSIX系统上为属主、属组内用户和所有用户设置读写权限的文件
// 注意:如果在创建文件时要指定访问许可,不要忽略其父目录强加给该文件的umask限制或受限许可。
//       比如说,你会发现即使为新文件指定了rw-rw-rw许可,但由于目录的掩码,实际上文件最终访问许可为rw-r--r--

java.nio.file.Path path = Paths.get("D:\\Backup\\MyStuff.txt");
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-rw-");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
Files.createFile(path, attr);

3.2 删除文件

// 删除文件,要删除文件需要有删除文件的权限
java.nio.file.Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.delete(target);

3.3 复制文件

// 复制文件
// StandardCopyOption有三种选项:
// [1] REPLACE_EXISTING:覆盖即替换已有文件
// [2] COPY_ATTRIBUTES:复制文件
// [3] ATOMIC_MOVE:确保在两边都操作成功,否则回滚
java.nio.file.Path sourcePath = Paths.get("C:\\MyDocuments\\Stuff.txt");
java.nio.file.Path targetPath = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);

3.4 移动文件

// 移动文件
// 在移动源文件时保留其属性,并且覆盖目标文件(如果存在的话)
java.nio.file.Path sources = Paths.get("C:\\MyDocuments\\Stuff.txt");
java.nio.file.Path targets = Paths.get("D:\\Backup\\MyStuff.txt");
Files.move(sources, targets, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);

4 文件读写数据

4.1 读文件

     
// 注意:在处理String时,要处理字符编码问题StandardCharsets.UTF_8,防止不可预料的字符编码问题
java.nio.file.Path path = Paths.get("/tmp/app.log");
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

4.2 写文件

// 写文件,对应的StandardOpenOptions可以确保文件的读写有正确的访问许可
// Files.newInputStream(path,OpenOptions)新建一个输入流
// Files.newOutputStream(path, OpenOptions)新建一个输出流
// StandardOpenOptions.WRITE
// StandardOpenOptions.READ
// StandardOpenOptions.APPEND
java.nio.file.Path logPath = Paths.get("/tmp/app.log");
try (BufferedWriter writer = Files.newBufferedWriter(logPath, StandardOpenOption.WRITE)) {
    writer.write("Hello World!");
} catch (IOException e) {
    e.printStackTrace();
}

4.3 读写文件的简化

// 读写简化
java.nio.file.Path logFile = Paths.get("/tmp/app.log");
try {
    List<String> lines = Files.readAllLines(logFile); // 读取文件所有行
    byte[] bytes = Files.readAllBytes(path); // 读取文件所有字节
} catch (IOException e) {
    e.printStackTrace();
}

4.4 拥有文件寻址的FileChannel读写文件

// SeekableByteChannel接口,是为了让开发人员能够改变字节通道的位置和大小。
// 它的实现类FileChannel,这个类可以在文件读取或写入时保持当前位置
// 读取日志文件中的最后1000个字符,FileChannel具有寻址能力
java.nio.file.Path file = Paths.get("/temp/app.log");
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileChannel channel;
try {
    channel = FileChannel.open(file, StandardOpenOption.READ);
    channel.read(buffer, channel.size() - 1000);
} catch (IOException e) {
    e.printStackTrace();
}

4.5 文件修改监听通知WatchService

// 文件修改通知
// WatchService类检测文件或目录变化,该类用客户线程监视注册文件或目录的变化
// 并且检测到变化时返回一个事件。是轮询机制的一个理想替代品
// 检测目录变化,打印消息
// StandardWatchEventKinds.ENTRY_MODIFY:监听事件修改
// StandardWatchEventKinds.ENTRY_CREATE:监听事件创建
// StandardWatchEventKinds.ENTRY_DELETE:监听事件删除
// StandardWatchEventKinds.OVERFLOW:监听事件已经丢失或丢弃
try {
    boolean isShutdown = false;
    WatchService watcher = FileSystems.getDefault().newWatchService();
    java.nio.file.Path dir = Paths.get("/usr/vincent");
    WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 检测变化
    while (!isShutdown) {
        key = watcher.take(); // 得到下一个key及其事件
        for (WatchEvent<?> event : key.pollEvents()) {
            if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { // 检查是否为变化事件
                System.out.println("dir changed!");
                isShutdown = true;
            }
        }
        key.reset(); // 重置监听key,等待下一个事件
    }
} catch (IOException | InterruptedException e) {
    e.printStackTrace();
}

5 处理目录与目录树

5.1 目录树查找文件过滤

// 目录树查找文件,过滤指定目录中所有.properties文件
java.nio.file.Path dir = Paths.get("C:\\workspace\\java7developer");
try {
	// Files.newDirectoryStream()返回指定目录中过滤后的DirectoryStream
        DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(dir, "*.properties");
        for (java.nio.file.Path path : stream) {
        	System.out.println(path.getFileName());
        }
} catch (IOException e) {
	e.printStackTrace();
}

5.2 遍历目录树

// 遍历目录树,walkFileTree可以实现递归移动、复制和删除操作
// API
// 关键方法:Files.walkFileTree(NIO2 startingDir, FileVisitor<? super NIO2> isitor);
// FileVisitor接口有五个方法(T一般就是Path):
// [1]FileVisitResult preVisitDirectory(T dir)
// [2]FileVisitResult preVisitDirectoryFailed(T dir, IOException e)
// [3]FileVisitResult visitFile(T file, BasicFileAttributes attrs)
// [4]FileVisitResult visitFileFailed(T file, IOException e)
// [5]FileVisitResult postVisitDirectory(T dir, IOException e)
// java7已经提供了一个默认实现类,SimpleFileVisitor<T>
// 注意:为了确保递归等操作的安全性,walkFileTree()不会自动跟随符号链接。如果需要跟随符号链接,需要使用		 	normalize()或toRealPath()检查。

// 遍历C:\workspace\java7developer\src目录下及其子目录内的所有.java文件
java.nio.file.Path startingDir = Paths.get("C:\\workspace\\java7developer\\src");
try {
	Files.walkFileTree(startingDir, new FindJavaVisitor());
} catch (IOException e) {
	e.printStackTrace();
}

private static final class FindJavaVisitor extends SimpleFileVisitor<java.nio.file.Path> {
        @Override
        public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException {
            if (file.toString().endsWith(".java")) {
                System.out.println(file.getFileName());
            }
            return FileVisitResult.CONTINUE;
        }
    }

6 异步IO

privatestatic void operate10() {
    // java7中有三个新的异步通道:
    // AsynchronousFileChannel:用于文件I/O
    // AsynchronousSocketChannel:用于套接字I/O,支持超时
    // AsynchronousServerSocketChannel:用于套接字接受异步连接
    // 新的异步I/O主要有两种形式:将来式和回调式

    // 将来式:表明使用java.util.concurrent.Future接口。
	// Future接口提供了几个方法:
	// ①get():用来获取结果。如果结果还没有准备好,get()会被阻塞直到它能取得结果。
	// ②get(int second, TimeUnit unit):获取结果,可以设置超时时间。
	// ③cancel():在运算结束前取消。
	// ④isDone():调用者用它来判断运算是否结束。

    // 当希望由主线程发起I/O操作并轮询等待结果时,一般都会用将来式异步处理
    // 流程:将来式用现有的java.util.concurrent技术声明一个Future,用来保存异步操作的处理结果。
    //       这很关键,因为这意味着当前线程不会因为比较慢的I/O操作而停滞。相反,有一个单独的
    //       线程发起I/O操作,并在操作完成时返回结果。与此同时,主线程可以继续执行其他需要完成的任务。
    //       在其他任务结束后,如果I/O操作还没有完成,主线程会一直等待。
    // 实现方式:AsynchronousFileChannel会关联线程池,它的任务是接收I/O处理事件,并分发给负责处理通道中I/O操作结果
    //           的结果处理器。跟通道中发起的I/O操作关联的结果处理器确保是由线程池中的某个线程产生的。
    // 如果在创建AsynchronousFileChannel时没有为其指明线程池,那就会为其分配一个系统默认的线程池(可能会和其他通道共享)。
    // 默认线程池是由AsynchronousChannelGroup类定义的系统属性进行配置的。

    try {
        java.nio.file.Path file = Paths.get("/usr/vincent/foobar.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
        ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000); // 在该文件中读取字节大小
        Future<Integer> result = channel.read(byteBuffer, 0);
        // 这里手动调用了isDone()判断是否读取完成
        // 通常情况下,result或结束(主线程会继续执行),或等待后台I/O完成
        while (!result.isDone()) {
            // 在Future并发异步执行文件读取操作时,主线程可以继续做其他操作
            System.out.println("do something in main thread");
        }
        Integer bytesRead = result.get(); // 获取读取结果
        System.out.println("Bytes read[" + bytesRead + "]");
    } catch (IOException | InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }

    // 回调式:其基本思想是主线程会派一个侦察员CompletionHandler到独立的线程中执行I/O操作。
    //         这个侦察员将带着I/O操作的结果返回到主线程中,这个结果会触发它自己的completed或failed方法。
    // CompletionHandler<V, A>:V是结果类型,A是提供的附着对象

    try {
        java.nio.file.Path files = Paths.get("/usr/vincent/foobar.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(files);
        ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000);
        channel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("Bytes read [" + result + "]");
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println(exc.getMessage());
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

7 Socket和Channel整合

private static void operate11() {
    // java7提供了NetworkChannel,把Socket和Channel结合到一起
    // java.nio.channel包:定义通道,表示连接到执行I/O操作的实体,比如文件和套接字。定义用于多路传输、非阻塞I/O操作的选择器。
    // java.nio.Socket包:该类实现了客户端套接字。套接字是两个机器间通信的端点。
    // 在旧版java中,为了执行I/O操作,比如向TCP端口中写入数据,你需要将通道绑定到Socket的实现类上,但存在问题:
    // ①在旧版java中,为了配置套接字选项和绑定在套接字上,必须把通道和套接字的API整合在一起。
    // ②在旧版java中,不能利用平台特定的套接字行为。

    // NetworkChannel:代表一个连接到网络套接字通道的映射。它定义了一组实用的方法,比如查看及设置通道上可用的套接字选项等。
    // 输出互联网套接字地址在端口3080上所支持的选项,设置IP服务条款选项以及确认套接字通道上的SO_KEEPALIVE选项
    SelectorProvider provider = SelectorProvider.provider();
    try {
        SocketChannel channel = provider.openSocketChannel();
        SocketAddress address = new InetSocketAddress(3080);
        channel = channel.bind(address); // 将端口绑定到3080

        Set<SocketOption<?>> socketOptions = channel.supportedOptions(); // 获取该端口所支持选项
        System.out.println(socketOptions.toString());
        channel.setOption(StandardSocketOptions.IP_TOS, 3); // 设置IP服务条款选项TOS
        Boolean keepAlive = channel.getOption(StandardSocketOptions.SO_KEEPALIVE);// 获取SO_KEEPALIVE选项
    } catch (IOException e) {
        e.printStackTrace();
    }

    // NetworkChannel让多播操作称为可能
    // 多播(组播):表示一对多的网络通讯,通常用来指代IP多播。其基本前提是将一个包发送到一个组播地址,
    //               然后网络对该包进行复制,分发给所有接收端(注册到组播地址中)
    // 为了让NetworkChannel加入多播组,java7提供了一个新接口java.nio.channels.MulticastChannel及默认实现类DatagramChannel
    // 加入IP地址为180.90.4.12的多播组,并对其发送和接收系统状态信息
    try {
        NetworkInterface networkInterface = NetworkInterface.getByName("net1"); // 选择网络接口
        DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET); // 打开DatagramChannel
        // 将通道设置为多播
        dc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        dc.bind(new InetSocketAddress(8080));
        dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
        // 加入多播组
        InetAddress group = InetAddress.getByName("180.90.4.12");
        MembershipKey key = dc.join(group, networkInterface);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值