NIO

与BIO的区别

传统BIO模型的缺点

202073116749

NIO 的线程模型

 while(channel=Selector.select()){//选择就绪的事件和对应的连接
      if(channel.event==accept){
         registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
      }
      if(channel.event==write){
         getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
      }
      if(channel.event==read){
          getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
      }
    }
   }

NIO 的 Reactor Proactor

Reactor

屏幕截图 2021-07-23 110347

事件驱动思想

Proactor

202172311918

Proactor 中,直接监听读/写操作是否完成,也就是说读/写操作是否 OS 来完成,并将读写数据放入一个缓冲区提供给程序

Buffer

核心类

屏幕截图 2020-09-28 140403

ByteBuffer

属性:

屏幕截图 2020-09-28 141745

对比项 HeapByteBuffer DirectByteBuffer
存储位置 Java Heap中 Native内存中
I/O 需要在用户地址空间和操作系统内核地址空间复制数据 不需复制
内存管理 Java GC回收,创建和回收开销少 通过调用System.gc要释放掉Java对象引用的DirectByteBuffer内空间, 如果Java对象长时间持有引用可能会导致Native内存泄漏,创建和回收内存开销较大
适用场景 并发连接数少于1000, I/O 操作较少时比较合适 数据量比较大、生命周期比较长的情况下比较合适

大多数垃圾收集过程中,都不会主动收集 DirectBuffer,其内部使用 Cleaner 和虚引用(PhantomReference)机制,其本身不是 public 类型,内部实现了一个 Deallocator 负责销毁的逻辑

零拷贝

文件输出例子

FileOutputStream fos = new FileOutputStream("file.txt");
FileChannel channel = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("20191204".getBytes());
// 翻转缓冲区
buffer.flip();
channel.write(buffer);
fos.close();

文件输入

File file = new File("file.txt");
FileInputStream fis = new FileInputStream(file);
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
FileChannel channel = fis.getChannel();
channel.read(buffer);
System.out.println(new String(buffer.array()));
fis.close();

文件复制

FileInputStream fis = new FileInputStream("file.txt");
FileOutputStream fos = new FileOutputStream("file1.txt");
FileChannel source = fis.getChannel();
FileChannel target = fos.getChannel();
target.transferFrom(source,0,source.size());
source.close();
target.close();

网络编程

它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接

stateDiagram-v2
    Selector --> Channel1: 轮询
    Selector --> Channel2: 轮询
    Selector --> Channel3: 轮询
    Selector --> Channel4: 轮询

一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系

Java NIO 中的 ServerSocketChannel 是一个可以监听新进来的 TCP 连接的通道, 就像标准 IO 中的 ServerSocket一样

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道

客户端

// 得到一个网络通道
SocketChannel channel = SocketChannel.open();
// 设置非阻塞方式
channel.configureBlocking(false);
// 提供服务器IP与端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 1999);
// 连接
if (!channel.connect(address)) {
    while (!channel.finishConnect()) {
        System.out.println("客户端:正在连接服务器");
    }
}
// 发送数据
ByteBuffer buffer = ByteBuffer.wrap("cxk 打篮球".getBytes());
channel.write(buffer);

服务端框架

// 获取网络通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 获取选择器
Selector selector = Selector.open();
// 绑定端口
serverSocketChannel.bind(new InetSocketAddress(1999));
// 设置为非阻塞方式(accept时不阻塞)
serverSocketChannel.configureBlocking(false);
// 注册选择器,让选择器监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
    // 每2000ms轮询一次,select返回的结果是客户数
    if (selector.select(2000) == 0){
        System.out.println("等待客户连接");
        continue;
    }
    // 获取准备连接的所有客户
    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
    while (iterator.hasNext()){
        SelectionKey key = iterator.next();
        if (key.isAcceptable()){
            // 客户端连接事件
            System.out.println("客户端连接");
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false); // 读取客户端数据时不会阻塞
            socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        }
        if (key.isReadable()){
            // 读取客户端数据事件
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            channel.read(buffer);
            System.out.println("客户端发来数据:"+new String(buffer.array()));
        }
        // 删除客户key,防止重复处理
        iterator.remove();
    }
}

系统层面的NIO

BIO模型

批注 2020-06-18 143426

弊端:每连接一线程

同步非阻塞 NIO

批注 2020-06-18 143440

使用一个线程处理N个连接读写

每次循环会发生大量无用的系统调用

多路复用器

同步IO模型

(select, pselect):

弊端:需要在内核与用户空间之间拷贝fd

批注 2020-06-18 143959

多路复用 无需再拷贝fd:

调用epoll_create创建一个fd指向共享空间

将客户端fd存放在共享空间 使用 epoll_ctl_add epoll_ctl_mod epoll_ctl_del

使用epoll_wait会返回可用fd列表 程序再对fd列表操作

批注 2020-06-18 144854