共计 5271 个字符,预计需要花费 14 分钟才能阅读完成。
程序读取数据模型
传统 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 的流程。