一、流与块
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 变量不会改变,下面的讨论会忽略它。
- 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
- 在将缓冲区的数据写到输出通道之前,需要先调用 flip () 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
- 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
- 最后需要调用 clear () 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
3. 文件 NIO 实例
1 2 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 配置非阻塞也没有意义。
1. 创建选择器
Selector selector = Selector.open();
2. 将通道注册到选择器上
1 2 3
| ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰
指定要注册的具体事件类型:
1 2 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. 获取到达的事件
1 2 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 () 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内
1 2 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 实例
1 2 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);