前言
前面两篇文章 (Java NIO 之理解 I/O 模型(一),Java NIO 之理解 I/O 模型(二)) 介绍了, IO 的机制, 以及几种 IO 模型的内容, 还有涉及到的设计模式. 这次要写一些更贴近实际一些的内容了, 终于要说到了 Java 中的各种 IO 了. 我也是边学边理解, 有写的不对的地方, 欢迎小伙伴们指出和补充.
Java 中的 IO 分类
BIO
BIO 是指 Blocking IO 在 JDK1.0 的时候就引入了, 直到 JDK1.4 一直都是 Java 中唯一的 IO 方式. 它的主要实现方式就是, 一个线程执行一个请求, 如果请求数据量较大线程就会一直占用着, 又或者请求什么也不做, 也是会占用一个线程的, 这样当客户端请求数量变多时, 服务端线程数也跟着变多, 最终就会导致服务端 CPU 崩溃的. 当然可以使用线程池来减小 CPU 的压力, 但是毕竟线程池也不是从本质上解决问题(长链接, 线程池大小, 以及拒绝策略等等因素都要考虑).
无论是网络连接还是本地读取输出操作都是这种方式. 具体的例子我就不举了(毕竟 BIO 不是本次的重点).
NIO
Java 中的 NIO 其实就是使用的多路 I/O 复用模型, 前面的文章已经介绍过原理了, 但是在理解 Java 的 NIO 之前, 还是先介绍几个 Java NIO 的基础概念: Channel(通道),Buffer(缓冲区),Selector(选择器).
Channel(通道)
Channel 可以理解为, 互通的管道, 和 Java 的 IO 中的各种 Stream(InputStream,OutputStream 等等)一个等级, 只不过 Channel 是双向的, 而 Stream 是单向的. 通道的作用是将数据移入或移出道各种 I/O 源, 即可读又可写.
在 Java 中 Channel 类的层次结构相当复杂, 有多个接口和许多可选操作. 不过, 常用的也就几个.
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel 可以对文件进行读和写, DatagramChannel 可以以 UDP 的协议来进行数据读写, SocketChannel 以 TCP 的协议来对网络两端进行读写, ServerSocketChanel 能够监听客户端发起的 TCP 连接, 并为每个 TCP 连接创建一个新的 SocketChannel 来进行数据读写.
Buffer(缓冲区)
Buffer 是一个高效的数据容器, 在 NIO 中所有的数据操作都必须经过缓冲区, 这点是和 BIO 不同的, BIO 是直接将数据写到 Stream 对象中的. 因为 Stream 对象的设计是按顺序一个字节一个字节的传送数据. 虽然出于性能考虑, 也可以传递字节数组, 但是基本概念都是一个字节一个字节的传递数据. 通道与之不同之处在于, 通道会传送缓冲区的数据块, 而且通道的基本概念就是按照一个数据块一个数据块的去读和写. 所以也可以将缓冲区理解为一个字节数组, 专门用来存储以及准备好出入通道的字节.
如下图:
如上图所示, 无论是客户端发送和接收数据, 还是服务端接收和相应数据, 都是从缓冲区中进行数据操作的.
在 Java 中除了 boolean 外, 所有的基本数据类型都有特定的 Buffer 子类:
ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer. 网络程序几乎只会使用 ByteBuffer, 但程序偶尔也会使用其他类型来取代 ByteBuffer.
除了数据列表外, 每个缓冲区都记录了信息的 4 个关键部分. 无论是何种类型, 都有相同的方法来获取和设置这些值:
位置(position)
缓冲区中将读取或写入的下一个位置. 这个位置从 0 开始计, 最大值等于缓冲区的大小. 可以用下面两个方法获取和设置.
- public final int position();
- public final Buffer position(int newPosition);
容量(capacity)
缓冲区可以保存的最大数目. 容量值在创建缓冲区时设置, 此后不能改变.
可以用以下方法读取:
public final int capacity();
限度(limit)
缓冲区中可访问数据的末尾位置. 只要不改变限度, 就无法读 / 写超过这个位置的数据, 即使缓冲区有更大的容量也没有用. 限度可以用下面两个放获取和设置.
- public final int limit();
- public final Buffer limit(int newLimit);
标记(mark)
缓冲区中客户端指定的索引. 通过调用 mark()可以将标记设置为当前位置. 调用 reset()可以将当前位置设置为所标识的位置.
- public final Buffer mark() ;
- public final Buffer reset();
如果将位置设置为低于现有标记, 则丢弃这个标记.
与读取 InputStream 不同, 读取缓冲区实际上不会以任何方式改变缓冲区中的数据. 只可能向前或向后设置位置, 从而可以从缓冲区中某个特定位置开始读取. 类似的程序可以调整限度, 从而控制将要读取的数据的末尾. 只有容量是固定的.
Selector(选择器)
Selector 是 Java NIO 中最重要的一部分, Selector 的作用就是用单线程来轮询处理注册的 Channel, 一旦哪个 Channel 的数据准备就绪了, 就可以进行处理了.
如下图:
使用 Selector, 要先向 Selector 中注册 Channel, 然后调用它的 select()方法, 这个方法会一直阻塞到某个注册的 Channel 中的事件准备就绪. 一旦 select()方法返回, 线程就可以处理这些事件了, 比如新的连接进入, 数据接收等.
Selector 类并没有注册新通道的方法, register()方法是在 SelectableChannel 类中声明. SelectableChannel 类是实现自 Channel 接口的, 它支持将 Channel 注册到 Selector 中.
SelectableChannel 提供了两个注册 Channel 的方法:
- public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;
- public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException;
第一个参数代表通道要向哪个选择器注册. 第二个参数是 SelectionKey 类中的一个命名常量, 标识所注册的操作.
SelectionKey 定义了 4 个命名位常量, 用户选择操作类型:
- SelectionKey.OP_READ;
- SelectionKey.OP_WRITE;
- SelectionKey.OP_CONNECT;
- SelectionKey.OP_ACCEPT;
当一个通道需要在同一个选择器中关注多个操作, 只需要用户位 "或" 操作符组合这些常量即可.
channel.register(selector,SelectionKey.OP_READ | SelectionKey.OP_WRITE);
第三个参数是可选的, 代表键的附件. 这个参数通常用户存储链接状态, 例如: 如果要实现一个 web 服务器, 可能要附加一个 FileInputStream 或 FileChannel, 这个流或通道连接到服务器提供给客户端的本地文件.
例子:
复制文件的读写操作, 可以用来举例说明 NIO 的一个大致过程.
- public static void copyFileByNIO(String src,String dst) throws IOException {
- // 声明源文件和目标文件
- RandomAccessFile aFile = new RandomAccessFile(src, "rw");
- RandomAccessFile bFile = new RandomAccessFile(dst, "rw");
- // 获得传输通道 channel
- FileChannel inChannel = aFile.getChannel();
- FileChannel outChannel = bFile.getChannel();
- // 获得容器 buffer
- ByteBuffer buffer= ByteBuffer.allocate(1024);
- while(true){
- // 判断是否读完文件
- int eof =inChannel.read(buffer);
- if(eof==-1){
- break;
- }
- // 重设一下 buffer 的 position=0,limit=position
- buffer.flip();
- // 开始写
- outChannel.write(buffer);
- // 写完要重置 buffer, 重设 position=0,limit=capacity
- buffer.clear();
- }
- inChannel.close();
- outChannel.close();
- aFile.close();
- bFile.close();
- }
- AIO
AIO 这次就不介绍了, 我后续要单独的拿出一整篇来介绍 AIO.
来源: https://www.cnblogs.com/jimoer/p/11575610.html