【Java学习之路】客官,了解一下 NIO 吧 ! ૮(˶ᵔ ᵕ ᵔ˶)ა

什么是 NIO ?

NIO,全称 New IO(新IO)或 Non-Blocking IO(非阻塞IO),使得当前程序在处理IO事务时不会影响其他程序的运行,而且可以在不使用多线程的情况下满足IO操作的需求。NIO包含三个核心部分:

通道(Channel): 用于文件操作和网络数据传递的通道。它提供了一种更灵活、高效的IO操作方式。

缓冲(Buffer): 缓冲的使用能够提高IO操作效率,减少不必要的读写次数,是NIO中的重要组成部分。

选择器(Selector): 是NIO的真正核心,用于管理多个通道的IO事件,实现非阻塞IO的核心机制。

通道(Channel)和 缓冲(Buffer)

Java NIO(New I/O)中的 Buffer 缓冲区和 Channel 通道是进行数据传输的重要组件。

Buffer 中,ByteBuffer 是最常用的字节缓冲区,同时还有其他基本数据类型对应的缓冲区,如 ShortBufferIntBufferCharBufferFloatBufferDoubleBuffer 等。以 ByteBuffer 为例:

ByteBuffer 常用方法:

  1. allocate(int capacity): 分配指定容量的字节缓冲区。
  2. get(): 从缓冲区中读取一个 byte。
  3. flip(): 翻转缓冲区,将限制设置为当前位置,然后将位置设置为 0,用于读取之前写入的数据。
  4. wrap(byte[] arr): 将字节数组包装为 ByteBuffer
  5. put(byte[] b): 将字节数组存入缓冲区。

Channel 接口中,常见的通道有 FileChannelDatagramChannelServerSocketChannelSocketChannel 等。以 FileChannel 为例:

FileChannel 常用方法:

  1. read(ByteBuffer buffer): 从通道中读取数据到 ByteBuffer 中。
  2. write(ByteBuffer buffer): 将数据从 ByteBuffer 写入通道。
  3. transferFrom(ReadableByteChannel src, long position, long count): 从指定的 srcChannel 中读取指定位置开始的 count 个元素到当前通道中,实现文件复制操作。
  4. transferTo(long position, long count, WritableByteChannel target): 将当前通道中的数据写入到指定的 target 通道中,从当前通道的 position 位置开始,写入 count 个元素。

一个简单的例子

java 复制代码
package com.qfedu.b_niofile;
​
import org.junit.Test;
​
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
​
/**
 * NIO文件操作测试类
 */
public class FileNioTest {
​
    /**
     * 通过NIO写入数据到文件中的操作
     */
    @Test
    public void testNioFileWrite() throws IOException {
        // 使用 try-with-resources 自动关闭资源
        try (FileOutputStream fos = new FileOutputStream("D:/aaa/1.txt");
             FileChannel foc = fos.getChannel()) {
​
            // 分配4KB缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 4);
​
            // 准备数据,放入缓冲区
            String str = "测试NIO";
            buffer.put(str.getBytes());
​
            // 翻转缓冲区,准备写入操作
            buffer.flip();
​
            // 缓冲区数据写入到通道中
            foc.write(buffer);
        }
    }
​
    /**
     * 通过NIO从文件中读取数据的操作
     */
    @Test
    public void testNioFileRead() throws IOException {
        // 使用 try-with-resources 自动关闭资源
        try (FileInputStream fis = new FileInputStream("D:/aaa/1.txt");
             FileChannel fic = fis.getChannel()) {
​
            // 分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
​
            // 从通道读取数据保存到缓冲区中
            int read = fic.read(buffer);
            System.out.println("Bytes read: " + read);
​
            // 输出缓冲区中的数据
            System.out.println("Content: " + new String(buffer.array(), 0, read));
        }
    }
​
    /**
     * 使用NIO进行文件复制的操作
     */
    @Test
    public void testCopyFile() throws IOException {
        long start = System.currentTimeMillis();
        // 使用 try-with-resources 自动关闭资源
        try (FileInputStream fis = new FileInputStream("D:/aaa/1.mp4");
             FileOutputStream fos = new FileOutputStream("D:/aaa/2.mp4");
             FileChannel srcChannel = fis.getChannel();
             FileChannel dstChannel = fos.getChannel()) {
​
            // 将数据从源通道传输到目标通道
            srcChannel.transferTo(0, srcChannel.size(), dstChannel);
        }
        long end = System.currentTimeMillis();
        System.out.println("Time:" + (end - start));
    }
​
    /**
     * 使用BufferedInputStream和BufferedOutputStream进行文件复制的操作
     */
    @Test
    public void testCopyUseBuffer() throws IOException {
        long start = System.currentTimeMillis();
        // 使用 try-with-resources 自动关闭资源
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/aaa/1.mp4"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:/aaa/3.mp4"))) {
​
            int length;
            byte[] buf = new byte[4 * 1024];
​
            // 读取数据到缓冲区,再写入目标文件
            while ((length = bis.read(buf)) != -1) {
                bos.write(buf, 0, length);
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("Time:" + (end - start));
    }
}

选择器(Selector)

Selector 是 Java NIO 中的一个关键组件。它允许一个单独的线程来管理多个通道(Channel),监控这些通道上的事件,当事件发生时,可以选择性地对这些事件进行响应。Selector 的主要作用是实现多路复用(Multiplexing),使得一个线程可以同时处理多个通道的 I/O 事件。

Selector常用方法

java 复制代码
// 获取一个选择器对象
public static Selector open();
​
// 监听所有注册通道,存在IO流操作时,将对应的信息SelectionKey存入内部集合
// 参数是一个超时时间,0表示无限期等待
public int select(long timeout);
​
// 返回当前Selector内部集合中保存的所有SelectionKey
public Set<SelectionKey> selectionKeys();

SelectionKey

java 复制代码
// 表示Selector和网络通道之间的关系
public interface SelectionKey {
    int OP_ACCEPT; // 16 需要连接
    int OP_CONNECT; // 8 已经连接
    int OP_READ;    // 1 读取操作
    int OP_WRITE;   // 4 写入操作
​
    // 获取与之关联的 Selector 对象
    public abstract Selector selector();
​
    // 获取与之关联的通道
    public abstract SelectableChannel channel();
​
    // 获取与之关联的共享数据
    public final Object attachment();
​
    // 设置或改变监听事件
    public abstract SelectionKey interestOps(int ops);
​
    // 是否可以 accept
    public final boolean isAcceptable();
​
    // 是否可以读
    public final boolean isReadable();
​
    // 是否可以写
    public final boolean isWritable();
}

网络编程 与 NIO

网络编程的特点

  • 通信:得满足通信能力,实现数据的传输与接收。
  • 异步:支持异步操作,即不需要等待一个操作完成才能执行下一个操作。
  • 多连接:能够处理多个连接,支持同时与多个客户端或服务端建立连接。
  • 并发性:具备处理多个任务的能力,能够同时处理多个请求或连接,提高系统的并发性。

传统 BIO 与 NIO 的对比

  1. 阻塞与非阻塞:

    • BIO: 阻塞I/O模型,每个I/O操作(读、写)都会阻塞线程,直到数据准备好或写入完成。
    • NIO: 非阻塞I/O模型,允许一个线程同时管理多个通道,通过选择器(Selector)实现,可以实现一个线程处理多个I/O操作。
  2. 连接数处理能力:

    • BIO: 对于每个连接都需要独立的线程,当连接数较多时,会导致线程数增加,资源消耗大。
    • NIO: 通过一个线程管理多个连接,避免了为每个连接创建独立线程,提高了连接数处理的能力。
  3. 缓冲:

    • BIO: 数据从流中读取或写入时,需要通过缓冲区进行。
    • NIO: 缓冲区的使用更加灵活,可以直接读写缓冲区,减少了数据从缓冲区到流的复制操作。
  4. 选择器(Selector):

    • BIO: 没有选择器概念,每个通道需要独立的线程。
    • NIO: 通过选择器,一个线程可以管理多个通道,实现了多路复用。
  5. 适用场景:

    • BIO: 适用于连接数较少且相对稳定的情况,例如传统的同步阻塞Socket。
    • NIO: 适用于连接数较多、连接时间短、通信频繁的情况,例如Web应用中的高并发处理

网络编程中使用的 通道 和 缓冲

  • 服务端Socket程序对应的Channel通道

    java 复制代码
    ServerSocketChannel
        
        // 开启服务器ServerSocketChannel通道,等于开始服务器程序
        public static ServerSocketChannel open();
    ​
        // 设置服务器端端口号
        public final ServerSocketChannel bind(SocketAddress local);
    ​
        // 设置阻塞或非阻塞模式, 取值 false 表示采用非阻塞模式
        public final SelectableChannel configureBlocking(boolean block);
    ​
        // [非阻塞] 获取一个客户端连接,并且得到对应的操作通道
        public SocketChannel accept();
    ​
        // [重点方法] 注册当前选择器,并且选择监听什么事件
        public final SelectionKey register(Selector sel, int ops);
  • 客户端Socket程序对应的Channel通道。

    java 复制代码
    SocketChannel
        // 打开一个Socket客户端Channel对象
        public static SocketChannel open();
    ​
        // 这里可以设置是阻塞状态,还是非阻塞状态,false表示非阻塞
        public final SelectableChannel configureBlocking(boolean block);
    ​
        // 连接服务器
        public boolean connect(SocketAddress remote);
    ​
        // 如果connect连接失败,可以通过finishConnect继续连接
        public boolean finishConnect();
    ​
        // 写入数据到缓冲流中
        public int write(ByteBuffer buf);
    ​
        // 从缓冲流中读取数据
        public int read(ByteBuffer buf);
    ​
        // 注册当前SocketChannel,选择对应的监听操作,并且可以带有Object attachment参数
        public final SelectionKey register(Selector sel, int ops, Object attachment);
    ​
        // 关闭SocketChannel
        public final void close();

NIO完成一个TCP聊天室

NIO TCP聊天室客户端

java 复制代码
​
/**
 * NIO 非阻塞状态的TCP聊天室客户端核心代码
 */
public class ChatClient {
    private static final String HOST = "192.168.31.154";
    private static final int PORT = 8848;
    private SocketChannel socket;
    private String userName;
​
    /**
     * 客户端构造方法,创建客户端对象
     *
     * @param userName 指定的用户名
     */
    public ChatClient(String userName) throws IOException, InterruptedException {
        // 1. 打开SocketChannel
        socket = SocketChannel.open();
​
        // 2. 设置非阻塞状态
        socket.configureBlocking(false);
​
        // 3. 根据指定的HOST IP地址和对应PORT端口号创建对应的 InetSocketAddress
        InetSocketAddress address = new InetSocketAddress(HOST, PORT);
​
        // 4. 连接服务器
        if (!socket.connect(address)) {
            // 如果没有连接到服务器,保持请求连接的状态
            while (!socket.finishConnect()) {
                System.out.println("服务器请求连接失败,等待2s继续请求连接...");
                Thread.sleep(2000);
            }
        }
​
        this.userName = userName;
​
        System.out.println("客户端 " + userName + " 准备就绪");
    }
​
    /**
     * 发送数据到服务器,用于广播消息,群聊
     *
     * @param message 指定的消息
     */
    public void sendMsg(String message) throws IOException {
        // 断开服务器连接 close
        if ("close".equals(message)) {
            socket.close();
            return;
        }
​
        message = userName + ":" + message;
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        socket.write(buffer);
    }
​
    /**
     * 接收服务器发送的数据
     */
    public void receiveMsg() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int length = socket.read(buffer);
        if (length > 0) {
            System.out.println(new String(buffer.array()));
        }
    }
}

NIO TCP聊天室服务端

java 复制代码
/**
 * NIO 非阻塞状态的TCP聊天室服务端核心代码
 *
 * @author Anonymous
 */
public class ChatServer {
​
    private ServerSocketChannel serverSocket;
    private Selector selector;
    private static final int PORT = 8848;
​
    /**
     * 服务器构造方法,开启ServerSocketChannel,同时开启Selector,注册操作
     *
     * @throws IOException 异常
     */
    public ChatServer() throws IOException {
        serverSocket = ServerSocketChannel.open();
        selector = Selector.open();
        serverSocket.bind(new InetSocketAddress(PORT));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
    }
​
    /**
     * 服务器干活方法,指定客户端绑定,数据接收和转发
     */
    public void start() {
        try {
            while (true) {
                if (0 == selector.select(2000)) {
                    System.out.println("服务器默默的等待连接,无人访问...");
                    continue;
                }
​
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
​
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 1. 连接
                    if (key.isAcceptable()) {
                        SocketChannel socket = serverSocket.accept();
                        socket.configureBlocking(false);
                        socket.register(selector, SelectionKey.OP_READ);
                        broadcast(socket, socket.getRemoteAddress().toString() + "上线了");
                    }
​
                    // 2. 接收数据转发
                    if (key.isReadable()) {
                        readMsg(key);
                    }
​
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 从指定的SelectionKey中读取数据
     *
     * @param key 符合OP_READ 要求的SelectionKey
     */
    public void readMsg(SelectionKey key) throws IOException {
        SocketChannel socket = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int length = socket.read(buffer);
​
        if (length > 0) {
            String message = new String(buffer.array());
            broadcast(socket, message);
        }
    }
​
    /**
     * 广播方法,该方法是群发消息,但是不要发给自己
     *
     * @param self    当前发送数据的客户端
     * @param message 消息
     */
    public void broadcast(SocketChannel self, String message) throws IOException {
        Set<SelectionKey> keys = selector.keys();
​
        for (SelectionKey key : keys) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel && !channel.equals(self)) {
                SocketChannel socketChannel = (SocketChannel) channel;
                ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                socketChannel.write(buffer);
            }
        }
    }
​
    public static void main(String[] args) throws IOException {
        new ChatServer().start();
    }
​
}

NIO TCP聊天室客户端线程开启

java 复制代码
import java.io.IOException;
import java.util.Scanner;
​
public class ChatClientThread {
​
    public static void main(String[] args) throws IOException, InterruptedException {
        Scanner scanner = new Scanner(System.in);
​
        System.out.println("请输入用户名:");
        String userName = scanner.nextLine();
​
        if (userName.length() == 0) {
            return;
        }
​
        ChatClient chatClient = new ChatClient(userName);
​
        // 接收消息
        new Thread(() -> {
            while (true) {
                try {
                    chatClient.receiveMsg();
                    Thread.sleep(2000);
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
​
        // 发送消息
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClient.sendMsg(msg);
        }
    }
}

搞定 ૮(˶ᵔ ᵕ ᵔ˶)ა

总结

一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉

觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天4 小时前
java的threadlocal为何内存泄漏
java
caridle4 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^4 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋34 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花4 小时前
【JAVA基础】Java集合基础
java·开发语言·windows