新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。

NIO核心组件:

  • 通道(Channels)
  • 缓冲区(Buffers)
  • 选择器(Selectors)

流与块

I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。
为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。
但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

I/O 包和 NIO 已经很好地集成了,java.io. 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。
例如,java.io.
包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

通道与缓冲区

1. 通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),
而通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过 UDP 读写网络中数据;
  • SocketChannel:通过 TCP 读写网络中数据;
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

2. 缓冲区

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

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

缓冲区状态变量

  • capacity:最大容量;
  • position:当前已经读写的字节数;
  • limit:还可以读写的字节数。

状态变量的改变过程举例:

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

七、NIO - 图1

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

七、NIO - 图2

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

七、NIO - 图3

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

七、NIO - 图4

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

七、NIO - 图5

文件 NIO 实例

FileChannel的使用

  1. 开启FileChannel
  2. 从FileChannel读取数据/写入数据

3.关闭FileChannel

  1. public class FileChannelDemo {
  2. public static void main(String[] args) throws IOException {
  3. //1.创建一个RandomAccessFile(随机访问文件)对象通过RandomAccessFile对象的getChannel()方法。
  4. RandomAccessFile raf=new RandomAccessFile("demo6.txt","rw");
  5. FileChannel fc=raf.getChannel();
  6. //使用FileChannel的read()方法读取数据:
  7. ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
  8. int bys=fc.read(byteBuffer);
  9. //使用FileChannel的write()方法写入数据:
  10. ByteBuffer byteBuffer2=ByteBuffer.allocate(1024);
  11. byteBuffer2.put("hello".getBytes());
  12. fc.write(byteBuffer2);
  13. //3.关闭FileChannel
  14. fc.close();
  15. }
  16. }
  • 以下展示了使用 NIO 快速复制文件的实例:
  1. public class CopyFile {
  2. public static void main(String[] args) throws IOException {
  3. String srcFile="国旗歌.mp4";
  4. String destFile="demo3.mp4";
  5. long start = System.currentTimeMillis();
  6. //copyFile(srcFile,destFile); //共耗时:75309毫秒
  7. //copyFile2(srcFile,destFile); //共耗时:153毫秒
  8. //copyFile3(srcFile,destFile);//共耗时:282毫秒
  9. //copyFile4(srcFile,destFile);//共耗时:44毫秒
  10. copyFile5(srcFile,destFile);//共耗时:共耗时:113毫秒
  11. long end = System.currentTimeMillis();
  12. System.out.println("共耗时:" + (end - start) + "毫秒");
  13. }
  14. /**
  15. * 基本字节流一次读写一个字节
  16. */
  17. public static void copyFile(String srcFile,String destFile) throws IOException {
  18. FileInputStream fis=new FileInputStream(srcFile);
  19. FileOutputStream fos=new FileOutputStream(destFile);
  20. int by=0;
  21. while((by=fis.read())!=-1){
  22. fos.write(by);
  23. }
  24. fis.close();
  25. fos.close();
  26. }
  27. /**
  28. * 基本字节流一次读写一个字节数组
  29. */
  30. public static void copyFile2(String srcFile,String destFile) throws IOException{
  31. FileInputStream fis=new FileInputStream(srcFile);
  32. FileOutputStream fos=new FileOutputStream(destFile);
  33. int len=0;
  34. byte[] bys=new byte[1024];
  35. while((len=fis.read(bys))!=-1){
  36. fos.write(bys,0,len);
  37. }
  38. fis.close();
  39. fos.close();
  40. }
  41. /**
  42. * 高效字节流一次读写一个字节
  43. */
  44. public static void copyFile3(String srcFile,String destFile) throws IOException{
  45. BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile));
  46. BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile));
  47. int by=0;
  48. while((by=bis.read())!=-1){
  49. bos.write(by);
  50. }
  51. bis.close();
  52. bos.close();
  53. }
  54. /**
  55. * 高效字节流一次读写一个字节数组
  56. */
  57. public static void copyFile4(String srcFile,String destFile) throws IOException{
  58. BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile));
  59. BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile));
  60. int len=0;
  61. byte[] bys=new byte[1024];
  62. while((len=bis.read(bys))!=-1){
  63. bos.write(bys,0,len);
  64. }
  65. bis.close();
  66. bos.close();
  67. }
  68. /**
  69. * 使用FileChannel复制文件
  70. */
  71. public static void copyFile5(String srcFile,String destFile) throws IOException{
  72. FileInputStream fis=new FileInputStream(srcFile);
  73. //获取输入字节流的文件通道
  74. FileChannel fcin=fis.getChannel();
  75. FileOutputStream fos=new FileOutputStream(destFile);
  76. //获取输出字节流的文件通道
  77. FileChannel fcout=fos.getChannel();
  78. //为缓冲区分配 1024 个字节
  79. ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  80. while(true){
  81. //从输入通道中读取数据到缓冲区中
  82. int r = fcin.read(buffer);
  83. // read() 返回 -1 表示 EOF
  84. if(r==-1){
  85. break;
  86. }
  87. //切换读写
  88. buffer.flip();
  89. //把缓冲区的内容写入输出文件中
  90. fcout.write(buffer);
  91. //清空缓冲区
  92. buffer.clear();
  93. }
  94. }
  95. }

SocketChannel和ServerSocketChannel的使用

SocketChannel用于创建基于TCP协议的客户端对象,因为SocketChannel中不存在accept()方法,
所以,它不能成为一个服务端程序。
通过connect()方法,SocketChannel对象可以连接到其他TCP服务器程序。

ServerSocketChannel允许我们监听TCP协议请求,通过ServerSocketChannel的accept()方法创建一个SocketChannel对象用户从客户端读/写数据。

  • 服务端:
  1. 通过ServerSocketChannel 绑定ip地址和端口号
  2. 通过ServerSocketChannel的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
  3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据
  4. 关闭SocketChannel和ServerSocketChannel
  1. public class Server {
  2. public static void main(String[] args) throws IOException {
  3. //通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象
  4. ServerSocketChannel ssc=ServerSocketChannel.open();
  5. //1. 通过ServerSocketChannel 绑定ip地址和端口号
  6. ssc.socket().bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888));
  7. //2. 通过ServerSocketChannel的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
  8. SocketChannel sc=ssc.accept();
  9. //3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据
  10. //读取客户端发送的数据
  11. ByteBuffer buffer=ByteBuffer.allocate(1024);
  12. //从通道中读取数据到缓冲区
  13. sc.read(buffer);
  14. StringBuffer sb=new StringBuffer();
  15. buffer.flip();
  16. while(buffer.hasRemaining()){
  17. sb.append((char)buffer.get());
  18. }
  19. System.out.println(sb.toString());
  20. ByteBuffer buffer2=ByteBuffer.allocate(1024);
  21. //向客户端发送数据
  22. buffer2.put("data has been received.".getBytes());
  23. buffer2.flip();
  24. sc.write(buffer2);
  25. //4. 关闭SocketChannel和ServerSocketChannel
  26. sc.close();
  27. ssc.close();
  28. }
  29. }
  • 客户端:

1.通过SocketChannel连接到远程服务器

2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据

3.关闭SocketChannel

  1. public class Client {
  2. public static void main(String[] args) throws IOException {
  3. //1.通过SocketChannel连接到远程服务器
  4. SocketChannel sc=SocketChannel.open();
  5. sc.connect(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888));
  6. //2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据
  7. //向通道中写入数据
  8. ByteBuffer buffer=ByteBuffer.allocate(1024);
  9. buffer.put("hello".getBytes());
  10. buffer.flip();
  11. sc.write(buffer);
  12. //读取从客户端中获取的数据
  13. ByteBuffer buffer2=ByteBuffer.allocate(1024);
  14. sc.read(buffer2);
  15. StringBuffer sb=new StringBuffer();
  16. buffer2.flip();
  17. while(buffer2.hasRemaining()){
  18. sb.append((char)buffer2.get());
  19. }
  20. System.out.println(sb.toString());
  21. //3.关闭SocketChannel
  22. sc.close();
  23. }
  24. }

DatagramChannel的使用

DataGramChannel,类似于java 网络编程的DatagramSocket类;
使用UDP进行网络传输, UDP是无连接,面向数据报文段的协议。

  • 服务端:
  1. public class Server {
  2. public static void main(String[] args) throws IOException {
  3. DatagramChannel dc= DatagramChannel.open();
  4. dc.bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888));
  5. //创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据
  6. //读取客户端发送的数据
  7. ByteBuffer buffer=ByteBuffer.allocate(1024);
  8. //从通道中读取数据到缓冲区
  9. dc.receive(buffer);
  10. StringBuffer sb=new StringBuffer();
  11. buffer.flip();
  12. while(buffer.hasRemaining()){
  13. sb.append((char)buffer.get());
  14. }
  15. System.out.println(sb.toString());
  16. ByteBuffer buffer2=ByteBuffer.allocate(1024);
  17. //向客户端发送数据
  18. buffer2.put("data has been received.".getBytes());
  19. buffer2.flip();
  20. dc.send(buffer2,new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),9999));
  21. dc.close();
  22. }
  23. }
  • 客户端:
  1. public class Client {
  2. public static void main(String[] args) throws IOException {
  3. DatagramChannel dc= DatagramChannel.open();
  4. dc.bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),9999));
  5. //创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据
  6. //向通道中写入数据
  7. ByteBuffer buffer=ByteBuffer.allocate(1024);
  8. buffer.put("hello".getBytes());
  9. buffer.flip();
  10. dc.send(buffer,new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888));
  11. //读取从客户端中获取的数据
  12. ByteBuffer buffer2=ByteBuffer.allocate(1024);
  13. dc.receive(buffer2);
  14. StringBuffer sb=new StringBuffer();
  15. buffer2.flip();
  16. while(buffer2.hasRemaining()){
  17. sb.append((char)buffer2.get());
  18. }
  19. System.out.println(sb.toString());
  20. dc.close();
  21. }
  22. }

通道之间的数据传输

在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。

  1. transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
  2. transferTo() :transferTo方法把FileChannel数据传输到另一个FileChhannel
  1. public static void copyFile6(String srcFile,String destFile) throws IOException {
  2. FileInputStream fis = new FileInputStream(srcFile);
  3. //获取输入字节流的文件通道
  4. FileChannel fcin = fis.getChannel();
  5. FileOutputStream fos = new FileOutputStream(destFile);
  6. //获取输出字节流的文件通道
  7. FileChannel fcout = fos.getChannel();
  8. //fcin通道中读出count bytes ,并写入fcout通道中
  9. //fcin.transferTo(0,fcin.size(),fcout);
  10. //或者
  11. fcout.transferFrom(fcin,0,fcin.size());
  12. }

选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式
去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,
就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,
对于 IO 密集型的应用具有很好地性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,
为 FileChannel 配置非阻塞也没有意义。

七、NIO - 图6

使用Selector的优点:

使用更少的线程来就可以来处理通道了, 相比使用多个线程,
避免了线程上下文切换带来的开销。

1. 创建选择器

  1. Selector selector = Selector.open();

2. 将通道注册到选择器上

  1. ServerSocketChannel ssChannel = ServerSocketChannel.open();
  2. ssChannel.configureBlocking(false);//通道必须配置为非阻塞模式
  3. ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它们在 SelectionKey 的定义如下:

  1. public static final int OP_READ = 1 << 0;
  2. public static final int OP_WRITE = 1 << 2;
  3. public static final int OP_CONNECT = 1 << 3;
  4. public static final int OP_ACCEPT = 1 << 4;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

  1. int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

3. 监听事件

  1. int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达

4. 获取到达的事件

  1. Set<SelectionKey> keys = selector.selectedKeys();
  2. Iterator<SelectionKey> keyIterator = keys.iterator();
  3. while (keyIterator.hasNext()) {
  4. SelectionKey key = keyIterator.next();
  5. if (key.isAcceptable()) {
  6. // ...
  7. } else if (key.isReadable()) {
  8. // ...
  9. }
  10. keyIterator.remove();
  11. }

5. 事件循环

因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

  1. while (true) {
  2. int num = selector.select();
  3. Set<SelectionKey> keys = selector.selectedKeys();
  4. Iterator<SelectionKey> keyIterator = keys.iterator();
  5. while (keyIterator.hasNext()) {
  6. SelectionKey key = keyIterator.next();
  7. if (key.isAcceptable()) {
  8. // ...
  9. } else if (key.isReadable()) {
  10. // ...
  11. }
  12. keyIterator.remove();
  13. }
  14. }

Reactor 模型

我们知道,在我们使用传统方法进行网络IO操作的时候,需要使用一个线程监听IO事件,当事件到达之后,创建一个线程去处理接收到的IO事件,这种模型需要创建大量的线程,会极大的浪费内存等资源。

这种情况下,有人提出了Reactor模式,在Reactor中,拆分为不同的小线程或者子过程,这些被拆分的小线程和子过程对应的是handler,每一种handler都会处理一种事件(event)。这个需要一个全局的管理者selector,向channel注册感兴趣的事件,selector不断在channel中监测是否有该事件发生,如果没有,那么主线程会被阻塞,否则会调用相应的事件处理函数来处理。这些事件典型的有连接、读取、写入,我们需要为这些事件分别提供处理器,事件到达后分发到处理器中就可以返回处理后面的事件,吞吐量能够极大的提高。其中定义了以下三种角色:

  • Reactor:负责将IO事件分派给指定的处理器(Handler)
  • Acceptor:负责处理新的连接,并将请求移交给Reactor
  • Handler:负责读写的处理器

单Reactor单线程模型

这是最基本的单Reactor单线程模型。其中Reactor线程,负责多路复用socket,有新连接到来触发事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。

Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于reactor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,有Reactor分发)。

该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。

七、NIO - 图7

单Reactor多线程模型

相对于第一种单线程的模式来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,这样可以减小主reactor的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐量。

七、NIO - 图8

多Reactor多线程模型

第三种模型比起第二种模型,是将Reactor分成两部分:

  • mainReactor:负责监听server socket,用来处理新连接的建立,将建立的socketChannel指定注册给subReactor;
  • subReactor:维护自己的selector, accept会将连接交给它来处理,读写(read、send)网络数据,对于业务逻辑的处理,将其扔给线程池中的worker来处理。

在该模型中,mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,一个subReactor对应一个线程处理。

此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的框架已经应用,比如Netty。

七、NIO - 图9

优缺点

优点:

  1. 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的
  2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
  3. 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源
  4. 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性

缺点:

  1. 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试
  2. Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效
  3. Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式

套接字 NIO 实例

  1. public class NIOServer {
  2. public static void main(String[] args) throws IOException {
  3. //1. 创建选择器
  4. Selector selector = Selector.open();
  5. //2.将通道注册到选择器上
  6. ServerSocketChannel ssChannel = ServerSocketChannel.open();
  7. ssChannel.configureBlocking(false);
  8. //通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了
  9. ssChannel.register(selector, SelectionKey.OP_ACCEPT);
  10. ServerSocket ss=ssChannel.socket();
  11. ss.bind(new InetSocketAddress("127.0.0.1",8888));
  12. while (true){
  13. //3. 监听事件
  14. selector.select();
  15. //4. 获取到达的事件
  16. Set<SelectionKey> keys = selector.selectedKeys();
  17. Iterator<SelectionKey> keyIterator = keys.iterator();
  18. while (keyIterator.hasNext()) {
  19. SelectionKey key = keyIterator.next();
  20. if (key.isAcceptable()) {
  21. ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
  22. // 服务器会为每个新连接创建一个 SocketChannel
  23. SocketChannel sChannel = ssChannel1.accept();
  24. sChannel.configureBlocking(false);
  25. // 这个新连接主要用于从客户端读取数据
  26. sChannel.register(selector, SelectionKey.OP_READ);
  27. } else if (key.isReadable()) {
  28. SocketChannel sChannel = (SocketChannel) key.channel();
  29. System.out.println(readDataFromSocketChannel(sChannel));
  30. sChannel.close();
  31. }
  32. keyIterator.remove();
  33. }
  34. }
  35. }
  36. private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
  37. ByteBuffer buffer = ByteBuffer.allocate(1024);
  38. StringBuilder data = new StringBuilder();
  39. while (true) {
  40. buffer.clear();
  41. int r = sChannel.read(buffer);
  42. if (r == -1) {
  43. break;
  44. }
  45. buffer.flip();
  46. int limit = buffer.limit();
  47. char[] dst = new char[limit];
  48. for (int i = 0; i < limit; i++) {
  49. dst[i] = (char) buffer.get(i);
  50. }
  51. data.append(dst);
  52. buffer.clear();
  53. }
  54. return data.toString();
  55. }
  56. }
  1. public class NIOClient {
  2. public static void main(String[] args) throws IOException {
  3. Socket socket = new Socket("127.0.0.1", 8888);
  4. OutputStream out = socket.getOutputStream();
  5. String s = "hello world";
  6. out.write(s.getBytes());
  7. out.close();
  8. }
  9. }

内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

  1. MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

NIO与IO对比

NIO 与普通 I/O 的区别主要有以下三点:

  • NIO 是非阻塞的;
  • NIO 面向块,I/O 面向流。
  • NIO有选择器,而I/O没有。

Path

Java7中文件IO发生了很大的变化,专门引入了很多新的类来取代原来的
基于java.io.File的文件IO操作方式。

创建一个Path

使用Paths工具类的get()方法创建Path对象

  1. public class PathDemo {
  2. public static void main(String[] args) {
  3. //方式一
  4. Path path=Paths.get("demo5.txt");
  5. System.out.println(path);
  6. //方式二
  7. Path path2 = FileSystems.getDefault().getPath("demo5.txt");
  8. System.out.println(path2);
  9. }
  10. }

File和Path之间的转换,File和URI之间的转换

  1. public class PathDemo2 {
  2. public static void main(String[] args) {
  3. Path path=Paths.get("demo5.txt");
  4. File file=path.toFile();
  5. URI uri=path.toUri();
  6. System.out.println(path);
  7. System.out.println(file);
  8. System.out.println(uri);
  9. }
  10. }
  1. demo5.txt
  2. demo5.txt
  3. file:///F:/Java_Review/05Java/JavaIO/demo5.txt

获取Path的相关信息

  1. public class PathDemo3 {
  2. public static void main(String[] args) {
  3. Path path= Paths.get("demo3\\test3.txt");
  4. System.out.println("文件名:"+ path.getFileName());
  5. System.out.println("名称元素的数量:"+path.getNameCount());
  6. System.out.println("父路径:"+ path.getParent());
  7. System.out.println("根路径:"+ path.getRoot());
  8. System.out.println("是否是绝对路径:"+path.isAbsolute());
  9. //startWith() 参数既可以是字符串,也可以是Path
  10. System.out.println("是否是以路径demo3开头:"+path.startsWith(Paths.get("demo3")));
  11. System.out.println("该路径的字符串形式:"+path.toString());
  12. }
  13. }
  1. 文件名:test3.txt
  2. 名称元素的数量:2
  3. 父路径:demo3
  4. 根路径:null
  5. 是否是绝对路径:false
  6. 是否是以路径demo3开头:true
  7. 该路径的字符串形式:demo3\test3.txt

移除Path中的冗余项

\ .表示的是当前目录

\ ..表示父目录或者说是上一级目录

normalize() : 返回一个路径,该路径是取出冗余项的路径。

toRealPath() : 可以看成,先进行toAbsolutePath()操作,然后进行normalize()操作

  1. public class PathDemo4 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("./demo3");
  4. System.out.println("original :"+ path.toAbsolutePath());
  5. System.out.println("after normalize:"+ path.toAbsolutePath().normalize());
  6. System.out.println("after toRealPath:"+ path.toRealPath());
  7. }
  8. }
  1. original :F:\Java_Review\05Java\JavaIO\.\demo3
  2. after normalize:F:\Java_Review\05Java\JavaIO\demo3
  3. after toRealPath:F:\Java_Review\05Java\JavaIO\demo3
  1. public class PathDemo5 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("../JavaIO");
  4. System.out.println("original :"+ path.toAbsolutePath());
  5. System.out.println("after normalize:"+ path.toAbsolutePath().normalize());
  6. System.out.println("after toRealPath:"+ path.toRealPath());
  7. }
  8. }
  1. original :F:\Java_Review\05Java\JavaIO\..\JavaIO
  2. after normalize:F:\Java_Review\05Java\JavaIO
  3. after toRealPath:F:\Java_Review\05Java\JavaIO

Files

java.nio.file.Files类是和java.nio.file.Path相结合使用的

检查给定的Path在文件系统中是否存在

Files.exists():检测文件路径是否存在

  1. public class FilesDemo {
  2. public static void main(String[] args) {
  3. Path path = Paths.get("demo5.txt");
  4. //LinkOptions.NOFOLLOW_LINKS:表示检测时不包含符号链接文件。
  5. boolean isExist= Files.exists(path,new LinkOption[]{LinkOption.NOFOLLOW_LINKS});
  6. System.out.println(isExist);
  7. }
  8. }

创建文件/文件夹

Files.createFile():创建文件

Files.createDirectory(): 创建文件夹

Files.createDirectories(): 创建文件夹

  1. public class FilesDemo2 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("demo7.txt");
  4. if(!Files.exists(path)){
  5. Files.createFile(path);
  6. }
  7. Path path2=Paths.get("demo4");
  8. if(!Files.exists(path2)){
  9. Files.createDirectory(path2);
  10. }
  11. Path path3=Paths.get("demo5\\test");
  12. if(!Files.exists(path3)){
  13. Files.createDirectories(path3);
  14. }
  15. }
  16. }

删除文件或目录

Files.delete():删除一个文件或目录

  1. public class FilesDemo3 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("demo7.txt");
  4. Files.delete(path);
  5. }
  6. }

把一个文件从一个地址复制到另一个位置

Files.copy():把一个文件从一个地址复制到另一个位置

  1. public class FilesDemo4 {
  2. public static void main(String[] args) throws IOException {
  3. Path srcPath= Paths.get("demo6.txt");
  4. Path destPath=Paths.get("demo7.txt");
  5. //Files.copy(srcPath,destPath);
  6. //强制覆盖已经存在的目标文件
  7. Files.copy(srcPath,destPath, StandardCopyOption.REPLACE_EXISTING);
  8. }
  9. }

获取文件属性

  1. public class FilesDemo5 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("demo7.txt");
  4. System.out.println(Files.getLastModifiedTime(path));
  5. System.out.println(Files.size(path));
  6. System.out.println(Files.isSymbolicLink(path));
  7. System.out.println(Files.isDirectory(path));
  8. System.out.println(Files.readAttributes(path,"*"));
  9. }
  10. }

遍历一个文件夹

  1. public class FilesDemo6 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("demo3\\demo2");
  4. DirectoryStream<Path> paths=Files.newDirectoryStream(path);
  5. for(Path p:paths){
  6. System.out.println(p.getFileName());
  7. }
  8. }
  9. }

遍历整个文件目录

FileVisitor需要调用方自行实现,然后作为参数传入walkFileTree();
FileVisitor的每个方法会在遍历过程中被调用多次。

  1. public class FilesDemo7 {
  2. public static void main(String[] args) throws IOException {
  3. Path path= Paths.get("demo3\\demo2");
  4. List<Path> paths=new ArrayList<>();
  5. Files.walkFileTree(path,new FileVisitor(paths));
  6. System.out.println("paths:"+paths);
  7. }
  8. private static class FileVisitor extends SimpleFileVisitor<Path> {
  9. private List<Path> paths;
  10. public FileVisitor(List<Path> paths){
  11. this.paths=paths;
  12. }
  13. @Override
  14. public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
  15. if(file.toString().endsWith(".txt")){
  16. paths.add(file.getFileName());
  17. }
  18. return super.visitFile(file, attrs);
  19. }
  20. }
  21. }

输出结果:

  1. paths:[a.txt, test2.txt, test.txt, test3.txt]

参考资料