一、流与块
NIO 与 IO 主要区别: I/O 与 NIO 最重要的区别是 ** 数据打包 ** 和 ** 传输 ** 的方式。NIO 是 ** 非阻塞 ** 的。
I/O 以 ** 流 ** 的方式处理数据
NIO 以 ** 块 ** 的方式处理数据
1. IO 特点
- 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据
- 创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分
- 速度慢
2. NIO 特点
- 一次处理一个数据块
- 速度快
- 面向块的 I/O ** 缺少 ** 一些面向流的 I/O 所具有的 优雅性和简单性
二、通道与缓冲区
1. 通道
概念: 通道 Channel 是对原 I/O 包中的 流的模拟,可以通过它读取和写入数据。
通道 与 流 的区别:
- 流只能在 ** 一个方向上 ** 移动(一个流必须是 InputStream 或者 OutputStream 的子类)
- 通道是 双向的,可以用于读、写或者 同时用于读写
类型:
- FileChannel: 从 ** 文件 ** 中读写数据
-  DatagramChannel: 通过 UDP 读写网络中数据
-  SocketChannel: 通过 TCP 读写网络中数据
-  ServerSocketChannel: 可以 监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
2. 缓冲区
概念: 不会直接对通道进行读写数据,要 先经过缓冲区。
发送给一个通道的所有数据都必须首先放到缓冲区中。
同样地,从通道中读取的任何数据都要先读到缓冲区中。
原理: 缓冲区实质上是一个 数组,但它不仅仅是一个数组。
作用: 缓冲区提供了 对数据的结构化访问,而且还可以 跟踪系统的读 / 写进程。
类型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
2.1. 缓冲区状态变量
- capacity: 最大容量
-  position: 当前已经读写的字节数
-  limit: 可读写的容量限制(写入时为最大容量大小,读取时为当前数据大小)
状态变量的改变过程例子:
- 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
![image.png]()
- 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
![image.png]()
- 在将缓冲区的数据写到输出通道之前,需要先调用 flip () 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
![image.png]()
- 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
![image.png]()
- 最后需要调用 clear () 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
![image.png]()
3. 文件 NIO 实例
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | public static void fastCopy(String src, String dist) throws IOException {
 
 FileInputStream fin = new FileInputStream(src);
 
 
 FileChannel fcin = fin.getChannel();
 
 
 FileOutputStream fout = new FileOutputStream(dist);
 
 
 FileChannel fcout = fout.getChannel();
 
 
 ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
 
 while (true) {
 
 
 int r = fcin.read(buffer);
 
 
 if (r == -1) {
 break;
 }
 
 
 buffer.flip();
 
 
 fcout.write(buffer);
 
 
 buffer.clear();
 }
 }
 
 | 
三、选择器
非阻塞 IO: NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
Selector 监听实现多路复用:
- NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去 监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
- 通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,Selector 就 ** 不会进入阻塞状态 ** 一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
良好性能: 因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
注意: 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
![image.png]()
1. 创建选择器
Selector selector = Selector.open();
2. 将通道注册到选择器上
| 12
 3
 
 | ServerSocketChannel ssChannel = ServerSocketChannel.open();ssChannel.configureBlocking(false);
 ssChannel.register(selector, SelectionKey.OP_ACCEPT);
 
 | 
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰
指定要注册的具体事件类型:
| 12
 3
 4
 
 | public static final int OP_READ = 1 << 0;public static final int OP_WRITE = 1 << 2;
 public static final int OP_CONNECT = 1 << 3;
 public static final int OP_ACCEPT = 1 << 4;
 
 | 
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
| 1
 | int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
 | 
3. 监听事件
int num = selector.select();
使用 select () 来监听到达的事件,它会一直阻塞直到有至少一个事件到达
4. 获取到达的事件
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = keys.iterator();
 while (keyIterator.hasNext()) {
 SelectionKey key = keyIterator.next();
 if (key.isAcceptable()) {
 
 } else if (key.isReadable()) {
 
 }
 keyIterator.remove();
 }
 
 | 
5. 事件循环
因为一次 select () 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | while (true) {int num = selector.select();
 Set<SelectionKey> keys = selector.selectedKeys();
 Iterator<SelectionKey> keyIterator = keys.iterator();
 while (keyIterator.hasNext()) {
 SelectionKey key = keyIterator.next();
 if (key.isAcceptable()) {
 
 } else if (key.isReadable()) {
 
 }
 keyIterator.remove();
 }
 }
 
 | 
6. 套接字 NIO 实例
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 
 | public class NIOServer {
 public static void main(String[] args) throws IOException {
 
 Selector selector = Selector.open();
 
 ServerSocketChannel ssChannel = ServerSocketChannel.open();
 ssChannel.configureBlocking(false);
 ssChannel.register(selector, SelectionKey.OP_ACCEPT);
 
 ServerSocket serverSocket = ssChannel.socket();
 InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
 serverSocket.bind(address);
 
 while (true) {
 
 selector.select();
 Set<SelectionKey> keys = selector.selectedKeys();
 Iterator<SelectionKey> keyIterator = keys.iterator();
 
 while (keyIterator.hasNext()) {
 
 SelectionKey key = keyIterator.next();
 
 if (key.isAcceptable()) {
 
 ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
 
 
 SocketChannel sChannel = ssChannel1.accept();
 sChannel.configureBlocking(false);
 
 
 sChannel.register(selector, SelectionKey.OP_READ);
 
 } else if (key.isReadable()) {
 
 SocketChannel sChannel = (SocketChannel) key.channel();
 System.out.println(readDataFromSocketChannel(sChannel));
 sChannel.close();
 }
 
 keyIterator.remove();
 }
 }
 }
 
 private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
 
 ByteBuffer buffer = ByteBuffer.allocate(1024);
 StringBuilder data = new StringBuilder();
 
 while (true) {
 
 buffer.clear();
 int n = sChannel.read(buffer);
 if (n == -1) {
 break;
 }
 buffer.flip();
 int limit = buffer.limit();
 char[] dst = new char[limit];
 for (int i = 0; i < limit; i++) {
 dst[i] = (char) buffer.get(i);
 }
 data.append(dst);
 buffer.clear();
 }
 return data.toString();
 }
 }
 
 public class NIOClient {
 
 public static void main(String[] args) throws IOException {
 Socket socket = new Socket("127.0.0.1", 8888);
 OutputStream out = socket.getOutputStream();
 String s = "hello world";
 out.write(s.getBytes());
 out.close();
 }
 }
 
 | 
四、内存映射文件
概念: 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,map () 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);