程序读取数据模型
传统 IO 和 NIO 的不同
传统 IO 特点
- 面向流。
- 单向(只能读或只能写)。
- 程序读写间接和操作系统交互。
- 读写线程阻塞。
NIO 特点
- 面向缓冲区。
- 双向(既能读,又能写)。
- 程序读写通过 Channel,程序和 Channel 之间的交互通过缓冲区。
- 读写线程不阻塞。(然而 selector 的 select() 会阻塞)。
NIO 读取数据模型
NIO 在读取数据时,是数据通过 Channel 写入缓冲区,程序从缓冲区读取数据。写数据也是同理。
NIO 四大外围组件
NIO 有 4 个外围组件,它们是:
- Buffer:为了读或写,存储数据的容器。
- Channel:与某些组件建设连贯,相似 Java IO 中的流,进行读或写操作,然而和 IO 流不同的是,Channel 是双向的。
- Charsets:蕴含字符集(charsets)、解码器(decoders)、编码器(encoders),能够用来进行 byte 与 unicode 的转换。
- Selector:通过 Selector,能够与多个 Channel 一起工作。
Buffer
Buffer 是一个存储固定数据大小、存储 Java 根本数据类型数据的容器。一个 Buffer 由上面这些组成:
• Capacity :容器容量• Limit: 不能读或写的索引• Position: 下一个读或写的元素索引• Flip: 切换 IO 操作(读写操作)• Rewind: 设置 position = 0,limit 放弃不变• Mark: 在 Buffer 标记一个地位• Reset: 将 position 重置为 mark 的地位
间接字节缓冲区
Java 能够应用 ByteBuffer 的 allocateDirect 办法创立间接缓冲区:
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
相比拟于非间接缓冲区。IO 效率会更高。因为咱们在应用非间接缓冲区时,操作系统会创立一个长期间接缓冲区,将非间接缓冲区中的内容读入创立的长期间接缓冲区再进行操作。如果单从这点来看,应用间接缓冲区是 IO 最好的抉择。
然而在 Java 中创立间接缓冲区,是间接调用操作系统的 API 办法来创立的,会绕过 JVM 的内存治理,而且 JVM 针对缓冲区也有优化,所以可能应用间接缓冲区不是最好的形式。
往 ByteBuffer 写入数据
public class ByteBufferReadDemo { /** * 读取文件门路 */ private static final String INPUT_FILE = "./resources/input.txt"; /** * 缓冲区大小 */ private static final int BYTE_BUFFER_LENGTH = 1024; public static void main(String[] args) { // 通过 IO 流获取 Channel try (FileChannel fileChannel = new FileInputStream(INPUT_FILE).getChannel()) { // 创立 ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(BYTE_BUFFER_LENGTH); StringBuilder content = new StringBuilder(); int read = 0; // 数据从通道读出(写入到 ByteBuffer) while ((read = fileChannel.read(byteBuffer)) != -1) { content.append(new String(byteBuffer.array(), 0, read)); // 让 ByteBuffer 复原如初:position = 0, limit = capacity, 丢掉 mark byteBuffer.clear(); } System.out.println(content); } catch (IOException e) { throw new RuntimeException("Unable to read file", e); } }}
Buffer 读写原理
ByteBuffer 继承 Buffer 抽象类,很重要的三个属性是:
# 读或者写的开始地位 private int position = 0; # 限度读或写的地位 private int limit; # 容量 private int capacity;
当新创建一个容量为 10 的 ByteBuffer 对象时,它们三个的地位别离为:
此时 ByteBuffer 为写模式,能够从 position 开始写, limit 是最多能写到的地位,此时 limit = capacity。
随后咱们往 buffer 写入 1234,buffer 内部结构变为:
position 挪动到最小的一个可写地位 5(position = 5)。
此时咱们想要将 buffer 数据读出,就要转成读模式(调用 flip() 办法):
position 挪动到 buffer 中第一个可读的地位,limit 挪动到 buffer 中最初一个可读的地位。
咱们将数据全副读出,此时 buffer 外部变为:
此时 position = limit,证实曾经没有可读。
此时咱们应用 clear(),将 ByteBuffer 复原,此时 buffer 回到写模式(默认是写模式):
compact()和 clear()的区别就是,clear() 会间接让 buffer 回到刚创立的状态,而 compact() 会压缩还没被读的数据。比方咱们只读了 1 和 2:
此时调用 compact(),buffer 内部结构会变为:
Channel
能够通过 Channel 对 Buffer 读写。
创立一个 FileChannel:
// 创立一个 File 文件对象 final File file = new File(FileChannelReadExample.class.getClassLoader().getResource(path).getFile()); // 依据须要读或者须要写创立不同的 FileChannel return fileOperation == FileOperation.READ ? new FileInputStream(file).getChannel() : new FileOutputStream(file).getChannel();
Charsets
蕴含 charsets、decoders、encoders。通过 Charsets 能够进行 unicode 与 byte 的转换。
编码与解码
编码(Encoding):一序列字符 -> 字节。
解码(Decoding):字节 -> 字符。
Charset 应用
// 拿到 UTF-8 字符集 Charset utf8Charset = Charset.forName("UTF-8"); // 编码 ByteBuffer byteBuffer = utf8Charset.encode("zhangsan"); // 解码 System.out.println(utf8Charset.decode(byteBuffer));
Selector
Selector
能够多路复用 SelectableChannel
,当 SelectableChannel
有 IO 事件产生时,Selector
就会告诉咱们的程序。
这里的 IO 事件(SelectableChannel
连贯 Selectors
的事件)包含:
- Connect
- Accept
- Read
- Write
SelectableChannel
:能够多路复用的连贯 Selecotr 的通道。
Server/Client Demo
应用 Java NIO 实现一个简略的 Server、Client 示例:
Server
public class Server { protected static final String HOST = "127.0.0.1"; protected static final int PORT = 8899; public static void main(String[] args) { try { ServerSocketChannel serverSocket = ServerSocketChannel.open(); InetSocketAddress hostAddress = new InetSocketAddress(HOST, PORT); // 绑定端口 serverSocket.bind(hostAddress); // 设置为非阻塞 serverSocket.configureBlocking(false); // 创立 selector Selector selector = Selector.open(); // Channel 注册到 selector(ACCEPT 事件) serverSocket.register(selector, serverSocket.validOps(), null); while (true) { // select 是阻塞的 int selectKeyNums = selector.select(); if (selectKeyNums > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator(); while (selectionKeyIterator.hasNext()) { SelectionKey selectionKey = selectionKeyIterator.next(); if (selectionKey.isAcceptable()) { // 注册读事件 registerRead(serverSocket, selector); } else if (selectionKey.isReadable()) { // 解决写事件 dealRead(selectionKey); } else { System.out.println("Invalid selection key"); } // 每次事件操作做完要删除,否则会持续读,报错 selectionKeyIterator.remove(); } } } } catch (Exception e) { e.printStackTrace(); } } public static void registerRead(ServerSocketChannel channel, Selector selector) throws IOException { SocketChannel client = channel.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ, null); System.out.println(client.getRemoteAddress() + " connected!"); } public static void dealRead(SelectionKey selectionKey) { try { SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = channel.read(byteBuffer); if (read == -1) { selectionKey.channel(); channel.close(); } else { byteBuffer.flip(); System.out.println("from " + channel.getRemoteAddress() + " reve a message: " + StandardCharsets.UTF_8.decode(byteBuffer)); } } catch (Exception e) { e.printStackTrace(); } }}
Client
public class Client { public static void main(String[] args) { try { InetSocketAddress hostAddress = new InetSocketAddress(Server.HOST, Server.PORT); SocketChannel client = SocketChannel.open(hostAddress); Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { client.write(StandardCharsets.UTF_8.encode(sc.next())); } } catch (Exception e) { e.printStackTrace(); } }}
总结
IO 与 NIO 比照:
NIO 四大外围组件:
- ByteBuffer (存储数据的容器)
- Charsets (不便字符和字节的转化)
- Channels (真正 IO 的中央)
- Selectors (多路复用 SelectableChannel)
它们四个互相配合,形成 NIO 的流程。