NIO实现原理
内容参考:
https://zhuanlan.zhihu.com/p/363504902
https://www.cnblogs.com/Courage129/p/14223988.html
https://www.cnblogs.com/fatmanhappycode/p/12345391.html
https://www.cnblogs.com/mikechenshare/p/16587635.html
几个概念
1.1 同步
同步是指当前线程调用一个方法之后,当前线程必须等到该方法调用返回后,才能继续执行后续的代码。
1.1.1 同步阻塞
同步阻塞是指在调用结果返回之前,当前线程会被挂起。当前线程只有在得到结果之后才会返回,然后才会继续往下执行。
1.1.2 同步非阻塞
同步非阻塞是指某个调用不能立刻得到结果时,该调用不会阻塞当前线程,此时当前线程可以不用等待结果就能继续往下执行其他的代码,等执行完别的代码再去检查一下之前的结果有没有返回。
1.2 异步
异步是指,当前线程在发出一个调用之后,这个调用就马上返回了,但是并没有返回结果,此时当前线程可以继续去执行别的代码,在调用发出后,调用者会通过状态、通知来通知调用者其返回结果,或者是通过回调函数来处理该调用的结果。
NIO三大组件:selector、channel、buffer
NIO中的三个核心分别是Selector、Channel、Buffer,他们之间的关系如下图:
1、每个 channel 都会对应一个 Buffer。
2、Selector 对应一个线程, 一个线程对应channel(连接)。
3、该图反应了有三个 channel 注册到 该 selector //程序。
4、程序切换到哪个 channel 是由事件决定的, Event 就是一个重要的概念。
5、Selector 会根据不同的事件,在各个通道上切换。
6、Buffer 就是一个内存块 , 底层是一个数组。
7、数据的读取写入是通过 Buffer, 这个和 BIO不同 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO的Buffer是可以读也可以写, 需要 flip 方法切换channel是双向的
Buffer(缓冲区)
缓冲区本质上是一个 可以读写数据的内存块,可以理解成是一个 容器对象( 含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
Buffer有几个重要属性
属性 | 含义 |
---|---|
mark | 标记作用,buffer.position(0).mark()进行标记,buffer.reset();就会回到刚刚的标记位置 |
capacity | 它代表这个缓冲区的容量,一旦设定就不可以更改。 |
position | 初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置,从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了 |
Limit | 写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。 |
刚刚初始化时,position指向Buffer的最左边,而limit和capacity都指向buffer数组的最右边。
Channel(通道)
Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。
FileChannel类
本地文件IO通道,用于读取、写入、映射和操作文件的通道,使用文件通道操作文件的一般流程为:
1)获取通道
文件通道通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式。
// 获取文件通道
FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
2)创建字节缓冲区
文件相关的字节缓冲区有两种,一种是基于堆的 HeapByteBuffer,另一种是基于文件映射,放在堆外内存中的 MappedByteBuffer。
// 分配字节缓存
ByteBuffer buf = ByteBuffer.allocate(10);
3)读写操作
读取数据一般需要一个循环结构来读取数据,读取数据时需要注意切换 ByteBuffer 的读写模式。
while (channel.read(buf) != -1){ // 读取通道中的数据,并写入到 buf 中
buf.flip(); // 缓存区切换到读模式
while (buf.position() < buf.limit()){ // 读取 buf 中的数据
text.append((char)buf.get());
}
buf.clear(); // 清空 buffer,缓存区切换到写模式
}
写入数据
for (int i = 0; i < text.length(); i++) {
buf.put((byte)text.charAt(i)); // 填充缓冲区,需要将 2 字节的 char 强转为 1 自己的 byte
if (buf.position() == buf.limit() || i == text.length() - 1) { // 缓存区已满或者已经遍历到最后一个字符
buf.flip(); // 将缓冲区由写模式置为读模式
channel.write(buf); // 将缓冲区的数据写到通道
buf.clear(); // 清空缓存区,将缓冲区置为写模式,下次才能使用
}
}
4)将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 方法可以确保对文件的操作能够更新到磁盘。
channel.force(false);
5)关闭通道
channel.close();
SocketChannel类
网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。
TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:
1)打开通道,连接到服务端。
SocketChannel channel = SocketChannel.open(); // 打开通道,此时还没有打开 TCP 连接
channel.connect(new InetSocketAddress("localhost", 9090)); // 连接到服务端
2)分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(10); // 分配一个 10 字节的缓冲区,不实用,容量太小
3)配置是否为阻塞方式。(默认为阻塞方式)
channel.configureBlocking(false); // 配置通道为非阻塞模式
4)与服务端进行数据交互
5)关闭连接
channel.close(); // 关闭通道
ServerSocketChannel类
网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:
1)打开一个 ServerSocketChannel 通道, 绑定端口。
ServerSocketChannel server = ServerSocketChannel.open(); // 打开通道
2)绑定端口
server.bind(new InetSocketAddress(9090)); // 绑定端口
3)阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信。等待连接到来的代码一般放在一个循环结构中。
SocketChannel client = server.accept(); // 阻塞,直到有连接过来
4)通过 SocketChannel 与客户端进行数据交互
5)关闭 SocketChannel
client.close();
Selector(选择器)
在传统的BIO当中,监听每个客户端的请求都需要一个线程去处理,线程数的上升会涉及到大量的上下文切换的操作,这也是非常浪费性能的。Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
Selector使用步骤
1 获取选择器
与通道和缓冲区的获取类似,选择器的获取也是通过静态工厂方法 open() 来得到的。
Selector selector = Selector.open(); // 获取一个选择器实例
2 获取可选择通道
能够被选择器监控的通道必须实现了 SelectableChannel 接口,并且需要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打开 SocketChannel 并连接到本机 9090 端口
socketChannel.configureBlocking(false); // 配置通道为非阻塞模式
3 将通道注册到选择器
通道在被指定的选择器监控之前,应该先告诉选择器,并且告知监控的事件,即:将通道注册到选择器。
通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来完成,ops 表示关注的事件,如果需要关注该通道的多个 I/O 事件,可以传入这些事件类型或运算之后的结果。这些事件必须是通道所支持的,否则抛出 IllegalArgumentException。
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 将套接字通过到注册到选择器,关注 read 和 write 事件
4 轮询 select 就绪事件
通过调用选择器的 Selector.select() 方法可以获取就绪事件,该方法会将就绪事件放到一个 SelectionKey 集合中,然后返回就绪的事件的个数。这个方法映射多路复用 I/O 模型中的 select 系统调用,它是一个阻塞方法。正常情况下,直到至少有一个就绪事件,或者其它线程调用了当前 Selector 对象的 wakeup() 方法,或者当前线程被中断时返回。
while (selector.select() > 0){ // 轮询,且返回时有就绪事件
Set<SelectionKey> keys = selector.selectedKeys(); // 获取就绪事件集合
.......
}
SelectionKey
SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4
例子
NIOServer端
public class ServerTest {
//1. 定义成员属性:选择器、服务端通道、端口
private Selector selector;
private ServerSocketChannel ssChannel;
private static final int PORT = 9999;
/**
* 构造方法
* @throws IOException
*/
public ServerTest() throws IOException {
// 创建 Selector
this.selector = Selector.open();
// 创建 ServerSocketChannel
this.ssChannel = ServerSocketChannel.open();
// 为 ServerSocketChannel 绑定端口
ssChannel.bind(new InetSocketAddress(PORT));
// ServerSocketChannel 设置非阻塞模式
ssChannel.configureBlocking(false);
// 将channel注册到selector上,监听连接事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端创建成功");
}
/**
* 监听方法
*/
public void listen() {
System.out.println("等待连接。。。。");
// 循环等待新接入的连接
while (true) {
try {
// select()方法返回的int值表示有多少通道已经就绪
if (selector.select() > 0) {
// 获取可用channel的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
// 开始遍历这些准备好的事件
while (iterator.hasNext()) {
// selectionKey实例
SelectionKey selectionKey = (SelectionKey) iterator.next();
iterator.remove();
// 如果是 接入事件
if (selectionKey.isAcceptable()) {
acceptHandler(ssChannel, selector);
}
// 如果是 可读事件
if (selectionKey.isReadable()) {
readHandler(selectionKey, selector);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 可读事件
* @param selectionKey
* @param selector
*/
private void readHandler(SelectionKey selectionKey, Selector selector) {
//得到当前客户端通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//创建缓冲区对象开始接受客户端通道数据
ByteBuffer buffer = ByteBuffer.allocate(4);
// 循环读取客户端请求信息
String request = "";
try {
while (socketChannel.read(buffer) > 0) {
// 切换buffer为读模式
buffer.flip();
// 读取buffer中的内容
request += Charset.forName("UTF-8").decode(buffer);
}
System.out.println("接收到客户端消息:" + bytes2hex(request.getBytes()));
buffer.flip();
socketChannel.write(ByteBuffer.wrap(new byte[] {0x00, 0x02}));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 接入事件
* @param ssChannel
* @param selector
*/
private void acceptHandler(ServerSocketChannel ssChannel, Selector selector) {
try {
// 直接获取当前接入的客户端通道
SocketChannel schannel = ssChannel.accept();
// 切换成非阻塞模式
schannel.configureBlocking(false);
// 将本客户端通道注册到选择器
System.out.println(schannel.getRemoteAddress() + " 上线 ");
schannel.register(selector , SelectionKey.OP_READ);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String bytes2hex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
String tmp;
sb.append("[");
for (byte b : bytes) {
// 将每个字节与0xFF进行与运算,然后转化为10进制,然后借助于Integer再转化为16进制
tmp = Integer.toHexString(0xFF & b);
if (tmp.length() == 1) {
tmp = "0" + tmp;//只有一位的前面补个0
}
sb.append(tmp).append(" ");//每个字节用空格断开
}
sb.delete(sb.length() - 1, sb.length());//删除最后一个字节后面对于的空格
sb.append("]");
return sb.toString();
}
public static void main(String[] args) throws IOException {
ServerTest server = new ServerTest();
server.listen();
}
}
NIOClient端
public class ClientTest {
//定义相关的属性
private final String HOST = "127.0.0.1"; // 服务器的ip
private final int PORT = 9999; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
public ClientTest() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = socketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将channel 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
//向服务器发送消息
public void sendInfo(byte[] info) {
try {
socketChannel.write(ByteBuffer.wrap(info));
}catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if(readChannels > 0) {//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isReadable()) {
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(2);
//读取
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(ServerTest.bytes2hex(msg.getBytes()));
}
}
iterator.remove(); //删除当前的selectionKey, 防止重复操作
} else {
//System.out.println("没有可以用的通道...");
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
ClientTest client = new ClientTest();
//发送数据给服务器端
while (true) {
client.sendInfo(new byte[] {0x00, 0x01});
client.readInfo();
}
}
}