NIO知识总结一

感兴趣的朋友可以去我的语雀平台进行查看更多的知识。
https://www.yuque.com/ambition-bcpii/muziteng

1. NIO 概述

1.1 IO 概述

IO 的操作方式通常分为几种:同步阻塞 BIO同步非阻塞 NIO异步非阻塞 AIO

  • 在 JDK1.4 之前,我们建立网络连接的时候采用的是 BIO 模式

  • Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 支持面

    向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。BIO 与 NIO 一个比较重要的不同是,我们使用

    BIO 的时候往往会引入多线程,每个连接对应一个单独的线程;而 NIO 则是使用单线程或者只使用少量的多线程,让连接共用一个线

    程。

  • AIO 也就是 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。

1.2 阻塞 IO (BIO)

阻塞 IO(BIO)是最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象,直至有可供读取的数据或者数据能够写入

特点:

  • 服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求,当客户较多时,使得占用资源较大。
  • 虽然可以使用线程池,但是当线程池中的线程都被占用时,再有客户端请求连接时,也会导致没有线程来进行处理。

模型图:

image-20220924203319983

1.3 非阻塞 IO(NIO)

NIO 采用非阻塞模式,基于 Reactor 模式的工作方式,I/O 调用不会被阻塞。

它的实现过程是:会先对每个客户端注册感兴趣的事件,然后有一个线程专门去轮询每个客户端是否有事件发生,当有事件发生时,便顺

序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:

image-20220924203521946

NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对

象告诉我们所发生的事件,如下图所示:

image-20220924203638092

NIO 的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,一个选择器线程可以同时处理成

千上万个连接,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

image-20220924203739838

1.4 异步非阻塞 IO(AIO)

异步 IO 是基于事件和回调机制实现的,也就是说 AIO 模式不需要selector 操作,而是是事件驱动形式,也就是当客户端发送数据之后,

会主动通知服务器,接着服务器再进行读写操作。

1.5 NIO 核心组件

Java NIO 由以下几个核心部分组成:

  • Channel
  • Buffer
  • Selector
1.5.1 Channel

首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:

InputStream, OutputStream。而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作,**可以从 channel 将数据读入 **

buffer,也可以将 buffer 的数据写入channel

常见的 Channel 有

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel

1.5.2 Buffer

Buffer 则用来缓冲读写数据,常见的 buffer 有

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer
1.5.3 Selector

Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在

一个聊天服务器中。要使用 Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通

道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等

1.5.4 三者关系
  1. 一个 Channel 就像一个流,只是 Channel 是双向的,Channel 读数据到 Buffer,Buffer 写数据到 Channel

image-20220924204428619

  1. 一个 selector 允许一个线程处理多个 channel

image-20220924204507445

2. FileChannel

2.1 Channel 概述

Java NIO 的通道类似流,但又有些不同

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入

从通道读取数据到缓冲区,从缓冲区写入数据到通道

image-20220924204707650

2.2 FileChannel详解

常用方法

image-20220924205001429

read() 方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾

2.2.1 从 FileChannel 读取数据
try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
    channel.read(byteBuffer);
} catch (Exception e) {
    throw new RuntimeException(e);
}
2.2.2 向 FileChannel 写入数据
try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(48);
    String newData = "New String to write to file..." + System.currentTimeMillis();
    byteBuffer.put(newData.getBytes(StandardCharsets.UTF_8));
    byteBuffer.flip();
    while (byteBuffer.hasRemaining()) {
        channel.write(byteBuffer);
    }
} catch (IOException e) {
    throw new RuntimeException(e);
}

注意,用完 FileChannel 后需要将 FileChannel 进行关闭

2.2.3 其他方法

position

有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可

以通过调用 position(long pos)方法设置 FileChannel 的当前位置。

size

FileChannel 实例的 size()方法将返回该实例所关联文件的大小。

truncate

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。

force

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。

force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。\

transferTo 和 transferFrom

这两个方法用来通道之间的数据传输。如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个

channel

FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中

transferTo()方法将数据从 FileChannel 传输到其他的 channel 中

try (FileChannel from = new RandomAccessFile("data.txt", "rw").getChannel();
     FileChannel to = new RandomAccessFile("out.txt", "rw").getChannel()) {
    from.transferTo(0, from.size(), to);
} catch (Exception e) {
    e.printStackTrace();
}

2.3 Scatter/Gather

Java NIO 开始支持 scatter/gather,scatter/gather 用于描述从 Channel 中读取或者写入到 Channel 的操作。

分散(scatter)从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer 中。因此,Channel 将从 Channel 中读取的数据“分

散(scatter)”到多个 Buffer 中。

聚集(gather)写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个Channel,因此,Channel 将多个 Buffer 中的数据“聚

集(gather)”后发送到 Channel。

scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头

分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。

2.3.1 Scattering Reads

Scattering Reads 是指数据从一个 channel 读取到多个 buffer 中

image-20220924211201225

read()方法按照 buffer 在数组中的顺序将从 channel 中读取的数据写入到 buffer,当一个 buffer 被写满后,channel 紧接着向另一个

buffer 中写。

Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话

说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。

public static void main(String[] args) {
    ByteBuffer byteBuffer1 = ByteBuffer.allocate(128);
    ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
    ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
    try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
        channel.read(byteBuffers);
    } catch (Exception e) {
        e.printStackTrace();
    }
    byteBuffer1.flip();
    byteBuffer2.flip();
    System.out.println(Charset.defaultCharset().decode(byteBuffer1));
    System.out.println("==========");
    System.out.println(Charset.defaultCharset().decode(byteBuffer2));
}

输出:

data.txt文件如下:共129个字符
1234567890abcdasdasdasdasdasdasdasdasdasdasdasdasddgjsknfdkgljnsdfkljgnkjsfdbgkahjdbfkljabdfdFKJCLbnkljzshdbfljsndfkjnlzjkdfnsZDf  

输出:
1234567890abcdasdasdasdasdasdasdasdasdasdasdasdasddgjsknfdkgljnsdfkljgnkjsfdbgkahjdbfkljabdfdFKJCLbnkljzshdbfljsndfkjnlzjkdfnsZD
==========
f
2.3.2 Gathering Writes

Gathering Writes 是指数据从多个 buffer 写入到同一个 channel。

image-20220924212048279

buffers 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数据写入到 channel,注意**只有 position 和 limit **

之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这 58byte 的数据将被写入到

channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息

public static void main(String[] args) {
    ByteBuffer byteBuffer1 = ByteBuffer.allocate(128);
    ByteBuffer byteBuffer2 = ByteBuffer.allocate(128);
    byteBuffer1.put("Hi".getBytes(StandardCharsets.UTF_8));
    byteBuffer2.put("Hello".getBytes(StandardCharsets.UTF_8));
    ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
    try (FileChannel channel = new FileOutputStream("out.txt").getChannel()) {
        byteBuffer1.flip(); // 切换为读模式
        byteBuffer2.flip();
        channel.write(byteBuffers);
        System.out.println(channel.size()); // 7
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3. SocketChannel

3.1 概述

SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了

传统的单向流 API,,Channel同时支持读写。

3.2 ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的任务,不过它增加了通

道语义,因此能够在非阻塞模式下运行。

同 java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。ServerSocketChannel 的 accept()方法会返回

SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。

非阻塞下的 accept() 演示:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);	// 设置非阻塞模式
ssc.bind(new InetSocketAddress(8080));
ByteBuffer byteBuffer = ByteBuffer.wrap("Hello".getBytes(StandardCharsets.UTF_8));
while (true) {
    SocketChannel sc = ssc.accept();	// 监听新的连接
    if (sc == null) {
        System.out.println("null.....");
        Thread.sleep(2000);
    } else {
        System.out.println("Incoming connection from:" + sc.getRemoteAddress());
        sc.write(byteBuffer);
        sc.close();
    }
}

3.3 SocketChannel

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

SocketChannel 是一种面向流连接 sockets 套接字的可选择通道,有以下特点

  • SocketChannel 是用来连接 Socket 套接字

  • SocketChannel 主要用途用来处理网络 I/O 的通道

  • SocketChannel 是基于 TCP 连接传输

  • SocketChannel 实现了可选择通道,可以被多路复用的

  • SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式

演示

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
sc.configureBlocking(false);    // 设置未非阻塞模式
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
// SocketChannel 收到数据写入 Buffer中
sc.read(byteBuffer);  // 当为阻塞模式时,read 执行时会导致线程阻塞
sc.close();
System.out.println("Over");

3.4 DatagramChannel

DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel 是无连接的,每个数据报(datagram)都是一个自

包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的 socket 不同,DatagramChannel 可以发送单独的

数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处

的信息(源地址)。

DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(8898));  // 绑定端口
System.out.println("waiting...");
ByteBuffer buffer = ByteBuffer.allocate(32);

// SocketAddress 可以获得发包的 ip、端口等信息
SocketAddress socketAddress = server.receive(buffer);// 接收数据

ByteBuffer sendBuffer = ByteBuffer.wrap("Hello".getBytes(StandardCharsets.UTF_8));
server.send(sendBuffer, new InetSocketAddress("localhost", 8898));  // 发送数据

// UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收发送数据包
// read()和 write()只有在 connect()后才能使用,不然会抛NotYetConnectedException 异常
server.connect(new InetSocketAddress("localhost", 8080));
server.write(sendBuffer);

演示:

@Test
public void sendDatagram() throws Exception {
    DatagramChannel sendChannel = DatagramChannel.open();
    while (true) {
        sendChannel.send(ByteBuffer.wrap("发包".getBytes(StandardCharsets.UTF_8))
                         , new InetSocketAddress("localhost", 9999));
        System.out.println("发包端发包");
        Thread.sleep(1000);
    }
}

@Test
public void receive() throws Exception {
    DatagramChannel receiveChannel = DatagramChannel.open();
    receiveChannel.bind(new InetSocketAddress(9999));
    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
    while (true) {
        SocketAddress socketAddress = receiveChannel.receive(byteBuffer);
        byteBuffer.flip();
        System.out.println(socketAddress + " ");
        System.out.println(Charset.defaultCharset().decode(byteBuffer));
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ambition0823

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值