NIO

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
NIO 与普通 I/O 的区别主要有以下:
1,BIO 是面向流的,而 NIO 是面向 Channel 或者面向缓冲区的,它的效率更高。
2,流是单向的,所以又分成 InputStream 和 OutputStream,而 Channel 是双向的,既可读也可写。
3,流只支持同步读写,而 Channel 是可以支持异步读写的。
4,流一般与字节数组或者字符数组配合使用,而 Channel 一般与 Buffer 配合使用。

阻塞与非阻塞

流与块

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. 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

通道(Channel)

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
NIO提供了多种通道对象,所有的通道对象都实现了Channel接口,通道包括以下类型:

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过 UDP 读写网络中数据;
  • SocketChannel:通过 TCP 读写网络中数据;客户端与服务端之间的 Channel.
  • ServerSocketChannel:可以监听新进来的 TCP 连接,仅用于服务端的 Channel


    图片.png

    NIO读数据

    使用NIO读取数据可以分为下面三个步骤。 以下展示了使用 NIO 读取文件文件的实例:

(1)从FileInputStream获取Channel。
(2)创建Buffer。
(3)将数据从Channel读取到Buffer中。

  1. public class FileChannelTest {
  2. public static void main(String[] args) throws IOException {
  3. // 从文件获取一个FileChannel
  4. FileChannel fileChannel = new RandomAccessFile("test.txt", "rw").getChannel();
  5. // 声明一个Byte类型的Buffer
  6. ByteBuffer buffer = ByteBuffer.allocate(10);
  7. // 将FileChannel中的数据读出到buffer中,-1表示读取完毕
  8. // buffer默认为写模式
  9. // read()方法是相对channel而言的,相对buffer就是写
  10. while ((fileChannel.read(buffer)) != -1) {
  11. // buffer切换为读模式
  12. buffer.flip();
  13. // buffer中是否有未读数据
  14. while (buffer.hasRemaining()) {
  15. // 未读数据的长度
  16. int remain = buffer.remaining();
  17. // 声明一个字节数组
  18. byte[] bytes = new byte[remain];
  19. // 将buffer中数据读出到字节数组中
  20. buffer.get(bytes);
  21. // 打印出来
  22. System.out.println(new String(bytes, StandardCharsets.UTF_8));
  23. }
  24. // 清空buffer,为下一次写入数据做准备
  25. // clear()会将buffer再次切换为写模式
  26. buffer.clear();
  27. }
  28. }
  29. }

将上面的fileChanel 抽取成一个工具类方法:

  1. public static void fastCopy(String src, String dist) throws IOException {
  2. /* 获得源文件的输入字节流 */
  3. FileInputStream fin = new FileInputStream(src);
  4. /* 获取输入字节流的文件通道 */
  5. FileChannel fcin = fin.getChannel();
  6. /* 获取目标文件的输出字节流 */
  7. FileOutputStream fout = new FileOutputStream(dist);
  8. /* 获取输出字节流的文件通道 */
  9. FileChannel fcout = fout.getChannel();
  10. /* 为缓冲区分配 1024 个字节 */
  11. ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  12. while (true) {
  13. /* 从输入通道中读取数据到缓冲区中 */
  14. int r = fcin.read(buffer);
  15. /* read() 返回 -1 表示 EOF */
  16. if (r == -1) {
  17. break;
  18. }
  19. /* 切换读写 */
  20. buffer.flip();
  21. /* 把缓冲区的内容写入输出文件中 */
  22. fcout.write(buffer);
  23. /* 清空缓冲区 */
  24. buffer.clear();
  25. }
  26. }

缓冲区(Buffer)

在NIO库中所以数据都用缓冲区处理的,发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。
所有的缓冲区类型都继承于抽象类Buffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,Buffer的类型有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer,最常用的就是ByteBuffer。
image.png
上面是按照基本类型的角度来划分的,其实针对每一种类型还有不同的内存实现,分为堆内存实现和直接内存实现,比如,ByteBuffer 又分为 HeapByteBuffer 和 DirectByteBuffer 两种不同的内存实现

图片.png

缓冲区属性

缓冲区实质上是一个特殊的数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。如果我们使用get()方法从缓冲区获取数据或者使用put()方法把数据写入缓冲区,都会引起缓冲区状态的变化。
在缓冲区中,最重要的属性有如下三个,它们一起合作完成对缓冲区内部状态的变化跟踪。

  • capacity:能够容纳多少数据,指定了底层数组的大小,在初始化后就不会改变,
  • position:当前已经读写的字节数,也就是读写的索引位置,表示的是最大可写或者最大可读的数据。它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。
  • limit:表示下一次可使用的位置,针对读模式表示下一个可读的位置,针对写模式表示下一个可写的位置,也是就是position增长不能超过limit。

状态变量的改变过程举例:
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
Java——NIO - 图7

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

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

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
Java——NIO - 图10

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

选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。[

](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20IO.md#1-创建选择器)

1. 创建选择器

  1. Selector selector =Selector.open();

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

  1. ServerSocketChannel ssChannel =ServerSocketChannel.open();
  2. ssChannel.configureBlocking(false);
  3. ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

io事件 说明
SelectionKey.OP_READ 通道可读事件 1 << 0 = 0000 0001
SelectionKey.OP_WRITE 通道可写事件 1 << 2 = 0000 0100
SelectionKey.OP_CONNECT 出现可接入的客户端事件,该事件主要被服务端使用 1 << 3 = 0000 1000
SelectionKey.OP_ACCEPT 出现通道连接成功事件,该事件主要出现在客户端连接服务端 1 << 4 = 0001 0000

四种事件的位正好是错开的,所以,我们可以使用 “位或” 操作监听多种感兴趣的事件。例如:

  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. } elseif (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. } elseif (key.isReadable()) {
  10. // ...
  11. }
  12. keyIterator.remove();
  13. }
  14. }

套接字 NIO 实例

服务器端

  1. import java.io.IOException;
  2. import java.net.InetSocketAddress;
  3. import java.net.ServerSocket;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.SelectionKey;
  6. import java.nio.channels.Selector;
  7. import java.nio.channels.ServerSocketChannel;
  8. import java.nio.channels.SocketChannel;
  9. import java.util.Iterator;
  10. import java.util.Set;
  11. public class NIOServer {
  12. public static void main(String[] args) throws IOException {
  13. Selector selector = Selector.open();
  14. ServerSocketChannel ssChannel = ServerSocketChannel.open();
  15. ssChannel.configureBlocking(false);
  16. ssChannel.register(selector, SelectionKey.OP_ACCEPT);
  17. ServerSocket serverSocket = ssChannel.socket();
  18. InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8090);
  19. serverSocket.bind(address);
  20. while (true) {
  21. selector.select();
  22. Set<SelectionKey> keys = selector.selectedKeys();
  23. Iterator<SelectionKey> keyIterator = keys.iterator();
  24. while (keyIterator.hasNext()) {
  25. SelectionKey key = keyIterator.next();
  26. if (key.isAcceptable()) {
  27. ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
  28. // 服务器会为每个新连接创建一个 SocketChannel
  29. SocketChannel sChannel = ssChannel1.accept();
  30. sChannel.configureBlocking(false);
  31. // 这个新连接主要用于从客户端读取数据
  32. sChannel.register(selector, SelectionKey.OP_READ);
  33. } else if (key.isReadable()) {
  34. SocketChannel sChannel = (SocketChannel) key.channel();
  35. System.out.println(readDataFromSocketChannel(sChannel));
  36. sChannel.close();
  37. }
  38. keyIterator.remove();
  39. }
  40. }
  41. }
  42. private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
  43. ByteBuffer buffer = ByteBuffer.allocate(1024);
  44. StringBuilder data = new StringBuilder();
  45. while (true) {
  46. buffer.clear();
  47. int n = sChannel.read(buffer);
  48. if (n == -1) {
  49. break;
  50. }
  51. buffer.flip();
  52. int limit = buffer.limit();
  53. char[] dst = new char[limit];
  54. for (int i = 0; i < limit; i++) {
  55. dst[i] = (char) buffer.get(i);
  56. }
  57. data.append(dst);
  58. buffer.clear();
  59. }
  60. return data.toString();
  61. }
  62. }

客户端

  1. import java.io.IOException;
  2. import java.io.OutputStream;
  3. import java.net.Socket;
  4. public class NIOClient {
  5. public static void main(String[] args) throws IOException {
  6. Socket socket =new Socket("127.0.0.1", 8090);
  7. OutputStream out = socket.getOutputStream();
  8. String s ="hello world";
  9. out.write(s.getBytes());
  10. out.close();
  11. }
  12. }