8.4 Java NIO

Java NIO(New Input/Output)是 Java 1.4 版本引入的新的 I/O API,它提供了与标准 I/O 不同的处理方式,主要区别在于 NIO 采用了非阻塞的 I/O 操作,并且基于通道(Channel)和缓冲区(Buffer)进行数据处理。NIO的核心思想是通过​​通道(Channel)​​、​​缓冲区(Buffer)​​和​​多路复用(Selector)​​实现高效的I/O操作,尤其适合网络服务器需要处理大量连接的场景。

NIO与传统IO(BIO)的区别​

​特性​​传统IO(BIO)​​NIO​
​模型​阻塞式(Blocking)非阻塞式(Non-blocking)
​数据流​面向流(Stream)面向缓冲区(Buffer)
​多线程处理​每个连接需一个线程,资源消耗大单线程处理多个连接(Selector)
​适用场景​低并发、简单连接高并发、大量短连接

核心组件

Java NIO 的核心组件包括:

  1. 通道(Channel)

    • 通道是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
    • 通道与流的不同之处在于,通道是双向的,而流是单向的(只能读或写)。
    • 主要实现类有:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel 等。
  2. 缓冲区(Buffer)

    • 缓冲区是一个用于存储特定基本类型数据的容器,本质上是一个数组。
    • 所有缓冲区都是 Buffer 抽象类的子类,主要实现类有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
  3. 选择器(Selector)

    • 选择器允许一个单独的线程来监视多个通道的输入,从而实现非阻塞 I/O。
    • 可以注册多个通道到一个选择器,然后使用一个线程来 "选择" 已准备好进行 I/O 操作的通道。

缓冲区(Buffer)

缓冲区是 NIO 中数据的容器,使用时需要了解几个核心概念:

  • 容量(Capacity):缓冲区能够容纳的数据元素的最大数量,在创建时设定,不能改变。
    • ByteBuffer(最常用)、CharBufferIntBuffer等。
  • 位置(Position):下一个要读取或写入的数据元素的索引。
  • 界限(Limit):缓冲区中可以操作的数据的界限,位于 limit 之后的数据不能读写。
  • 标记(Mark):一个备忘位置,调用 mark () 方法将 mark 设为当前的 position 值,之后可以通过 reset () 方法恢复到这个位置。

这四个属性之间的关系满足:0 <= mark <= position <= limit <= capacity

缓冲区的基本使用流程:

  1. 写入数据到 Buffer
  2. 调用 flip () 方法,将 Buffer 从写模式切换到读模式
  3. 从 Buffer 中读取数据
  4. 调用 clear () 方法或者 compact () 方法,将 Buffer 清空,以便再次写入

通道(Channel)

通道是数据传输的载体,主要有以下几种类型:

  • FileChannel:用于文件的数据读写。
  • SocketChannel:用于 TCP 网络数据读写。
  • ServerSocketChannel:允许我们监听 TCP 连接请求,为每个连接创建一个新的 SocketChannel。
  • DatagramChannel:用于 UDP 数据读写。

FileChannel 示例:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelExample {
    public static void main(String[] args) throws Exception {
        // 创建文件输入流
        FileInputStream fin = new FileInputStream("input.txt");
        // 获取输入流的通道
        FileChannel inChannel = fin.getChannel();
        
        // 创建文件输出流
        FileOutputStream fout = new FileOutputStream("output.txt");
        // 获取输出流的通道
        FileChannel outChannel = fout.getChannel();
        
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 从输入通道读取数据到缓冲区
        while (inChannel.read(buffer) != -1) {
            // 切换到读模式
            buffer.flip();
            
            // 从缓冲区写入数据到输出通道
            outChannel.write(buffer);
            
            // 清空缓冲区,准备下一次读取
            buffer.clear();
        }
        
        // 关闭通道和流
        inChannel.close();
        outChannel.close();
        fin.close();
        fout.close();
    }
}

选择器(Selector)

选择器是 Java NIO 中实现非阻塞 I/O 的关键组件,它允许一个线程处理多个通道。使用选择器的步骤如下:

  1. 创建 Selector 对象
  2. 将 Channel 注册到 Selector,并指定感兴趣的事件
  3. 调用 Selector 的 select () 方法,检查是否有通道准备好进行 I/O 操作
  4. 获取就绪通道的集合,进行相应的处理

Selector 支持的事件类型:

  • SelectionKey.OP_READ:通道有数据可读
  • SelectionKey.OP_WRITE:通道可以写入数据
  • SelectionKey.OP_CONNECT:通道建立了连接
  • SelectionKey.OP_ACCEPT:通道准备好接受新的连接

非阻塞服务器示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NonBlockingServer {
    public static void main(String[] args) throws IOException {
        // 创建选择器
        Selector selector = Selector.open();
        
        // 创建ServerSocketChannel并绑定端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        
        // 将ServerSocketChannel注册到Selector,并监听ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        System.out.println("服务器启动,监听端口8080...");
        
        while (true) {
            // 等待就绪的通道,select()方法会阻塞直到有通道就绪
            int readyChannels = selector.select();
            
            if (readyChannels == 0) {
                continue;
            }
            
            // 获取所有就绪的SelectionKey
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                
                // 处理接受连接事件
                if (key.isAcceptable()) {
                    // 获取服务器套接字通道
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    
                    // 接受客户端连接
                    SocketChannel socketChannel = ssc.accept();
                    System.out.println("接受新连接: " + socketChannel);
                    
                    // 设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    
                    // 注册到Selector,并监听READ事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } 
                // 处理读取数据事件
                else if (key.isReadable()) {
                    // 获取客户端套接字通道
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    
                    // 创建缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    
                    // 读取数据
                    int bytesRead = socketChannel.read(buffer);
                    
                    if (bytesRead > 0) {
                        // 切换到读模式
                        buffer.flip();
                        
                        // 处理数据
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        String message = new String(data, "UTF-8");
                        System.out.println("收到消息: " + message);
                        
                        // 回写数据
                        ByteBuffer response = ByteBuffer.wrap(("服务器已收到消息: " + message).getBytes());
                        socketChannel.write(response);
                    } else if (bytesRead == -1) {
                        // 客户端关闭连接
                        System.out.println("客户端关闭连接: " + socketChannel);
                        socketChannel.close();
                    }
                }
                
                // 移除处理过的key
                keyIterator.remove();
            }
        }
    }
}

客户端可以连接到服务器,发送消息并接收服务器的响应:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class NonBlockingClient {
    public static void main(String[] args) {
        try (
            // 创建SocketChannel并连接到服务器
            SocketChannel socketChannel = SocketChannel.open();
            // 创建Scanner用于读取用户输入
            Scanner scanner = new Scanner(System.in)
        ) {
            // 连接到服务器
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            System.out.println("已连接到服务器");
            
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            // 读取用户输入并发送消息
            while (true) {
                System.out.print("请输入消息(输入exit退出):");
                String message = scanner.nextLine();
                
                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }
                
                // 发送消息到服务器
                buffer.clear();
                buffer.put(message.getBytes("UTF-8"));
                buffer.flip();
                socketChannel.write(buffer);
                
                // 接收服务器响应
                buffer.clear();
                int bytesRead = socketChannel.read(buffer);
                
                if (bytesRead > 0) {
                    buffer.flip();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    String response = new String(data, "UTF-8");
                    System.out.println("服务器响应:" + response);
                }
            }
            
            System.out.println("客户端关闭");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码说明:

  1. 建立连接

    • 创建SocketChannel并连接到服务器的地址和端口(localhost:8080)
  2. 消息发送

    • 使用标准输入读取用户输入的消息
    • 将消息编码为 UTF-8 格式并放入 ByteBuffer
    • 通过 SocketChannel 将消息发送到服务器
  3. 响应接收

    • 从 SocketChannel 读取服务器响应
    • 将接收到的数据解码为字符串并打印
  4. 资源管理

    • 使用 try-with-resources 自动关闭 SocketChannel
    • 输入 "exit" 时退出客户端程序

使用方法:

  1. 先启动服务器程序(NonBlockingServer)
  2. 再启动客户端程序(NonBlockingClient)
  3. 在客户端输入消息并按回车发送
  4. 查看服务器响应
  5. 输入 "exit" 结束会话

运行效果图:

阻塞与非阻塞 I/O

Java NIO 的非阻塞模式是其核心优势之一:

  • 阻塞 I/O:传统的 Java I/O 是阻塞的,当一个线程调用 read () 或 write () 方法时,该线程会被阻塞,直到有数据可读或数据完全写入,在此期间线程不能做其他事情。
  • 非阻塞 I/O:Java NIO 的非阻塞模式允许线程在没有数据可读时立即返回,继续执行其他任务,而不是被阻塞。通过 Selector,可以监视多个通道的 I/O 状态,一个线程可以处理多个通道的请求。

非阻塞 I/O 的优势在于:

  • 减少线程数量,降低系统开销
  • 提高系统吞吐量和响应速度
  • 更高效地利用系统资源

应用场景

Java NIO 适合以下场景:

  • 高并发连接:需要处理大量并发连接的应用,如服务器、中间件等。
  • 实时通信:需要实时响应的应用,如即时通讯、游戏服务器等。
  • I/O 密集型应用:需要处理大量 I/O 操作的应用,使用 NIO 可以减少线程数量,提高系统效率。

Java NIO 通过通道、缓冲区和选择器提供了非阻塞的 I/O 操作方式,相比传统的 I/O 更加高效,尤其适合处理大量并发连接的场景。理解通道、缓冲区和选择器的工作原理,以及它们之间的协作方式,是掌握 Java NIO 编程的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chxii

小小打赏,大大鼓励!

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

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

打赏作者

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

抵扣说明:

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

余额充值