ToB企服应用市场:ToB评测及商务社交产业平台

标题: Java NIO(io模型,三大组件,网络编程) [打印本页]

作者: 科技颠覆者    时间: 2024-11-25 12:15
标题: Java NIO(io模型,三大组件,网络编程)
一、NIO

Java NIO(New I/O,新的输入输出)是 Java 1.4 引入的一套 I/O 库,相比传统的 IO(字节流和字符流),它重要用于处理高效的、非阻塞的 I/O 操作,特别是在需要处理大规模数据或高并发的场景中表现突出。Java NIO 提供了非阻塞模式、内存映射文件、缓冲区等一系列加强功能,实用于当代的高性能应用。
1. NIO 与传统 IO 的区别

在 Java 中,传统的 IO(字节流和字符流)是 阻塞 的,意味着每次读写操作都会阻塞当前线程,直到数据完全读取或写入。对于高并发场景,这种方式的效率较低。
1. BIO (Blocking I/O)

BIO 是 Java 的传统 I/O 模型,简称阻塞 I/O。
2. NIO (New I/O)

NIO 是 Java 1.4 引入的非阻塞 I/O 模型,提供更高效的多路复用能力。
3. BIO 和 NIO

特性BIO (Blocking I/O)NIO (New I/O)编程模型阻塞式编程非阻塞式编程线程模型一个线程处理一个连接一个线程可处理多个连接性能高并发时线程数可能爆炸,性能较低高并发时性能更优,占用资源少数据处理以流(Stream)为单位处理数据以块(Buffer)为单位处理数据实用场景连接数少,业务逻辑复杂高并发、多连接场景4. I/O 模型

I/O 模型描述了应用程序与内核之间进行输入/输出操作的方式,尤其是网络通信场景中的数据收发。I/O 模型的选择直接影响程序的性能、复杂度和实用场景。
常见的 I/O 模型有以下五种:
1. I/O 操作的两大阶段

I/O 操作通常分为两个阶段:
每种 I/O 模型在这两个阶段处理方式不同,导致了行为的差异。
2. 模型对比

模型阻塞阶段非阻塞能力是否高效阻塞 I/O数据准备、数据拷贝否低非阻塞 I/O无是较低(需轮询)I/O 多路复用等待数据准备是较高信号驱动 I/O等待数据准备是较高异步 I/O无是最高下面相识一下 NIO 中的一些 低级api 。(NIO 中也包罗一些高级抽象的api,比如 Files、Paths、Path等。)高级抽象 api 可参考链接文章 java 文件的操作(Path、Paths、Files)
2. NIO 的三大组件


3. Buffer

这里我们以 ByteBuffer 来举例
1. NIO 文件读写

读取一个文本文件,并输出里面的字符。Buffer 可读可写,因此有两种模式,读模式和写模式,需要手动切换
  1. public class Main {
  2.    public static void main(String[] args) throws IOException {
  3.                    // 创建一个可读的 channel
  4.        try (FileChannel fileChannel = new FileInputStream("test.txt").getChannel()) {
  5.            // 创建一个buff,容量为10字节, 容量固定不可变。初始状态为 写 模式
  6.            ByteBuffer buffer = ByteBuffer.allocate(10);
  7.            // ByteBuffer buffer = ByteBuffer.allocateDirect(10);
  8.            while (true) {
  9.                // 从管道中读取数据,向 buffer 中写入。(read是相对于管道的,从管道中读)
  10.                int len = fileChannel.read(buffer);
  11.                if (len == -1) {    // len是读取到的字节数, 如果没有去读到返回 -1
  12.                    break;
  13.                }
  14.                buffer.flip();  // 切换成 读 模式
  15.                while (buffer.hasRemaining()) {
  16.                    System.out.println((char) buffer.get());
  17.                }
  18.                buffer.clear(); // 切换成 写 模式
  19.                // buffer.compact(); // 切换成 写 模式
  20.            }
  21.        }
  22.    }
  23. }
复制代码
下面我们对代码进行讲解:
2. ByteBuffer 常用方法

1. 读写数据

2. 状态查询

3. 数据操作

3. 文件编程

1. 文件channel
  1. FileChannel channel = new FileInputStream("test,txt").getChannel();
  2. FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel();
  3. FileChannel channel = new FileOutputStream("test.txt").getChannel();
复制代码
方式用途FileInputStream只读文件FileOutputStream只写文件RandomAccessFile读写文件,支持随机访问2. 字符串 和 ByteBuffer的转换
  1. // 1 插入数据后依然是写模式
  2. ByteBuffer buffer1 = ByteBuffer.allocate(10);
  3. buffer1.put("hello".getBytes());
  4. System.out.println(buffer1);
  5. // 2 插入数据后会变为 读模式
  6. ByteBuffer buffer2;
  7. buffer2 = StandardCharsets.UTF_8.encode("world");
  8. System.out.println(buffer2);
  9. // 3 插入数据后会变为 读模式
  10. ByteBuffer buffer3;
  11. buffer3 = ByteBuffer.wrap("hello world".getBytes());
  12. System.out.println(buffer3);
复制代码
将 ByteBuffer 转成 字符串
  1. StandardCharsets.UTF_8.decode(buffer1);
复制代码
3.  分散读取 和 集中写入

4. 粘包半包

粘包:指多条消息的数据被粘在一起,吸取端在读取时不能正确区分消息的界限。
半包:指一条消息的数据被拆分到多个 TCP 数据包中,吸取端读取时只能读取到部分数据。
  1. public class Main {
  2.     public static void main(String[] args) throws IOException {
  3.         ByteBuffer buffer = ByteBuffer.allocate(36);
  4.         buffer.put("hello\n".getBytes());
  5.         buffer.put("world\nhel".getBytes());        // 粘包
  6.         split(buffer);
  7.         buffer.put("lo world\n".getBytes());        // 半包
  8.         split(buffer);
  9.     }
  10.     public static void split(ByteBuffer buffer) {
  11.         buffer.flip();
  12.         for (int i = 0; i < buffer.limit();i ++) {
  13.             if (buffer.get(i) == '\n') {        // get(i) 不会改变 position
  14.                 int len = i + 1 - buffer.position();
  15.                 ByteBuffer bf = ByteBuffer.allocate(len);
  16.                 for (int j = 0; j < len;j ++) {
  17.                     bf.put(buffer.get());
  18.                 }
  19.                 bf.flip();
  20.                 System.out.println(StandardCharsets.UTF_8.decode(bf));
  21.                 // 最终输出了三次:hello\n  world\n  hello world\n
  22.             }
  23.         }
  24.         buffer.compact();
  25.     }
  26. }
复制代码
4. 网络编程

Java NIO(New I/O)是 Java 提供的一种高性能、非阻塞的 IO 模型,实用于开辟高并发的网络应用程序。
4.1 网络Channel

特性SocketChannelServerSocketChannelDatagramChannel协议TCPTCPUDP连接需要连接到服务器监听并接受客户端连接无需连接阻塞模式支持阻塞和非阻塞支持阻塞和非阻塞支持阻塞和非阻塞实用场景客户端通信服务端监听小型数据包传输数据传输方式流式传输流式传输数据报文传输4.2 明白阻塞和非阻塞

下面代码为一单线程服务端demo代码,代码是阻塞模式。(当把 configureBlocking 解释关掉后是非阻塞模式。)
当为阻塞模式下,如果没有客户端发送连接 ssn.accept() 会一直阻塞在这里,当有一个客户端发送连接后,代码往下执行会在 channel.read(bf) 阻塞,直到连接的客户端发送消息,如果此时有新的客户端发送连接,是不能正常 accept 的,因为代码阻塞在了 channel.read(bf)。(除非用多线程)
这里如果把:ServerSocketChannel  设置成非阻塞,那么 ssn.accept() 就不会阻塞,而是执行接下来的代码,当客户端连接时 sc 为 SocketChannel,没有连接时是null
同样,如果把 SocketChannel 设置成非阻塞,那么 channel.read(bf) 就不会阻塞。
  1. public class Service {
  2.     public static void main(String[] args) throws IOException {
  3.         ByteBuffer bf = ByteBuffer.allocate(16);
  4.         ServerSocketChannel ssn = ServerSocketChannel.open();
  5.         // ssn.configureBlocking(false);
  6.         ssn.bind(new InetSocketAddress(8888));
  7.         List<SocketChannel> scs = new ArrayList<>();
  8.         while (true) {
  9.             SocketChannel sc = ssn.accept();        // 阻塞
  10.             // sc.configureBlocking(false);
  11.             scs.add(sc);
  12.             for (SocketChannel channel : scs) {
  13.                 int len = channel.read(bf);                // 阻塞
  14.                 bf.flip();
  15.                 System.out.println(StandardCharsets.UTF_8.decode(bf));
  16.                 bf.compact();
  17.             }
  18.         }
  19.     }
  20. }
复制代码
上面代码在非阻塞模式下会一直在 while 循环中执行,如果加上         System.out.println("hello");, 可以发现一直在输出 hello,显然这样非常浪费资源,而 Selector 可以办理这个问题。
4.3 Selector

Selector 是 Java NIO 实现非阻塞 IO 的关键,允许一个线程同时监控多个通道的事件(如读、写、连接等)。可以通过它检测通道是否有就绪的事件(如可读、可写等)当不存在就绪事件时阻塞代码,有事件时取消阻塞。(多路复用)
Selector 的核心方法:
4.4 SelectionKey

用于表示一个通道(Channel)和选择器(Selector)之间的注册关系,同时维护该关系的状态和事件。
常用方法:
4.5 用Selector优化 4、1.2 非阻塞代码

这里我们优化之前代码,之前代码的问题是,当非阻塞时,while 循环中一直在执行,占用大量 cpu 等资源,这里我们用 selector 来管理之前的 ServerSocketChannel 和 SocketChannel ,使他们只有在自身事件被触发时才会执行;同时我们优化了服务端应对客户端主动或被动断开的情况,具体流程才考下面代码
补充:客户端除了发送数据会触发read事件,正常、异常关闭也会触发read事件,所以下面代码中对关闭进行了处理
  1. public class Service {
  2.     public static void main(String[] args) throws IOException {
  3.         ServerSocketChannel ssn = ServerSocketChannel.open();
  4.         ssn.configureBlocking(false);   // 非阻塞
  5.         ssn.bind(new InetSocketAddress(8888));
  6.         Selector selector = Selector.open();    // 创建一个 selector
  7.         // 将 ssn 注册到selector
  8.         SelectionKey ssnKey = ssn.register(selector, SelectionKey.OP_ACCEPT); // 指定通道(Channel)感兴趣的事件类型的: accept
  9.         while (true) {
  10.             selector.select();  // 无事件时阻塞代码
  11.             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
  12.             while (iterator.hasNext()) {
  13.                 SelectionKey key = iterator.next();
  14.                 iterator.remove();  // 删除我们当前要处理的 SelectionKey,
  15.                 if (key.isAcceptable()) {
  16.                     ServerSocketChannel channel = (ServerSocketChannel) key.channel();
  17.                     SocketChannel sc = channel.accept();    // 接受客户端连接
  18.                     sc.configureBlocking(false);    // 将连接到的客户端 SocketChannel 注册到selector
  19.                     sc.register(selector, SelectionKey.OP_READ);
  20.                 } else if (key.isReadable()) {
  21.                     ByteBuffer buffer = ByteBuffer.allocate(16);
  22.                     SocketChannel sc = (SocketChannel) key.channel();
  23.                     try {
  24.                         int len = sc.read(buffer);
  25.                         if (len < 0) {      // 当客户端主动关闭连接时,会返回 -1,需要处理 key,不然会让selector的事件一直处于激活状态
  26.                             key.cancel();
  27.                         }
  28.                     } catch (IOException e) {
  29.                         key.cancel();   // 当客户端因为异常断开连接时,会导致服务端抛出 IO 异常,手动处理key
  30.                     }
  31.                     buffer.flip();
  32.                     System.out.println(StandardCharsets.UTF_8.decode(buffer));
  33.                     buffer.clear();
  34.                 }
  35.             }
  36.         }
  37.     }
  38. }
复制代码
4.6 Selector优化3、4中的粘包和半包

上面代码中我们将 channle 注册到 selector 中时,只用到了两个参数,实在 register 也就第三个参数,第三个参数可以接受一个数组,作为 attachment 属性。
  1. SocketChannel sc = channel.accept();
  2. ByteBuffer buffer = ByteBuffer.allocate(1024);
  3. sc.register(selector, SelectionKey.OP_READ, buffer);        // 注册
  4. SelectionKey key = iterator.next();
  5. ByteBuffer attachment = (ByteBuffer) key.attachment();        // 获取数组
  6. key.attach(newBuffer);        // 可以通过 这个更新 attachment 属性
复制代码
在之前办理粘包和半包中,我们通过让客户端在每段消息中添加特别符号 \n。当没有遇到特别符号时,我们用 compact 让字节往数组前面压缩,但是这存在一个问题,如果一段消息的长度超过我们 buffer 的长度时,就会导致这段消息丢失。
要办理这个问题就需要在 buffer 容量不敷时创建一个新的 buffer,把之前的 buffer 内容拷贝进来。我们在每个客户端注册金 selector 时给他们各自分配一个 buffer,在 position == limit 雷同时(容量不敷)进行扩容操作即可。这里必须用attachment属性,因为他可以确保每个channel都有一个buufer。
总结:阻塞io,非阻塞io,多路复用io
特点阻塞 I/O非阻塞 I/O多路复用 I/O核心机制每个 I/O 操作会阻塞线程,直到完成非阻塞通道立即返回,无需等待一个线程管理多个通道的状态与线程的关系一个线程管理一个通道一个线程管理一个通道一个线程管理多个通道(利用 Selector)效率对比每个连接占用一个线程,效率低需要对每个通道轮询,效率较低通过 Selector 集中管理,效率更高应用场景小型应用或低并发场景小型 I/O 任务,低并发高并发网络服务,如 HTTP、WebSocket5. 多线程网络编程

这里我们写 一个用来接受连接的线程和2个处理读写的线程 的多线程demo来掌握nio中多线程中遇到的常见问题
最终效果如下,一个负责接受连接的线程,两个处理读写的线程,他们三个线程中都有一个 selector 来实现多路复用。

代码1

<ul>接受连接的线程。
代码中的 worker1.register(); 如果先于 sc.register(worker1.selector, SelectionKey.OP_READ); 执行,就会导致代码在 Worker  run 方法的 selector.select(); 阻塞住,会导致下面的 sc.register(worker1.selector, SelectionKey.OP_READ); 代码不会执行成功。
由于是多线程,这两个的执行顺序是不可控的。
[code]public class Service {    public static void main(String[] args) throws IOException {        ServerSocketChannel ssc = ServerSocketChannel.open();        ssc.configureBlocking(false);        ssc.bind(new InetSocketAddress(8888));        Selector selector = Selector.open();        ssc.register(selector, SelectionKey.OP_ACCEPT);        Worker worker1 = new Worker("worker1");                while (true) {            selector.select();            Iterator iter = selector.selectedKeys().iterator();            while (iter.hasNext()) {                SelectionKey key = iter.next();                iter.remove();                if (key.isAcceptable()) {                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();                    SocketChannel sc = channel.accept();                    sc.configureBlocking(false);                                        worker1.register();                //




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4