NIO
Java NIO 三大核心组件
- Buffer(缓冲区):每个客户端连接都会对应一个Buffer,读写数据通过缓冲区读写。
- Channel(通道):每个channel用于连接Buffer和Selector,通道可以进行双向读写。
- Selector(选择器):一个选择器对应多个通道,用于监听多个通道的事件。Selector可以监听所有的channel是否有数据要读取,当某个channel有数据时,就去处理,所有channel都没有数据时,线程可以去执行其他任务。
使用 NIO 模型操作 Socket 步骤:
- 创建 ServerSocketChannel 服务器;
- 创建多路复用器 Selector(每个操作系统创建出来的是不一样的 ,Windows创建的是 WindowsSelectorImpl)
- ServerSocketChannel 将建立连接事件注册到 Selector中(register 方法往 EPollArrayWrapper 中添加元素)
- 处理事件
- 如果是建立连接事件,则把客户端的读写请求也注册到Selector中;
- 如果是读写事件则按业务处理。
案例代码:
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false); // 配置成非阻塞式的channel
// 创建一个IO多路复用选择器
Selector selector = Selector.open();
// 注册
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞的方法,返回值代表发生事件的通道的个数
// 返回值 0 超时
// -1 错误
// select方法可以传递超时时间,如果不传的话是timeout最后会为-1表示不会超时
selector.select();// 如果不设的话客户端不操作会一直阻塞在这
// 只要走到这里,必然说明,发送了事情,有可读可写可连接的channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 这个事件处理完就删除
if (selectionKey.isAcceptable()) {
// 有客户端来连接了
// 三次握手建立连接
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("某某客户端连接来啦");
}
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = socketChannel.read(buffer);
if (read == -1) { // 如果是可读事件,然后又没有数据,说明是客户端与服务器端的连接断开了,
// 这个时候我们关闭通道,不然选择器会一直监听通道,导致不必要的业务执行
socketChannel.close();
} else {
System.out.println(new String(buffer.array(), 0, buffer.position()));
System.out.println("有信息需要读取");
}
}
iterator.remove();
}
}
}
}
doSelect 方法是由 WindowsSelectorImpl
类去实现的,这是select方法最后执行的方法,因为加了互斥锁,也是为什么说这里同步阻塞的原因。
俩问题:
-
当 Selector.select() 方法返回后,它会返回一组 SelectionKey 对象,这些对象代表了已经就绪的 I/O 通道,即对应的文件描述符上有事件发生。这些 SelectionKey 对象Key用来处理对应的事件。但是,如果不将已经处理过的 SelectionKey 对象从 Selector 中删除,下次调用 Selector.select() 方法时,这些已经处理过的 SelectionKey 对象扔然会被返回,导致多余的事件处理,影响性能问题。
-
删除的话,我们可以通过
SelectionKey.cancel()
方法来实现,并且使得对应的通道(即文件描述符)不再被Selector监视。(这是有问题的,这样的话以后这个 SelectionKey 就不会再被监听了)可以在迭代器使用的时候对其进行删除。
Netty 封装好后就帮我们解决了这种问题,不会出现事件处理完后续还会一直处理的现象。